If I have type like string[] | number[]
is there a way to narrow type to one of these array types without explicit function that returns like T is number[]
?
I tried this but typescript (5.5.4 is the latest now) doesn't get it: playground
declare const arr : string[] | number[]
if (arr.length) {
if (typeof arr[0] === 'string') {
// Property 'substr' does not exist on type 'string | number'.
// Property 'substr' does not exist on type 'number'
arr.map(x => x.substr(1))
} else {
// Operator '+' cannot be applied to types 'string | number' and 'number'.
arr.map(x => x + 1)
}
}
TypeScript doesn't generally narrow an object type when you narrow one of its properties. There is an open feature request for this behavior at microsoft/TypeScript#42384, but for now it's not part of the language. Currently the only way you can narrow the type of an object by checking a property is if the object's type is a discriminated union and you check a discriminant property. But while string[] | number[]
is a union, it's not a discriminated union. The discriminant property must be a literal type like "abc"
or 123
or true
(or at least one of the constituents of the union needs to have such a literal type). Neither string
nor number
are literal types, so checking typeof arr[0] === "string"
doesn't narrow arr
.
For now if you want narrowing to happen you'll need to write a type guard function. One way to do this is to emulate the general desired behavior of narrowing an object by checking a property:
function unionPropGuard<T, K extends keyof T, U extends T[K]>(
obj: T, key: K, guard: (x: T[K]) => x is U):
obj is T extends unknown ? (T[K] & U) extends never ? never : T : never {
return guard(obj[key])
}
This will extract all elements of the union T
whose K
property has overlap with the guarded type U
. Now you can rewrite if (guard(obj[prop]))
to if (unionPropGuard(obj, prop, guard))
. In your example code that looks like:
if (arr.length) {
if (unionPropGuard(arr, 0, x => typeof x === "string")) {
arr.map(x => x.substring(1))
} else {
arr.map(x => x + 1)
}
}
Note that x => typeof x === "string"
is inferred to be a type guard function of type (x: string | number) => x is string
. So the call to unionPropGuard()
performs the narrowing you were trying to perform by writing typeof arr[0] === "string"
. If it returns true
then arr
is narrowed to string[]
, otherwise arr
is narrowed to number[]
.