Weboldal keresés

visual studio code - Egy érdekes történet a típuskövetkeztetésről RxJs-sel és TypeScripttel


A következő kódom van

  let obs$: Observable<string>
   obs$.pipe(
        tap(data => {
          // do stuff
        }),
      );

Ha a VSCode-ot használom, és az egérrel a data változó fölé viszem, látom, hogy ennek típusa a várt módon string.

Aztán hozzáadom a ingnoreElements-t a tap után

obs$.pipe(
    tap(data => {
       // do stuff
    }),
    ignoreElements()
  );

Most, ha ráviszem az egeret a data-ra, a típusbecslés azt mondja, hogy a data típusa any.

Ez retroaktív következtetésnek tűnik. Mi az oka ennek a típus-következtetési viselkedésnek?

Itt egy stackblitz ennek a helyzetnek a reprodukálásához.

A VÁLASZ @Andrei Gătej válasza

Az egész rejtély abban rejlik, ahogyan a ignoreElements() van deklarálva, azaz.

export declare function ignoreElements(): OperatorFunction<any, never>;

Most a ignoreElements()-t egy olyan operátor után helyezed el, mint a tap(), amelyet így deklarálnak

export declare function tap<T>(observer: PartialObserver<T>): MonoTypeOperatorFunction<T>;

és alapvetően a pipe() belsejében van egy ilyen lánc

MonoTypeOperatorFunction<T>,
OperatorFunction<any, never>

amelyet a pipe() így értelmez: „T” egy generikus típus, az „any” egy valós típus, tehát mivel az ignoreElements() „any”-t vár, ezért „T”-nek is az „any” típusúnak kell lennie

És ez magyarázza a rejtélyt.

Most néhány lehetséges megoldás van. Az egyik mód, hogy az ignoreElements()-t egy külön pipe()-ba izoláljuk így

obs$.pipe(
    tap(data => {
       // do stuff
    }),
  )
  .pipe(
    ignoreElements()
  );

Ha ezt megteszed, a data változó típusa string lesz, ami az, amit szeretnénk.

Egy elegánsabb megoldás azonban az lenne, ha megváltoztatnánk az ignoreElements() deklarációját. Ha így deklarálod

export declare function ignoreElements<T>(): OperatorFunction<T, never>;

Minden úgy működik, ahogy várható, anélkül, hogy bármilyen trükköt kellene alkalmazni.

Érdemes lehet javasolni a ignoreElements() deklarációjának megváltoztatását.

Válasz #1

Ez egy nagyon érdekes kérdés!

Nézzük meg az ignoreElements() típusait:

export function ignoreElements(): OperatorFunction<any, never> {
  return function ignoreElementsOperatorFunction(source: Observable<any>) {
    return source.lift(new IgnoreElementsOperator());
  };
}

Forrás.

A legfontosabb dolog, amit itt észre kell venni, az a OperatorFunction, amely így van definiálva:

export interface UnaryFunction<T, R> { (source: T): R; }

export interface OperatorFunction<T, R> extends UnaryFunction<Observable<T>, Observable<R>> {}

Forrás.

Tehát a UnaryFunction<T, R> olyan függvényt ír le, amely egy paramétert vesz T típusúként, és visszaad valamit R típusúként.

Hasonlóképpen, a OperatorFunction<T, R> egy olyan függvény, amely egy T típusú Observable típusú argumentumot fogad, és egy R típusú Observable értéket ad vissza.

Térjünk vissza az ignoreElements() típusaihoz: OperatorFunction<any, never>. Tehát ez azt jelenti, hogy az ignoreElements egy függvényt ad vissza, amelynek argumentuma egy Observable<any>, és a visszatérési típusa egy Observable<never>.

Most nézzük meg a pipe() típusait:

pipe(): Observable<T>;
pipe<A>(op1: OperatorFunction<T, A>): Observable<A>;
pipe<A, B>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>): Observable<B>;
pipe<A, B, C>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>, op3: OperatorFunction<B, C>): Observable<C>;

Forrás.

Minden operátor egy OperationFunction-t ad vissza. Például, innen

pipe<A, B>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>): Observable<B>;

Láthatjuk, hogy az első operátor visszatérési típusa a második operátor bemeneti típusa, és így tovább. Így történik a típusok kikövetkeztetése.

A T típusa szintén kikövetkeztethető az Observable konstruktorából:

constructor(subscribe?: (this: Observable<T>, subscriber: Subscriber<T>) => TeardownLogic) {
  if (subscribe) {
    this._subscribe = subscribe;
  }
}

class Subscriber<T> implements Observer<T> { /* ... */ }

export interface Observer<T> {
  closed?: boolean;
  next: (value: T) => void; // ! 
  error: (err: any) => void;
  complete: () => void;
}

Forrás.

Kapcsolódó cikkek