typescript - Hur kontrollerar man om en tupel innehåller en viss typ inom en villkorlig typ?
Från och med TypeScript 4.2 är det trivialt att kontrollera om en tuplet-typ börjar eller slutar med en specifik typ:
type HeadIsString<T extends any[]> = T extends [string, ...any[]] ? true : false;
type TailIsString<T extends any[]> = T extends [...any[], string] ? true : false;
Jag skulle dock vilja kontrollera om en tuppel innehåller en viss typ på vilken position som helst i tuppeln. Något sådant är för närvarande inte möjligt
type IncludesString<T extends any[]> = T extends [...any[], string, ...any[]] ? true : false;
eftersom en tuppel inte kan ha mer än ett rest-element. Den enda lösningen jag kan komma på är att först göra tuplen till en union.
type IncludesString<T extends any[]> = string extends T[number] ? true : false;
Mitt specifika användningsfall är, givet en array av funktioner, att veta om någon av dem returnerar ett Promise. För detta specifika användningsfall är ovanstående lösning tillräcklig. Jag kan dock se andra scenarier där detta tillvägagångssätt inte skulle fungera. Till exempel, där tupelelement själva kan vara unioner, men en typ som string | null skulle vara oönskad. Så i detta exempel skulle [number, string | null] omvandlas till number | string | null och typen skulle utvärderas till true.
För att klargöra, jag letar efter en typ som, i vårt exempel, skulle utvärderas enligt följande:
IncludesString<[]> // false
IncludesString<[number]> // false
IncludesString<[[string]]> // false
IncludesString<[string, number]> // true
IncludesString<[number, string, number]> // true
Användningen av string i detta exempel är godtycklig -- jag letar bara efter ett generellt tillvägagångssätt som skulle undvika det ovan nämnda problemet med att använda en union.
Svar #1
Här är ett möjligt tillvägagångssätt:
type IncludesString<T extends any[]> =
{ [I in keyof T]: T[I] extends string ? unknown : never }[number] extends
never ? false : true;
Idén är att vi vill mappa tuppeltypen T till en annan tuppel, där vi omvandlar allt som utvidgar string till typen unknown som är "översta" typen, och allt som inte utvidgar string till typen never som är "lägsta" typen. Så till exempel skulle [string, number, string | number, "abc"] transformeras till [unknown, never, never, unknown], eftersom både string och "abc" utvidgar string, medan både number och string | number inte gör det.
När vi väl har den mappade tuplen, indexerar vi den med number för att få unionen av alla element i den mappade tuplen. I det tidigare exemplet är det [unknown, never, never, unknown][number], vilket blir unknown | never | never | unknown, vilket blir unknown. TypeScript kollapsar omedelbart alla unioner av formen A | unknown till unknown och alla unioner av formen A | never till A. Det innebär att när man förenar ihop en mängd typer som antingen är unknown eller never, kommer resultatet att bli unknown om minst en av ingångstyperna är unknown, och resultatet kommer att bli never om ingen av ingångstyperna är unknown.
Och eftersom vi har transformerat tuplen så att allt som utvidgar string blir unknown och allt som inte utvidgar string blir never, kommer den resulterande unionen att vara unknown om och endast om det finns minst ett medlem i den ursprungliga tuplen som utvidgar string.
Så nu är allt vi behöver göra att mappa unknown till true och never till false. Vi gör det med extends never ? false : true (eftersom unknown extends never är false, och never extends never är true).
Låt oss prova det:
type X0 = IncludesString<[]> // false
type X1 = IncludesString<[number]> // false
type X2 = IncludesString<[[string]]> // false
type X3 = IncludesString<[string, number]> // true
type X4 = IncludesString<[number, string, number]> // true
type X5 = IncludesString<[string | number]>; // false
type X6 = IncludesString<["abc"]> // true
type X7 = IncludesString<[string] | [number]> // true?
De flesta av dessa är exakt det resultat du ville ha. Den sista, däremot, IncludesString<[string] | [number]> är true, och det är inte klart om det är vad du vill. Kanske vill du ha true | false som resultat. Eller kanske false. Vem vet? Det finns alltid gott om kantfall involverade i anpassade typfunktioner som denna, och det enda sättet att hantera det är att testa noggrant mot dina faktiska användningsfall och justera definitionen därefter. Men jag anser att det ligger utanför frågans omfattning som den ställdes.
Playground-länk till kod
Svar #2
Kartlagda typer är inte nödvändiga för detta. Du kan indexera tupeln på number-typen för att göra den till en union, sedan kan du kontrollera om din typ utvidgar den.
type Tuple = [string, number]
// true
type HasString = string extends Tuple[number] ? true : false
// false
type HasBoolean = boolean extends Tuple[number] ? true : false
Du kan skapa en nyttotyp för att göra detta generiskt:
type Has<Tuple extends any[], Type> = Type extends Tuple[number] ? true : false
// true
type HasSymbol = Has<[string, symbol, number], symbol>
// false
type HasNumber = Has<[string, boolean], number>
Eller så kan du bara använda detta mönster i dina egna typer.
Lekplatslänk
