If the members of a union type share a property, and the type of that property can be used to discriminate between those members, I should be able to narrow the type within an if
clause using typeof
as a condition. But it doesn't work.
For example, within the if
clause below, the type of event
should be inferred as UserTextEvent
and the type of event.target
should be inferred as HTMLInputElement
:
type UserTextEvent = { value: string, target: HTMLInputElement };
type UserMouseEvent = { value: [number, number], target: HTMLElement };
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
if (typeof event.value === 'string') {
event.value // string, as expected
event.target // should be narrowed to HTMLInputElement, but
// is still HTMLInputElement | HTMLElement. Why?
}
}
Here are the types of discriminant properties supported when discriminated unions support was added in Typescript 2.0:
A discriminant property type guard is an expression of the form
x.p == v
,x.p === v
,x.p != v
, orx.p !== v
, wherep
andv
are a property and an expression of a string literal type or a union of string literal types. The discriminant property type guard narrows the type of x to those constituent types of x that have a discriminant property p with one of the possible values of v.Note that we currently only support discriminant properties of string literal types. We intend to later add support for boolean and numeric literal types.
Typescript 3.2 expanded that support:
Common properties of unions are now considered discriminants as long as they contain some singleton type (e.g. a string literal, null, or undefined), and they contain no generics.
As a result, TypeScript 3.2 considers the error property in the following example to be a discriminant, whereas before it wouldn’t since Error isn’t a singleton type. Thanks to this, narrowing works correctly in the body of the unwrap function.
type Result<T> = { error: Error; data: null } | { error: null; data: T }; function unwrap<T>(result: Result<T>) { if (result.error) { // Here 'error' is non-null throw result.error; } // Now 'data' is non-null return result.data; }
Typescript 4.5 added support for "Template String Types as Discriminants".
TypeScript 4.5 now can narrow values that have template string types, and also recognizes template string types as discriminants.
As an example, the following used to fail, but now successfully type-checks in TypeScript 4.5.
export interface Success { type: `${string}Success`; body: string; } export interface Error { type: `${string}Error`; message: string } export function handler(r: Success | Error) { if (r.type === "HttpSuccess") { const token = r.body; (parameter) r: Success } }Try
In all cases, it's the value of the discriminant property that is used to discriminate, not its type.
In other words, as of the current version of Typescript, 4.5, you're out of luck. It's just not supported (yet). Here are the relevant open issues:
typeof x.y
as a discriminant (#32399)Here is a comment from Typescript dev team lead Ryan Cavanaugh:
From the looks of it, this feature is unfortunately much more complex to implement than we had anticipated, and we don't think that the cost/benefit ratio is good in this case. As much as we want to support this pattern, the implications of these changes feel too far-reaching (even if they're necessary to actually support the scenario). We'll leave the original issue open in case later on down the line a simpler method of approach becomes available, but we aren't comfortable with introducing this much complexity in a critical codepath right now.