typescript - Vad uppnås genom att kontrollera om en generisk tuppel utökar ett uttryck inom hakparenteser?


Så jag har detta fabriksdesigner där jag kan skapa nya instanser av mina tjänstklasser med rätt typer. För detta behöver jag en typ där jag extraherar en enda nyckel:

type ServiceSingleKeys<T> = [T] extends (
  T extends 'foo' | 'oof' ? [T] : never
)
  ? T
  : never;

Koden ovan är från ett kodexempel jag hittade, men exemplet är inte förklarat. Jag har tyvärr tappat bort länken till exemplet så jag kan inte ge länken här.

Här är en mer detaljerad reproduktion: https://stackblitz.com/edit/typescript-ha1bha?file=index.ts

Vad gör [T]? (Besvarad i kommentar --> Tuples)

Vilken effekt uppnås genom att först kontrollera om [T] utvidgas till (T extends ServiceKeys ? [T] : never)? Hur fungerar detta eller med andra ord, vad gör detta? Enligt min förståelse kan jag uppnå samma effekt från typen med detta

type ServiceSingleKeys<T> = T extends 'foo' | 'oof' ? T : never;

Svar #1

Koden

type ServiceSingleKeys<T> = [T] extends (
  T extends 'foo' | 'oof' ? [T] : never
) ? T : never;

har effekten av att kontrollera om hela indatatypen T är tilldelningsbar till unionstypen "foo" | "oof". Om den är det, returnerar den T. Om inte, returnerar den never. Så du får detta beteende:

type A = ServiceSingleKeys<"foo" | "oof"> // "foo" | "oof"
type B = ServiceSingleKeys<"foo"> // "foo"
type C = ServiceSingleKeys<"foo" | "bar"> // never

Typ A och B är samma som deras indata, eftersom "foo" | "oof" och "foo" båda kan tilldelas till "foo" | "oof". Typ C är never, eftersom "foo" | "bar" inte kan tilldelas till "foo" | "oof".


Detta skulle kunna skrivas om enklare som:

type ServiceSingleKeys<T> = [T] extends ['foo' | 'oof'] ? T : never;

type A = ServiceSingleKeys<"foo" | "oof"> // "foo" | "oof"
type B = ServiceSingleKeys<"foo"> // "foo"
type C = ServiceSingleKeys<"foo" | "bar"> // never

Och faktiskt, så här skulle jag ha skrivit en sådan typ om jag behövde. Jag kan inte säga varför den ursprungliga versionen är skriven som den är. Mitt antagande skulle vara att den sattes ihop genom försök och misstag. (Om det visar sig att det finns något gränsfall där dessa två versioner inte är funktionellt ekvivalenta, då är kanske det gränsfallet anledningen, men utan mer information är jag skeptisk).

Du kan inte ta bort tuppeltypens omslag från kontrollen utan att ändra beteendet:

type NotServiceSingleKeys<T> = T extends 'foo' | 'oof' ? T : never;

type A = NotServiceSingleKeys<"foo" | "oof"> // "foo" | "oof"
type B = NotServiceSingleKeys<"foo"> // "foo"
type C = NotServiceSingleKeys<"foo" | "bar"> // "foo" <-- difference

Det beror på att en villkorlig typ där den kontrollerade typen är en bar generisk typparameter är en distributiv villkorlig typ, där indatatypen delas upp i sina individuella unionselement innan den utvärderas, och utdata sammanfogas till en ny union. Så NotServiceSingleKeys<"foo" | "bar"> utvärderas som NotServiceSingleKeys<"foo"> | NotServiceSingleKeys<"bar"> vilket blir "foo" | aldrig eller bara "foo".

Distributiva konditionstyper är ofta vad folk vill se (detta gör att du kan få ett union-filtreringsbeteende som Extract<T, U>-hjälptypen), men när det är oönskat är lösningen vanligtvis att omsluta båda sidor av extends-kontrollen i en-tuplar. Kontroll [T] extends ['foo' | 'oof'] är icke-distributiv eftersom [T] inte är en ren generisk typparameter, men kontrollen är liknande eftersom tuplar är kovarianta (vilket betyder att [XXX] kan tilldelas [YYY] om och endast om XXX kan tilldelas YYY). Som det står i dokumentationen som länkats ovan:

Typiskt sett är distributivitet det önskade beteendet. För att undvika det beteendet kan du omge varje sida av extends-nyckelordet med hakparenteser.

Återigen, mitt antagande här är att den ursprungliga versionen av koden var ett försök-genom-fel-försök att tillämpa det rådet för att förhindra att kontrollen av misstag blev distributiv. Om det finns något annat syfte med den speciella nästlade villkors typen i frågan, ser jag det inte.


Lekplatslänk till kod