When using typeof
to narrow the union type, it surprises me that on the premise that under the if
condition we get the particular type of field value
but cannot infer the type of field target
correctly. Why cannot infer that the type of target is HTMLInputElement when know the value is a string?
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
//---------------------------------------------------------------
event.target // HTMLInputElement | HTMLElement but not HTMLInputElement, why ?
//
return
}
}
The "property does not exist on type union" error occurs when we try to access a property that is not present on every object in the union type. To solve the error, use a type guard to ensure the property exists on the object before accessing it.
TypeScript Union Type Narrowing To narrow a variable to a specific type, implement a type guard. Use the typeof operator with the variable name and compare it with the type you expect for the variable.
The in operator narrowing JavaScript has an operator for determining if an object has a property with a name: the in operator. TypeScript takes this into account as a way to narrow down potential types. For example, with the code: "value" in x . where "value" is a string literal and x is a union type.
Intersection types are closely related to union types, but they are used very differently. An intersection type combines multiple types into one. This allows you to add together existing types to get a single type that has all the features you need.
The type of narrowing you're looking for I believe falls under the category discriminated unions.
🚩 I say "I believe" because I'm not 100% certain of the definition of "discriminated unions". I know the values of a property shared by all member types of a type union act as discriminants (e.g.
o.kind: 'circle'
vso.kind: 'square'
). I'm not sure if type inference based on the types of shared properties (e.g.typeof o.value === 'string'
vsArray.isArray(o.value)
) is considered the same or something else. But if both property values and types are considered discriminants vis-a-vis "discriminated unions", my explanation below answers your question.
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, I think you're out of luck. It's just not supported (yet).
🌶 I searched Typescript Issues and couldn't find a mention. Perhaps open a new Issue pointing to this SO question?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With