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.
