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.