In TypeScript, narrowing an object type by narrowing one of its properties is not a standard behavior. If you want this functionality, there is an open feature request for it on the GitHub page at microsoft/TypeScript#42384. Currently, the only way to narrow the type of an object based on a property check is if the object's type is a discriminated union and you check a discriminant property. However, a union like string[] | number[]
is not a discriminated union because the discriminant property must be a literal type. This means that neither string
nor number
are literal types, so checking typeof arr[0] === "string"
won't narrow down arr
.
To achieve narrowing in such cases, you can create a type guard function. One approach is to simulate the desired behavior of narrowing an object through a property check with the following function:
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 function filters elements of the union T
based on whether the K
property overlaps with the guarded type U
. You can then refactor your code from using if (guard(obj[prop]))
to utilizing
if (unionPropGuard(obj, prop, guard))
. In practical terms, this would look something like:
if (arr.length) {
if (unionPropGuard(arr, 0, x => typeof x === "string")) {
arr.map(x => x.substring(1))
} else {
arr.map(x => x + 1)
}
}
It's important to note that x => typeof x === "string"
is automatically inferred as a type guard function of type
(x: string | number) => x is string
. Therefore, calling
unionPropGuard()
accomplishes the narrowing effect you initially attempted with
typeof arr[0] === "string"
. If the function returns
true
,
arr
is narrowed to
string[]
; otherwise, it narrows to
number[]
.
Check out the Playground link for interactive code samples