Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

why typeof cannot narrow the union type

Tags:

typescript

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 targetcorrectly. 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
  }


}
like image 271
hliu Avatar asked Dec 14 '21 10:12

hliu


People also ask

Does not exist on type union type?

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.

How do you handle a union type in TypeScript?

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.

How do you narrow type TypeScript down?

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.

When would you use an intersection type instead of 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.


1 Answers

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' vs o.kind: 'square'). I'm not sure if type inference based on the types of shared properties (e.g. typeof o.value === 'string' vs Array.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.

Discriminated Unions currently support discriminant property values, not types.

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, or x.p !== v, where p and v 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?

like image 200
Inigo Avatar answered Oct 30 '22 09:10

Inigo