Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A complete guide to TypeScripts never type - zhenghao #8383

Open
guevara opened this issue Mar 26, 2022 · 0 comments
Open

A complete guide to TypeScripts never type - zhenghao #8383

guevara opened this issue Mar 26, 2022 · 0 comments

Comments

@guevara
Copy link
Owner

guevara commented Mar 26, 2022

A complete guide to TypeScript’s never type - zhenghao



https://ift.tt/8Nbuk9S






A complete guide to TypeScript’s never type

#typescript

Published on 04 March, 2022Last updated on 11 March, 2022

See discussions on Hacker News

This post has been translated into Korean

TypeScript’s never type is very under-discussed, because it’s not nearly as ubiquitous or inescapable as other types. A TypeScript beginner can probably ignore never type as it only appears when dealing with advanced types, such as conditional types, or reading their cryptic type error messages.

The never type does have quite a few good use cases in TypeScript. However, it also has its own pitfalls you need to be careful of.

In this blog post, I will cover:

  • The meaning of never type and why we need it.
  • Practical applications and pitfalls of never.
  • a lot of puns 🤣

What is never type#

To fully understand never type and its purposes, we must first understand what a type is, and what role it plays in a type system.

A type is a set of possible values. For example, string type represents an infinite set of possible strings. So when we annotate a variable with type string, such a variable can only have values from within that set, i.e. strings:

In TypeScript, never is an empty set of values. In fact, in Flow, another popular JavaScript type system, the equivalent type is called exactly empty

Since there’s no values in the set, never type can never (pun-intended) have any value, including values of any type. That’s why never is also sometimes referred to as an uninhabitable type or a bottom type.

The bottom type is how the TypeScript Handbook defines it. I found it makes more sense when we place never in the type hierarchy tree, a mental model I use to understand subtyping

The next logical question is, why do we need never type?

Why we need never type#

Just like we have zero in our number system to denote the quantity of nothing, we need a type to denote impossibility in our type system.

The word "impossibility" itself is vague. In TypeScript, “impossibility” manifests itself in various ways, namely:

  • An empty type that can't have any value, which can be used to represent the following:
    • Inadmissible parameters in generics and functions.
    • Intersection of incompatible types.
    • An empty union (a union type of nothingness).
  • The return type of a function that never (pun-intended) returns control to the caller when it finishes executing, e.g., process.exit in Node
    • Not to confuse it with void, as void means a function doesn’t return anything useful to the caller.
  • An else branch that should never (pun-intended... ok I think that's enough puns for today) be entered in a condition type
  • The fulfilled value's type of a rejected promise

How never works with unions and intersections#

Analogous to how number zero works in addition and multiplication, never type has special properties when used in union types and intersection types:

  • never gets dropped from union types, similiar to when zero added to a number gives the same number.

    • e.g. type Res = never | string // string
  • never overrides other types in intersection types, similiar to when zero multiplying a number gives zero.

    • e.g. type Res = never & string // never

These two behaviors/characteristics of never type lay the foundation for some of its most important use cases that we will see later on.

How to use never type#

While you probably wouldn’t find yourself use never a lot, there are quite a few legit use cases for it:

Annotate inadmissible function parameters to impose restrictions#

Since we can never assign a value to never type, we can use it to impose restrictions on functions for various use cases.

Ensure exhaustive matching within switch and if-else statement#

If a function can only take one argument of never type, that function can never be called with any non-never value (without the TypeScript compiler yelling at us):

We can use such a function to ensure exhaustive matching within switch and if-else statement: by using it as the default case, we ensure that all cases are covered, since what remains must be of type never. If we accidentally leave out a possible match, we get a type error. For example:

Partially disallow structural typing#

Let’s say we have a function that accepts a parameter of either the type VariantA or VariantB. But, the user mustn’t pass a type encompassing all properties from both types, i.e., a subtype of both types.

We can leverage a union type VariantA | VariantB for the parameter. However, since type compatibility in TypeScript is based on structural subtyping, passing an object type that has more properties than the parameter’s type has to a function is allowed (unless you pass object literals):

The above code snippet doesn't give us a type error in TypeScript.

By using never, we can partially disable structural typing and prevent users from passing object values that include both properties:

Prevent unintended API usage#

Let’s say we want to create a Cache instance to read and store data from/to it:

Now, for some reason we want to have a read-only cache only allowing for reading data via the get method. We can type the argument of the put method as never so it can’t accept any value passed in it:

Unrelated to never type, as a side note, this might not be a good use case of derived classes. I am not really an expert on object-oriented programming, so please use your own judgment.

Denote theoretically unreachable conditional branches#

When using infer to create an additional type variable inside a conditional type, we must add an else branch for every infer keyword:

In my previous post I mentioned how you can create declare “local (type) variable” together with extends infer. Check it out here if you haven’t seen it.

Filter out union members from union types#

Beside denoting impossible branches, never can be used to filter out unwanted types in conditional types.

As we have discussed this before, when used as a union member, never type is removed automatically. In other words, the never type is useless in a union type.

When we are writing a utility type to select union members from a union type based on certain criteria, never type's uselessness in union types makes it the perfect type to be placed in else branches.

Let's say we want a utility type ExtractTypeByName to extract the union members with the name property being string literal foo and filter out those that don't match:

Here are a list of steps TypeScript folllows to evaluate and get the resultant type:

  1. Conditional types are distributed over union types (namely, Name in this case):
  2. Substitue the implementation and evaluate separately
  3. Remove never from the union

Filter out keys in mapped types#

In TypeScript, types are immutable. If we want to delete a property from an object type, we must create a new one by transforming and filtering the existing one. When we conditionally re-map keys in mapped types to never, those keys get filtered out.

Here’s an example for a Filter type that filters out object type properties based on their value types.

Narrow types in control flow analysis#

When we type a function’s return value as never, that means the function never returns control to the caller when it finishes executing. We can leverage that to help control flow analysis to narrow down types.

A function can never return for several reasons: it might throw an exception on all code paths, it might loop forever, or it exits from the program e.g. process.exit in Node.

In the following code snippet, we use a function that returns never type to strip away undefined from the union type for foo:

Or invoke throwError after || or ?? operator:

Denote impossible intersections of incompatible types#

This one might feel more like a behavior/characteristic of the TypeScript language than a practical application for never. Nevertheless, it’s vital for understanding some of the cryptic error messages you might come across.

You can get never type by intersecting incompatible types

And you get never type by intersecting any types with never

When intersecting object types, depending on whether or not the disjoint properties are considered as discriminant properties (basically literal types or unions of literal types), you might or might not get the whole type reduced to never

In this example only name property becames never since string and number are not discriminant properties

In the following example, the whole type Baz is reduced to never because a boolean is a discriminant property (a union of true | false)

Check out this PR to learn more.

How to read never type (from error messages)#

You might have gotten error messages involving an unexpected never type from code you didn’t annotate with never explicitly. That’s usually because the TypeScript compiler intersects the types. It does this implicitly for you to retain type safety and to ensure soundness.

Here’s an example (play with it in TypeScript playground) that I used in my previous blog post on typing polymorphic functions:

The function returns either a number, a string, or a boolean depending on the type of argument we pass. We use an indexes access ReturnTypeByInputType[T] to retrieve the corresponding return type.

However, for every return statement we have a type error, namely: Type X is not assignable to type 'never' where X is string or number or boolean, depending on the branch.

This is where TypeScript tries to help us narrow down the possibility of problematic states in our program: each return value should be assignable to the type ReturnTypeByInputType[T] (as we annotated in the example) where ReturnTypeByInputType[T] at runtime could end up being either a number, a string, or a boolean.

Type safety can only be achieved if we make sure that the return type is assignable to all possible ReturnTypeByInputType[T], i.e. the intersection of number , string, and boolean. And what’s the intersection of these 3 types? It’s exactly never as they are incompatible with each other. That’s why we are seeing never in the error messages.

To work around this, you must use type assertions (or function overloads):

  • return Math.floor(Math.random() * 10) as ReturnTypeByInputType[T]
  • return Math.floor(Math.random() * 10) as never

Maybe another more obvious example:

obj[key] could end up being either a string or a number depending on the value of key at runtime. Therefore, TypeScript added this constraint, i.e., any values we write to obj[key] must be compatible with both types, string and number, just to be safe. So, it intersects both types and gives us never type.

How to check for never#

Checking if a type is never is harder than it should be.

Consider the following code snippet:

Is Res true or false? It might surprise you that the answer is neither: Res is actually never. In fact,

It definitely threw me off the first time I came across this. Ryan Cavanaugh explained this in this issue. It boils down to:

  • TypeScript distributes union types in conditional types automatically
  • never is an empty union
  • Therefore, when distribution happens there’s nothing to distribute over, so the conditional type resolves to never again.

The only workaround here is to opt out of the implicit distribution and to wrap the type parameter in a tuple:

This is actually straight out of TypeScript’s source code and it would be nice if TypeScript could expose this externally.

In summary#

We covered quite a lot in this blog post:

  • First, we talked about never type's definition and purposes.
  • Then, we talked about its various use cases:
    • imposing restrictions on functions by leveraging the fact that never is an empty type
    • filtering out unwanted union members and object type's properties
    • aiding control flow analysis
    • denoting invalid or unreachable conditional branches
  • We also talked about why never can come up unexpectedly in type error messages due to implicit type intersection
  • Finally, we covered how you can check if a type is indeed never type.

Special thanks to my friend Josh for reviewing this post and giving invaluable feedback!







via zhenghao.io

March 26, 2022 at 03:49PM
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant