typescript - Typ-säkert sätt att begränsa typen av arrayer efter längd när noUncheckedIndexedAccess är sant


Givet en funktion som tar ett enda string[]-argument myArray.

Om jag utvärderar .length-egenskapen och den egenskapen är större än 0, då (förutsatt att min variabel inte är any i förklädnad) är det omöjligt för myArray[0] att vara undefined. Men om jag aktiverar noUncheckedIndexedAccess, kommer dess typ att vara string | undefined

const myFunc = (myArray: string[]) => {
   if(myArray.length > 0) {
     const foo = myArray[0] // now typed at 'string | undefined'
   }
}

Nu kan jag ändra if-satsen för att utvärdera myArray[0], och undefined tas bort från typen som man förväntar sig. Men vad händer om jag nu vill kontrollera att längden på arrayen är större än 6. Jag vill inte behöva göra samma sak för index 0-5 för att korrekt begränsa typen. t.ex:

const myFunc = (myArray: string[]) => {
   if(myArray[0] && myArray[1] && myArray[2] && myArray[3] && myArray[4] && myArray[5]) {
      const foo = myArray[0] // now typed at 'string', but this is uggggly
   }
}

Finns det ett mer elegant sätt att avgränsa typen baserat på längden av arrayen eller kommer jag att behöva titta på att bidra till TypeScript-koden?

Svar #1

Som du misstänkte, smalnar TypeScript inte automatiskt typen av en array baserat på en kontroll av dess length-egenskap. Detta har föreslagits tidigare i microsoft/TypeScript#38000, som är markerad som "för komplex". Det verkar som att det hade föreslagits innan dess, i microsoft/TypeScript#28837, som fortfarande är öppet och markerat som "väntar på mer feedback". Möjligen skulle du kunna gå till det ärendet och lämna feedback om varför en sådan sak skulle vara användbar för dig och varför de nuvarande lösningarna inte är tillräckliga, men jag vet inte om det kommer att ha någon större effekt. Hur som helst tvivlar jag på att TS-teamet just nu tar emot pull requests för att implementera en sådan funktion.

I avsaknad av någon automatisk begränsning kan du istället skriva en användardefinierad typvaktfunktion som har önskad effekt. Här är en möjlig implementation:

type Indices<L extends number, T extends number[] = []> =
    T['length'] extends L ? T[number] : Indices<L, [T['length'], ...T]>;

type LengthAtLeast<T extends readonly any[], L extends number> = 
  Pick<Required<T>, Indices<L>>

function hasLengthAtLeast<T extends readonly any[], L extends number>(
    arr: T, len: L
): arr is T & LengthAtLeast<T, L> {
    return arr.length >= len;
}

Typen Indices<L> är avsedd att ta ett enda, relativt litet, icke-negativt heltal som numeriskt litteraltyp L och returnera en union av de numeriska indexen för en array med längd L. Ett annat sätt att säga detta är att Indices<L> bör vara en union av de icke-negativa heltalen mindre än L. Observera:

type ZeroToNine = Indices<10>
// type ZeroToNine = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

Den här typen fungerar genom att använda rekursiva villkorstyper tillsammans med variadiska tuplatyper för att gå från 0 upp till L. Rekursiva typer tenderar att fungera bra tills de inte gör det, och en stor varning här är att saker kommer att bli konstiga eller till och med kasta fel om du skickar in ett L som är för stort, eller bråktal, eller negativt, eller number, eller en union. Det är en stor varning för detta tillvägagångssätt.

Nästa tar LengthAtLeast<T, L> en arraytyp T och en längd L, och returnerar ett objekt som är känt för att ha egenskaper på alla index i en array med minst längd L. Så här:

type Test = LengthAtLeast<["a"?, "b"?, "c"?, "d"?, "e"?], 3>
/* type Test = {
    0: "a";
    1: "b";
    2: "c";
} */

type Test2 = LengthAtLeast<string[], 2>
/* type Test2 = {
    0: string;
    1: string;
} */

Slutligen är hasLengthAtLeast(arr, len) typbevakningsfunktionen. Om den returnerar true, så begränsas arr från typen T till T & LengthAtLeast<T, L>. Låt oss se den i aktion:

const myFunc = (myArray: string[]) => {
    if (hasLengthAtLeast(myArray, 6)) {
        myArray[0].toUpperCase(); // okay
        myArray[5].toUpperCase(); // okay
        myArray[6].toUpperCase(); // error, possibly undefined
    }
}

Ser bra ut. Kompilatorn tillåter gärna att du behandlar myArray[0] och myArray[5] som definierade, men myArray[6] är fortfarande möjligt att vara odefinierad.


Hur som helst, om du bestämmer dig för att använda en typkontroll, kanske du vill balansera komplexitet mot hur mycket du faktiskt behöver använda den. Om du bara kontrollerar längden på några ställen kan det vara värt att helt enkelt använda en icke-null-påståendesoperator som myArray[0]!.toUpperCase() och inte oroa dig för att få kompilatorn att verifiera typkontroll åt dig.

Eller, om du inte har kontroll över värdet av len, kanske du inte vill ha en bräcklig rekursiv villkorlig typ, och istället bygga något mer robust men mindre flexibelt (som kanske en överlagrad typvakt som bara fungerar för specifika len-värden som i en kommentar på microsoft/TypeScript#38000).

Allt handlar om dina användningsfall.

Lekplatslänk till kod

Svar #2

Om jag utvärderar .length-egenskapen och den egenskapen är större än 0, då (förutsatt att min variabel inte är any i förklädnad) är det omöjligt för myArray[0] att vara undefined.

Observera att, strikt taget, är detta faktiskt möjligt för glesa matriser. Som ett motexempel:

let xs /* : number[] */ = new Array<number>(100);
console.log(xs.length); // -> 100
console.log(xs[0]);     // -> undefined

I koden ovan är xs:s length-egenskap 100, större än noll, och ändå utvärderas xs[0] till undefined. Detta beror på att xs är en gles array.

Egenskapen length motsvarar antalet element i en tät array. Men om arrayen är glest befolkad är antalet element strikt mindre än värdet på length-egenskapen, och elementen kan starta vid ett icke-noll-index.

Relaterade artiklar