typescript - RxJS filter-funktion begränsar inte typen om den inte direkt ges en typvakt som är dess enda parameter


Jag har arbetat på en autentiseringstjänst som använder ett rxjs behavior subject för att lagra det senaste hämtade autentiseringsobjektet och utlöser en återhämtning om det har gått ut (eller inte har hämtats alls än).

Min fråga handlar om TypeScript-typkontrollanten. Jag har skrivit typvakten isNotUndefined som fastställer - tja, precis vad du skulle förvänta dig.

export function isNotUndefined<T>(input: T | undefined): input is T {
  return input !== undefined;
}

Jag har redan varit tvungen att skriva ovanstående typkontroll istället för att kunna förlita mig på auth !== undefined. Jag kan helt enkelt inte förstå varför typen på värdet i pipen i authGetter$ i koden nedan inte reduceras ner till bara Auth efter det första filtret. Istället är typen fortfarande Auth | undefined, och det krävs det andra filtret med bara typkontrollen för att typen ska smalnas ner till bara Auth.

Så sammanfattningsvis, varför behöver jag det andra filtret för att begränsa typen till bara Auth? Dessutom, eftersom jag programmerar på egen hand utan att någon granskar det, skulle jag vara mycket tacksam om någon påpekade de 'kodluktar' de känner igen (med förslag på vad man bör göra istället).

export default class AuthService {
  private static lastAuth$ = new BehaviorSubject<Auth | undefined>(undefined);

  private static authGetter$ = AuthService.lastAuth$.pipe(
    filter(auth => {
      if (isNotUndefined(auth) && auth.expiry > new Date()) {
        return true ; // identical resulting type with "return isNotUndefined(auth);"
      } else {
        // retry if auth doesn't exist or is expired
        AuthService.authorise().then(newAuth =>
          AuthService.lastAuth$.next(newAuth)
        );
        return false;
      }
    }),
    tap(v => {}), // typechecker says "(parameter) v: Auth | undefined"
    filter(isNotUndefined),
    tap(v => {}) // typechecker says "(parameter) v: Auth"
  );

  static getAuth$(): Observable<Auth> {
    return this.authGetter$.pipe(first());
  }

  private static async authorise(): Promise<Auth> {
    // auth code goes here (irrelevant for this question)...
    // typecast dummy return to make typechecker happy
    return Promise.resolve(<Auth>{});
  }
}

Jag bifogar ett foto av min kod med fin syntaxmarkering för din bekvämlighet och nöje :)

Svar #1

Uppdatering för TypeScript 5.5:

TypeScript härleder nu typavkastning för typ-predikat från funktionskroppar, så nu bör auth !== undefined i kroppen av din funktion verkligen betraktas som en typvakt. Det vill säga, typvaktfunktioner kommer nu att sprida:

const doesPropagate = <T>(x: T | undefined) => isNotUndefined(x);
// const doesPropagate: <T>(x: T | undefined) => x is T

Så du kommer att få det önskade filterbeteendet om du skriver en giltig typvaktkropp:

const b = o.pipe(filter(x => isNotUndefined(x))); // Observable<string>

Osäkra typ-skärmar (type guard) kommer inte att begränsa typen, vilket dock är bra. Du vill inte att (x: string | undefined) => isNotUndefined(x) && x.length > 3 ska tolkas som en typ-skärm, eftersom när den returnerar false kan du inte anta att x är undefined, vilket är vad en typ-skärmfunktion skulle göra. Detta är inte ett problem för filter() som endast ser på true-fallet, men det är fortfarande inte något du vill att TS ska tolka generellt. Så här skulle du fortfarande behöva annotera returtypen:

const d = o.pipe(filter((x): x is string => isNotUndefined(x) && x.length > 3)); 
// Observable<string>;

Det är fortfarande en bättre idé att göra flera filter om du vill ha detta beteende, dock:

const e = o.pipe(filter(isNotUndefined), filter(x => x.length > 3)); // Observable<string>;

Före TypeScript 5.5:

Användardefinierade typvaktfunktioner är, åtminstone för närvarande, strikt användardefinierade. De härleds inte automatiskt av kompilatorn. Om du vill att en boolean-returnerande funktion ska fungera som en typvakt med en typ-predikat returtyp, behöver du uttryckligen annotera den som sådan:

const doesNotPropagate = <T>(x: T | undefined) => isNotUndefined(x);
// const doesNotPropagate: <T>(x: T | undefined) => boolean

Funktionen doesNotPropagate() beter sig på samma sätt som isNotUndefined() vid körning, men kompilatorn ser den inte längre som en typvakt, så om du använder den som ett filter kommer du inte att eliminera undefined i kompilatorn.

Det finns flera problem på GitHub om detta; det för närvarande öppna problemet som spårar spridning/flöde av typbevakningssignaturer är microsoft/TypeScript#16069 (eller eventuellt microsoft/TypeScript#10734). Men det ser inte ut att hända mycket här, så för tillfället måste vi bara hantera språket som det är.


Här är ett litet exempel att leka med för att utforska de olika möjliga sätten att hantera detta, eftersom exempel koden i din fråga inte utgör ett minimalt reproducerbart exempel som är lämpligt att använda i en fristående IDE. Du bör kunna anpassa dessa till din egen kod.

Säg att vi har ett värde o av typen Observable<string | undefined>. Då fungerar detta:

const a = o.pipe(filter(isNotUndefined)); // Observable<string>

men detta gör det inte på grund av anledningen som listas ovan... typskydds-signaturer sprider sig inte:

const b = o.pipe(filter(x => isNotUndefined(x))); // Observable<string | undefined>

Vi kan återfå typvaktens signatur och beteende om vi manuellt annoterar pilfunktionen på detta sätt:

const c = o.pipe(filter((x): x is string => isNotUndefined(x))); // Observable<string>;

Utifrån detta kan du göra extra filtreringslogik om du vill:

const d = o.pipe(filter((x): x is string => isNotUndefined(x) && x.length > 3)); 
// Observable<string>;

Här kontrollerar filtret att strängen är definierad och att dess längd är större än 3.

Observera att detta tekniskt sett inte är en välfungerande användardefinierad typvakt, eftersom de tenderar att behandla false-resultat som att det innebär att indata är begränsad till att utesluta den bevakade typen:

function badGuard(x: string | undefined): x is string {
  return x !== undefined && x.length > 3;
}
const x = Math.random() < 0.5 ? "a" : undefined;
if (!badGuard(x)) {
  x; // narrowed to undefined, but could well be string here, oops
}

Här, om badGuard(x) returnerar true, vet du att x är string. Men om badGuard(x) returnerar false, vet du inte att x är undefined... men det är vad kompilatorn tror.

Det är sant att i din kod hanterar du egentligen inte situationen där filtret returnerar false (jag antar att de efterföljande pipe-parametrarna bara inte körs?), så du behöver egentligen inte oroa dig för mycket över detta. ändå kan det vara bättre att omstrukturera koden till en korrekt typkontroll följd av filtret som inte är en typkontroll och som utför den extra logiken:

const e = o.pipe(filter(isNotUndefined), filter(x => x.length > 3)); // Observable<string>;

Detta bör ge samma resultat vid körning, men här smalnar det första filtret korrekt ner från Observable<string | undefined> till Observable<string>, och det andra filtret behåller Observable<string> (och x i callbacken är en string) och utför den extra logiken som filtrerar på längd.

Och detta har den extra fördelen att det inte kräver en typannotering, eftersom du inte försöker sprida typen av vaktsignaturen någonstans. Så detta skulle förmodligen vara den metod jag skulle rekommendera.


Stackblitz-länk till kod

Relaterade artiklar