I'm working with an array for which the type of the first element is known, if the array has length one, and which could be one of two types, otherwise; i.e. something like the following:
type NarrowableArray<TKnown, TGenereal> = [TKnown] | [TGeneral, TGeneral, ...TGeneral[]]
type Foo = number | number[]
let a: NarrowableArray<number, Foo> = Math.random() > 0.5 ? [3] : [3,4,5]
if (a.length === 1) {
a
console.log(a[0] + 2) //Error > TS can't tell `a[0]` has type 'number'.
}
Is this expected? Is there a work around?
The problem is that the length property of an open-ended tuple type is number, as described in microsoft/TypeScript#24897; TypeScript doesn't have accurate range types (as requested in microsoft/TypeScript#15480) to express "any number except 1" or {length: WholeNumber & GreaterThan<1>}.
So unfortunately just checking the length property for 1 won't be able to discriminate the union, since both union members have a length to which 1 is assignable.
There are other narrowing methods that work directly with tuples more naturally, for example, you could use in narrowing and check "1" in a as follows:
function aFunction(a: NarrowableArray<number, Foo>) {
if (!("1" in a)) {
a // (parameter) a: [number]
console.log(a[0] + 2) // okay
} else {
a // (parameter) a: [Foo, Foo, ...Foo[]]
}
}
However, if you want to perform a length check yourself you might need to refactor to a user-defined type guard function of the form:
function hasLengthOne(x: any[]): x is [any] {
return x.length === 1;
}
And then call that instead of directly checking the length:
function aFunction(a: NarrowableArray<number, Foo>) {
if (hasLengthOne(a)) {
a // (parameter) a: [number]
console.log(a[0] + 2); // okay
} else {
a // (parameter) a: [Foo, Foo, ...Foo[]]
}
}
That works because the compiler is able to filter NarrowableArray<number, Foo> by [any]; we're circumventing the length issue entirely.