Given a function that takes a single string[]
argument myArray
.
If I evaluate the .length
property and that property is greater than 0, then (assuming my variable isn't any
in disguise) its impossible for myArray[0]
to be undefined. However, if I enable noUncheckedIndexedAccess
, its type will be string | undefined
const myFunc = (myArray: string[]) => {
if(myArray.length > 0) {
const foo = myArray[0] // now typed at 'string | undefined'
}
}
Now I can change the if statement to evaluate myArray[0]
, and undefined
is removed from the type as you'd expect. But what if I now want to check that the length of the array is greater than 6. I don't want to have to do the same thing for indexes 0-5 to correctly narrow down the type. e.g:
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
}
}
Is there a more elegant way of narrowing the type based on the length of the array or am I going to have to look into contributing to the TypeScript codebase?
As you suspected, TypeScript does not automatically narrow the type of an array based upon a check of its length
property. This has been suggested before at microsoft/TypeScript#38000, which is marked as "too complex". It looks like it had been suggested prior to that, at microsoft/TypeScript#28837, which is still open and marked as "awaiting more feedback". Possibly you could go to that issue and leave feedback as to why such a thing would be helpful to you and why the current solutions aren't sufficient, but I don't know that it'll have much effect. Either way I doubt that the TS team is taking pull requests to implement such a feature right now.
In the absence of any automatic narrowing, you could instead write a user defined type guard function that has the effect you want. Here's one possible 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;
}
The Indices<L>
type is intended to take a single, relatively small, non-negative, integral numeric literal type L
and return a union of the numeric indices of an array with length L
. Another way to say this is that Indices<L>
should be a union of the nonnegative whole numbers less than L
. Observe:
type ZeroToNine = Indices<10>
// type ZeroToNine = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
This type works by leveraging recursive conditional types along with variadic tuple types to walk from 0
up to L
. Recursive types tend to work fine until they don't, and one big caveat here is that things will be weird or even throw errors if you pass in an L
that is too large, or fractional, or negative, or number
, or a union. That's a big caveat for this approach.
Next, LengthAtLeast<T, L>
takes an array type T
and a length L
, and returns an object which is known to have properties at all the indices of an array of at least length L
. Like this:
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;
} */
Finally, hasLengthAtLeast(arr, len)
is the type guard function. If it returns true
, then arr
is narrowed from type T
to T & LengthAtLeast<T, L>
. Let's see it in action:
const myFunc = (myArray: string[]) => {
if (hasLengthAtLeast(myArray, 6)) {
myArray[0].toUpperCase(); // okay
myArray[5].toUpperCase(); // okay
myArray[6].toUpperCase(); // error, possibly undefined
}
}
Looks good. The compiler is happy to allow you to treat myArray[0]
and myArray[5]
as defined, but myArray[6]
is still possibly undefined.
Anyway, if you do decide to go for a type guard, you might want to balance complexity against how much you need to use it. If you're only checking length a few places, it might be worthwhile just to use a non-null
assertion operator like myArray[0]!.toUpperCase()
and not worry about getting the compiler to verify type safety for you.
Or, if you have no control over the value of len
, then you might not want a fragile recursive conditional type, and instead build something more robust but less flexible (like maybe an overloaded type guard that only works for specific len
values like in a comment on microsoft/TypeScript#38000).
It all comes down to your use cases.