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.
Playground link to code