preface

This article is the first in a series on TypeScript’s other side: Type programming, which will be published in a column of the same name (see Zhihu or Nuggets). At the same time, this series of articles will mainly inherit from the author’s article of the same name last year (original version, fried rice version), and further expand and deepen, in addition to the type of never, We will also include sections such as conditional typing and covariant & inverse, infer and recursion & tail recursion, control flow analysis in TypeScript, and exploration of TypeScript toolchains.

As for the reason of this series, first of all, the original version of this article is actually quite widely read. Whether on the Intranet or the Internet, it is probably the one I read the most and the most widely. In this article, I tried to cram most of it in so that not everyone could read it, or stay sane.

At the same time, part of the content of the original in today’s author seems to be too shallow too extensive (level is limited, but also hope forgive). The result is this series, which focuses on a single issue for each article, rather than on a single issue, thus ensuring that the content is pure (as pure as the author’s personality) + the length is minimal. Most importantly, this series assumes that you already have the basics of TypeScript, even if you’ve read previous columns.

In addition, this article and the rest of the series are part of my upcoming QCon+ “TypeScript Landing in Medium and large Projects” feature, so stay tuned.

This article introduces TypeScript’s never type from the following sections. I’m not familiar with the type systems of other computer languages, so I’d like to point out any errors.

  • Three special types in TypeScript: any, unknown, and never
  • What can we do if we just use never?
  • The role of never in tool types

Before we begin, I would like to thank our predecessors Sprite, Ah Wei, Find Hailan, 37 37 and 20 for sharing TypeScript. This article has been greatly influenced by them.

P.S. You can get all the sample code at GitHub.

Never, any, unknown

In TypeScript, there are several types that might confuse beginners: any, unknown, and never. When do you use any? When do you use unknown? Never?

Let’s think about when we use any, for example, when a variable is actually a type, but you didn’t do it properly, and at some point the type is wrong, so you can use as any, and then as the type you want, and then you get the type hint again (of course, I think it’s better to go as any.

// If you get beaten writing code like this, don't report me
const foo = {} as any as Function;
Copy the code

What does as mean? You point it in the compiler’s face and tell it that this variable is of type.

Why as twice? As a Function? Good question! Because the TS compiler will throw an error message at you, it can only cast parent-child relationships, and it won’t cast you for unrelated relationships, so you need to use as any to enforce the association between the original type and the new type. If you want to be a little more formal, you should first form the parent type of the original type and the parent type of the new type, and then form the new type as, such as Sprite teacher’s example:

// Deer, Horse public parent type
interface Animal {}

interface Deer extends Animal {
  deerId: number
}

interface Horse extends Animal {
  horseId: number
}

let deer: Deer = { deerId: 0 }

// It can't be done in one step
let horse = deer as Horse

// Promote to the common parent type first, then locate to the child type
let horse = deer as Animal as Horse
Copy the code

The compiler says “please as unknown” when two unrelated types are associated. What’s the difference between unknown and any?

First, in TypeScript’s Type system, any and unknown are both Top types (mixed in PHP, any? In Kotlin). That is, they are at vertices in the type hierarchy, but variables of type any can be assigned to values of any type, while unknown can only accept unknown and any. The starting point for both is the same, which is to quickly represent an unknown/dynamic value, but any is more unfettered:

let foo: any;
foo.bar().baz();
Copy the code

This is no error! What’s the difference from JavaScript? Using any means you’ve given up type checking altogether. What’s worse is that any is contagious: a variable is declared as any, Then all subsequent values derived from their operations are labeled with implicit any (if there is no type assertion or type narrowing based on control flow analysis). Unknown, however, is like a type-safe version of any: type checking still exists.

let bar: unknown;

// This is an error
// @ts-expect-error
bar.baz().foo();
Copy the code

For variables declared as unknown, you cannot read or write them directly. Instead, you must specify the type, either explicitly, type-guarding, or automatic compiler analysis, such as type-guarding:

function isString(input: unknown) :input is string {
  return typeof input === "string";
}
Copy the code

Since there is a Top Type, there should be a Bottom Type, and in TypeScript never is that Bottom Type. Bottom Type means a Type that represents Nothing, in Kotlin it is Nothing, in Rust it is! Decorates a type that has no return value. You might think that string is pretty specific, and literal types like ‘linbudu’ are even more specific, but never is even more specific. It is a subtype of all types and is the lowest level of the type system, meaning that no type can be assigned to it except never itself.

In TypeScript, a function that must throw an error returns never. At this point, you might be thinking of a special companion: void (we often write a Promise

to indicate a direct resolve Promise), and the difference between void and never is that, A function that returns void still calls a return internally, except that it returns nothing, that void represents nothing (and that void shouldn’t even be considered a type), and that a function that returns never doesn’t call a return at all. Never means return does not exist at all.

Now that you know the basics of never, you’re ready to use it 🙂

The most basic use of never

Scenarios where never is used on its own are rare, but not unknown. As we said earlier, it is a subtype of any type, and no type is narrower than it, so it cannot be copied to never. We can use this to ensure that if… Else or SWicth case statements, all possible branches are exhausted. In TSConfig, there is a similar noImplicitReturns option, which ensures that all path branches of the function must return a value, but it is clear that the granularity (or strength) is insufficient.

A simple example of a variable defined using a union type:

const strOrNum: string | number = "foo";

if (typeof strOrNum === "string") {
  console.log("str!");
} else if (typeof strOrNum === "number") {
  console.log("num!");
} 
Copy the code

As if there were no problem, suppose one day this variable’s union type had another member:

const strOrNumOrBool: string | number | boolean = false;
Copy the code

A very common scenario is that you want all possible union types of this variable to be handled, with special treatment for each member of the union type. In the above code, TypeScript doesn’t fail if you forget to handle Boolean types (of course it doesn’t; if it’s smart enough to do that, it might as well call it AIScript).

So, how do you throw an error when you miss a type branch? First, due to TypeScript’s type narrowing capability, we can narrow the type of a variable to the corresponding value in each else syntax block. If we add a bottom else statement to the above statement:

const strOrNumOrBool: string | number | boolean = "foo";

if (typeof strOrNumOrBool === "string") {
  console.log("str!");
} else if (typeof strOrNumOrBool === "number") {
  console.log("num!");
} else {
  // ...
}
Copy the code

In the final else block, if we also use this variable, it will be intelligently derived to Boolean. This is definitely not what we want to see, it’s all the way to the bottom of the block and the unnarrowed type. So we simply assign it to never (back to the original example) :

const strOrNumOrBool: string | number | boolean = false;

if (typeof strOrNumOrBool === "string") {
  console.log("str!");
} else if (typeof strOrNumOrBool === "number") {
  console.log("num!");
} else {
  const _exhaustiveCheck: never = strOrNumOrBool;
}
Copy the code

Ok, error: cannot assign type “Boolean” to type “never”. ts(2322)

Let’s add a branch that handles Boolean types:

if (typeof strOrNumOrBool === "string") {
  console.log("str!");
} else if (typeof strOrNumOrBool === "number") {
  console.log("num!");
} else if (typeof strOrNumOrBool === "boolean") {
  console.log("bool!");
} else {
  const _exhaustiveCheck: never = strOrNumOrBool;
}
Copy the code

Now that’s fine, because after exhausting all the type branches, strOrNumOrBool is of course also of type never. Doing so only avoids omissions from the TypeScript type level. To be safe, we can throw an error in the else bottom statement:

// ...
else {
  const _exhaustiveCheck: never = strOrNumOrBool;
  throw new Error(`Unknown input type: ${_exhaustiveCheck}`);
}
Copy the code

A similar scenario, the enumeration + switch case statement, is probably one of the most common combinations:

enum PossibleType {
  Foo = "Foo",
  Bar = "Bar",
  Baz = "Baz",}function checker(input: PossibleType) {
  switch (input) {
    case PossibleType.Foo:
      console.log("foo!");
      break;
    case PossibleType.Bar:
      console.log("bar!");
      break;
    case PossibleType.Baz:
      console.log("baz!");
      break;
    default:
      const _exhaustiveCheck: never = input;
      break; }}Copy the code

This is fine now, but as soon as you add a member to the enumerated value (see the example), you will get an error message that you cannot assign to never.

Make it big in tool types

Okay, here we go again to the favorite tool type segment, how many of you have scratched your head over a tool type with dozens of lines. In fact, for type gymnastics this kind, I feel that the intermediate level is basically enough, you write a business to a huge complex around more than a dozen generic parameters of the tool type, you see colleagues will beat you.

Going back to never, the role of never in tool types can be summarized in three ways:

  • As the default value of a generic parameter to support type handling logic within tool types based on the number of input arguments.
  • Infer + condition Type is combined to extract the value at a specific position conforming to a specific Type Structure. If the Type parameter to be extracted does not conform to this Structure, never is returned.
  • Tailoring/assembling/extending the interface by specifying some of the attributes of the interface to never and removing some of them.

First, as the default value for the generic parameter, the last slightly dodgy example:

type Equal<X, Y, A = X, B = never> = (<T>() = > T extends X ? 1 : 2) extends <
  T
>() = > T extends Y ? 1 : 2
  ? A
  : B;
Copy the code

Strictly speaking, this example is not particularly relevant to the first point, but it will be the basis for more important tool types later on, and will also be generally difficult to understand by feedback, so I throw it here in advance.

This is A weird structure, isn’t it? What are 1 and 2 things? Why does extends have generics?

Focus on a look (sorry, recently Guo Degang and Yu Qian two teachers listen to crosstalk, focus on a look always think of…)

() => T extends X? 1:2) and (

() => T extends X? 1:2), look again, this is not the same two tuo it. Wait, where did the T come from?

One by one, the first is that if (

() => T extends X? 1:2) Extends (

() => T extends Y? 1:2), then return A, otherwise return B.

With extends, which involves covariant and contravariant parts, you can simply say that the type on the left is more narrow and specific and the type on the right is more broad and broad (i.e., whatever is on the right is on the left!). , extends establishes:

// "TRUE"
type Test = { foo: string; bar: boolean; } extends { foo: string}?"TRUE" : "FALSE";
Copy the code

This generic T, you can see that it also doesn’t actually have a specific value, but instead acts as the whole (

() => T extends X? 1:2) Part of the structure participates in the comparison (after all, it is just defined, by

). In this case, we’re really just using this as a set of conditions to compare whether X and Y are equal. Extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends

Take a closer look, old chap. This is not a simple X extends Y. Why else would I put a layer on top of it? By placing X and Y in the return value of a temporary function, the indirect determination ensures that the extends only goes through if X and Y are exactly the same. For example, if foo of interface A is read-only and foo of interface B is read-only, then foo of interface B must be read-only as well.

// fail
type TestEqual1 = Equal<[], readonly[]."pass"."fail">;
// pass
type TestEqual2 = Equal<[], [], "pass"."fail">;
Copy the code

A = X and B = never are the values that determine whether extends extends passes or not. A = X and B = never are the values that determine whether extends extends passes or not.

The next point is to combine with infer + conditional types. I won’t get too sleepy here. This is the simplest scenario, and you can even look at examples. Of course, if you know Infer.

type FuncReturnType<T extends(... args:any) = >any> = T extends (
  ...args: any
) => infer R
  ? R
  : never;
Copy the code

This structure is used for almost all of the utility types that perform the single-fetch behavior, such as the new CONCEPT in TS 4.5 called “COMMIT” (or PromiseValue), which extracts an internal type of a Promise; There are also built-in Parameters and ConstructorParameters for extracting function parameter types.

Infer is then played without infer, never and conditional types, and can also infer easily, such as Exclude and Extract:

type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
Copy the code

This may seem naive, but that’s because T and U are usually of the combined type, as in:

interface Tmp1 {
  foo: string;
  bar: string;
  baz: string;
}

interface Tmp2 {
  foo: string;
  baz: string;
}

// "bar"
type ExcludedKeys = Exclude<keyof Tmp1, keyof Tmp2>;
// "foo" | "baz";
type ExtractedKeys = Extract<keyof Tmp1, keyof Tmp2>;
Copy the code

Of course, you can also encapsulate keyof, i.e

type ExcludedKeys<T, U> = keyof T extends keyof U ? never : keyof T;
type ExtractedKeys<T, U> = keyof T extends keyof U ? keyof T : never;
Copy the code

Why not?

I hope you’ve got a glimmer of what Never actually does, in terms of tool types, and if it doesn’t, it doesn’t matter, or I’m not going to make it. Let’s take a look at some of the more common and powerful never applications.

Let’s start with an interface like this:

interface ITmp {
  foo: number;
  bar: string;
  baz: never;
}
Copy the code

Can objects using this interface have a Baz attribute? There’s always a demon when things go wrong, so not here. Does such an interface work? No, declaring never explicitly means that you already know which attributes to exclude (and need to declare manually). What if we go through all the gymnastics and end up with this interface?

This is the third use of Never, which I personally think is the most powerful and Amazing: tailoring/assembling/extending the interface by specifying some of its properties as Never and removing some of them.

We can easily obtain this structure of the interface by using the mapping type + condition type, such as

type ProcessedTypeWithNonFuncPropAsNever<T extends object> = {
  [K inkeyof T]-? : T[K]extends Function ? K : never;
};

interface IInterfaceWithFuncProps {
  foo: string;
  bar: string;
  func1: () = > void;
  func2: () = > void;
}
Copy the code

Well, it’s a little long, but I’ll just stick with it. Foo, bar, Baz.

– here? Get rid of? : sign.

The result of this is:

type Result = ProcessedTypeWithNonFuncPropAsNever<IInterfaceWithFuncProps>

type Result = {
    foo: never;
    bar: never;
    fun1: "func1";
    func2: "func2";
}
Copy the code

This doesn’t seem to be working, does it? Function can also be one step closer, get all the function types of key joint type, such as “foo1” | “foo2”, only need to add a little:

type FuncTypeKeys<T extends object> = {
  [K inkeyof T]-? : T[K]extends Function ? K : never;
}[keyof T];

// "func1" | "func2"
type Result = FuncTypeKeys<IInterfaceWithFuncProps>
Copy the code

T[K] extends Function? K : never; T[K] extends Function? T[K] : never; You can get the value directly.

This might be a little bit simpler, but let’s do a little bit more complicated:

type MutableKeys<T extends object> = {
  [P inkeyof T]-? : Equal< { [Qin P]: T[P] },
    { -readonly [Q in P]: T[P] },
    P,
    never
  >;
}[keyof T];

type Equal<X, Y, A = X, B = never> = (<T>() = > T extends X ? 1 : 2) extends <
  T
>() = > T extends Y ? 1 : 2
  ? A
  : B;
Copy the code

The MutableKeys would still take three or five minutes to look at if we hadn’t talked about the Equal type, but that’s a lot easier. We saw earlier that the extends in Equal requires the exact same type on both sides, including modifiers such as readOnly and? , so I believe you probably still don’t understand, take a look:

  • First extract the interface’s keys using the mapping type:[P in keyof T]
  • Then extract the key again:{ [Q in P]: T[P] } 与 { -readonly [Q in P]: T[P] }
  • useEqualCompare the two extracts above and return the key first extracted in the first step when the comparison passes.

For step 2, we implement a mediation type:

interface IInterfaceWithReadonlyProps {
  readonly foo: string;
  bar: string;
  readonly func1: () = > void;
  func2: () = > void;
}

type Tmp<T extends object> = {
  [P inkeyof T]-? : { [Qin P]: T[P] };
};

type A1 = Tmp<IInterfaceWithReadonlyProps>;
Copy the code

The type of A1 looks something like this:

type A1 = {
    readonly foo: {
        readonly foo: string;
    };
    bar: {
        bar: string;
    };
    readonly func1: {
        readonly func1: () = > void;
    };
    func2: {
        func2: () = > void;
    };
}
Copy the code

K:V becomes K:(K:V). Why do you do that?

We passed the outermost readonly version of Y in Equal for comparison, which changed to readonly foo: {readonly foo: string} vs foo: {readonly foo: string}! Obviously these two comparisons do not pass, so foo must be read-only and cannot be in MutableKeys!

Compared to readonly, possible? Add OptionalKeys:

export type OptionalKeys<T> = {
  [K inkeyof T]-? : {}extends Pick<T, K> ? K : never;
}[keyof T];
Copy the code

We said earlier that the left side of extends cannot be looser than the right, so {} extends {a:number} is definitely not true, but {} extends {a? : number} is ok, so use Pick to Pick out the alternative types.

Ok, good times are always short, and I don’t know how long it will take to read this, ten minutes should be enough, let’s finish with one more useful tool type, then please turn off your phone/computer screen and do five minutes of eye exercises.

We often come across this scenario:

  • An object can and must satisfy only one of multiple interfaces at a time. For example, if you are a regular user, you are definitely not a VIP.
  • You can only have one of several groups of attributes in an object at the same time. For example, you either have an attribute for ordinary users (how long until the next VIP coupon is given) or an attribute for VIP users (VIP level).
  • Several attributes in an object must exist or not exist at the same time. For example, you either have a VIP level and a VIP expiration date, or nothing at all.

All of these scenarios can be abstracted into a mutually exclusive type, and never is probably the most elegant and simple way to do that. {sendVIPExpTime: number; vipLevel: never; } | { sendVIPExpTime: never; vipLevel: number; }, the implementation is also simple:

type Without<T, U> = { [P inExclude<keyof T, keyof U>]? :never };

type XOR<T, U> = (Without<T, U> & U) | (Without<U, T> & T);
Copy the code

I think this should not be explained, look at the effect:

interface Foo {
  foo: string;
}

interface Bar {
  bar: string;
}

// foo + bar:never | bar + foo:never
type FooOrBar = XOR<Foo, Bar>;

const fooOrBar1: FooOrBar = { foo: "foo" };
const fooOrBar2: FooOrBar = { bar: "bar" };
// Error
// @ts-expect-error
const fooOrBar3: FooOrBar = { baz: "baz" };
// Error
// @ts-expect-error
const fooOrBar4: FooOrBar = { foo: "foo".bar: "bar" };
Copy the code

And if at least one alternative type is met:

// There must be at least one sharedProp, container, or Module
// If Partial is removed, there must be at least one Partial
type ComposedOption = { sharedProp: string } & Partial<
  XOR<
    {
      container: {
        containerId: number;
      };
    },
    {
      module: { modId: number };
    }
  >
>;

const option: ComposedOption = {
  sharedProp: "foo".container: {
    containerId: 599,}};Copy the code

This is the end of the article. In fact, this article is more than I expected to simplify, after all, a lot of knowledge is related to the whole body (especially the tool type part), if thrown out directly will only be me and the reader confused. This article as the first article of this series, there should be a lot of deficiencies, such as I listen to crosstalk write this article, so that it is not very serious… But hopefully you learned something from it. Again, you can find all the code for this series on GitHub, and we’ll see you next time next year.