preface

As one of the trends in front-end development, TypeScript is becoming increasingly popular among developers. On a larger scale, almost 90% of frameworks and tool libraries are written in TypeScript (or similar type solutions such as Flow). On a smaller level, TypeScript can be a boon when it comes to writing configuration files (like vite’s) or small scripts (thanks to TS-Node). For example, Remy Sharp, the author of Nodemon, once said that she has never used TS (see #1565) and will not learn TS in the future, which may be due to language habits. There’s another barrier that often prevents newcomers from getting into TypeScript: high learning costs.

In the early stages of learning TypeScript, many students have a love-hate relationship with TypeScript, depending on its type hints and engineering capabilities, but often suffer from type errors and end up using any. As a result, TypeScript has evolved into AnyScript…

The main culprit is actually part of the problem students, at the beginning of the learning TypeScript or to revolt, in the case of a blank TS program, either don’t know how to learn, then the conscience of the official document, look at a few relevant article will feel, finally have a problem or a scratching his head.

This article tries to address the latter by focusing on TypeScript’s type programming part (how many other parts of TS are there? See below), starting with the most basic generics, and moving on to index, mapping, and condition types, to keywords like IS, In, and infer, and finally to the final tool types. Open your IDE and follow me through the code to help you take TypeScript to the next level.

It is important to note that this is not an introduction to TypeScript and is not intended for those who have no current experience with TypeScript. If you’re still a beginner, I recommend the XCATLiu TypeScript tutorial as well as the official documentation. From my experience, you can start with the tutorial and go to the official documentation if you feel confused.

With the basics of TypeScript behind you, welcome back to this article.

TypeScript = type programming + ES proposal

Authors typically divide TypeScript into two parts:

  • Pre-implemented ES proposals, such as decorators, optional chains? Null value merge operator?? (introduced with optional chains in TypeScript3.7), private, the class’s private member, and so on. With the exception of some extremely volatile syntax (I’m looking at you, decorator), most TS implementations are actually the ES syntax of the future.

    Strictly speaking, the ES version of decorators and TS version of decorators are already two things, the author previously in approaching MidwayJS: The first understanding of TS decorators and IoC mechanism this article introduced some history of TS decorators, interested students may wish to read.

    For this part, whether you have only JavaScript experience, or Java, C# experience, it is very fast to learn, after all, the main syntax sugar. Of course, this is the most used part of actual development, because it’s more down to earth than the other part: type programming.

  • Type programming, from a simple interface to the seemingly advanced T extends SomeType, to the Partial, Required, and other obscure utility types, falls under this category. This part of the code has no effect on the actual functional level of the code, even if you had ten ANY in one line and encountered a type error @ts-ignore (similar to @eslint-ignore, which would disable type checking on the next line), Even the –transpileOnly option (which disables the TS compiler’s type-checking ability and only compiles the code for faster compilations) does not affect the logic of your code itself.

    However, this is why type programming has not received much attention: compared to syntax, it introduces a lot of extra code (type-defining code can even exceed the amount of business code) and so on. And the actual business doesn’t require very strict type definitions, usually only interface data, application state flow, etc., and it’s usually the underlying framework library that requires a lot of type programming code.

    If the previous part makes your code sweeter, the most important thing this part does is make your code more elegant and robust (yes, elegant and robust are not at war). If you’re on a team that uses a monitoring platform like Sentry, The most common error for JS code is Cannot read property ‘XXX’ of undefined, undefined is not a function (see top-10-javascripts -errors), Even TS can’t completely erase this error, but it can probably be solved.

Ok, with that in mind, it’s time to get down to business. The chapters in this article are distributed as follows, and if you already know some of the basics of prior knowledge (such as generics), you can skip ahead.

  • The foundation of type programming: generics
  • Type guard with is and in keywords
  • Index type and mapping type
  • Condition type, distributed condition type
  • Infer the keyword
  • Tool type
  • New TypeScript 4.x features

The generic

We include generics first because it is the most fundamental part of TypeScript’s type programming architecture, on which all advanced types are written. Just as we can’t program without variables, variables in type programming are generics.

Suppose we have a function like this:

function foo(args: unknown) :unknown {... }Copy the code
  • If it receives a string, returns a partial interception of the string.
  • If you receive a number, return n times that number.
  • If an object is received, return the object whose key value has been changed (the key name remains the same).

One thing these scenarios have in common is that the return value of the function is of the same type as the input parameter.

What do you do if you want to get an exact type definition here?

  • theunknownReplace withstring | number | object? But what this means is that the function can take any value, and its return type can be String/number/object, which, despite the type definition, is far from exact.

Don’t forget that what we need is the same effect of the input return value type. This is where generics come in. We use a generic to collect the parameter’s type value and return it as the value, like this:

function foo<T> (arg: T) :T {
  return arg;
}
Copy the code

This way, when we use foo, the editor can determine the return value of the function in real time based on the parameters we pass in. Just as in programming, the value of a variable in a program is determined at run time, the value (type) of a generic type is determined when a method is called, a class is instantiated, and similar execution actually takes place.

Generics make it easy to reuse the type definition of a code snippet (for example, a subsequent implementation of a function that accepts Boolean and returns Boolean) and increase flexibility and rigor.

In addition, you may have seen the use of Array

Map

. Usually we call an unassigned form like T in the above example a type parameter variable or a generic type. Instances such as Array

that have been instantiated are called actual type parameters or parameterized types.

,>

Typically, generics only use a single letter. Such as T, U, K, V, S, etc. My recommendation is to use generic variable declarations with concrete meaning, in the form of BasicBusinessType, once the project has reached a certain level of complexity.

foo<string> ("linbudu");
const [count, setCount] = useState<number> (1);
Copy the code

The above example can also be omitted, as TS automatically deduces the actual type of a generic, and in some Lint rules it is not actually recommended to add type values that can be automatically derived.

Writing generic types under arrow functions:

const foo = <T>(arg: T) = > arg;
Copy the code

If you write this in a TSX file,

may be recognized as a JSX tag, so you need to explicitly tell the compiler:

const foo = <T extends SomeBasicType>(arg: T) => arg;
Copy the code

In addition to being used in functions, generics can also be used in classes:

class Foo<T.U> {
  constructor(public arg1: T, public arg2: U) {}

  public method(): T {
    return this.arg1; }}Copy the code

This is the end of generics alone (there’s really nothing to be said for generics alone), and we’ll cover more of their use in the next chapter on advanced types.

Type guard, is in keyword

Let’s start with a relatively straightforward topic: type guarding, and work our way through generics-based type programming.

Suppose you have a field that could be a string or a number:

numOrStrProp: number | string;
Copy the code

Now when you want to narrow the union type of this field down to, say, string exactly, you might write:

export const isString = (arg: unknown): boolean= > typeof arg === "string";
Copy the code

Take a look at this:

function useIt(numOrStr: number | string) {
  if (isString(numOrStr)) {
    console.log(numOrStr.length); }}Copy the code

The isString function does not seem to narrow down the type range; the arguments are still union types. This is where the is keyword is used:

export const isString = (arg: unknown): arg is string= >typeof arg === "string";
Copy the code

When isString(numOrStr) is true, numOrStr is reduced to string. This is just a union type with primitive types as members, and we can extend it to all kinds of scenarios. Let’s start with a simple false value judgment:

export type Falsy = false | "" | 0 | null | undefined;

export constisFalsy = (val: unknown): val is Falsy => ! val;Copy the code

This is probably one of the most common type aliases I use on a daily basis, along with isPrimitive, isFunction and other type guards.

And use the in keyword, we can further narrow Type (Type Narrowing), think about the following example, how will A | B “joint Type to” A “?

class A {
  public a() {}

  public useA() {
    return "A"; }}class B {
  public b() {}

  public useB() {
    return "B"; }}Copy the code

First think of for… The in loop iterates over the property names of the object, as does the in keyword, which determines whether an property is owned by the object:

function useIt(arg: A | B) :void {
  'a' in arg ? arg.useA() : arg.useB();
}
Copy the code

If there is an A attribute in the parameter, the intersection of a and B types does not contain A, so this can immediately narrow the type to A.

Since the intersection of types A and B does not contain the attribute A, the in judgment here Narrows the type pair to exactly before and after the ternary expression. That’s either A or B.

Here’s another example of using a literal type as a type guard:

interface IBoy {
  name: "mike";
  gf: string;
}

interface IGirl {
  name: "sofia";
  bf: string;
}

function getLover(child: IBoy | IGirl) :string {
  if (child.name === "mike") {
    return child.gf;
  } else {
    returnchild.bf; }}Copy the code

About literal type literal types, it is the type of further restrictions, such as your status code can only be 0/1/2, and then you can write the status: 0 | 1 | 2 form, rather than use a number to express.

Literal types include string literals, number literals, Boolean literals, and template literals introduced in version 4.1 (we’ll cover this later).

  • A string literal, commonly known asmode: "dev" | "prod".
  • Boolean literals are often mixed with other literal types, such asopen: true | "none" | "chrome".

Such bits of basic knowledge are interspersed throughout the text so that concepts don’t get too monotonous without specific scenarios.

Distinguish interfaces based on fields

In daily life, I often see students asking similar questions: The user information under login is completely different from that under login, or

You can also use the in keyword to solve the problem: the user information under login and unlogin is completely different from that under login.

interface ILogInUserProps {
  isLogin: boolean;
  name: string;
}

interface IUnLoginUserProps {
  isLogin: boolean;
  from: string;
}

type UserProps = ILogInUserProps | IUnLoginUserProps;

function getUserInfo(user: ILogInUserProps | IUnLoginUserProps) :string {
  return 'name' in user ? user.name : user.from;
}
Copy the code

Or through literal types:

interface ICommonUserProps {
  type: "common".accountLevel: string
}

interface IVIPUserProps {
  type: "vip";
  vipLevel: string;
}

type UserProps = ICommonUserProps | IVIPUserProps;

function getUserInfo(user: ICommonUserProps | IVIPUserProps) :string {
  return user.type === "common" ? user.accountLevel : user.vipLevel;
}
Copy the code

In the same vein, you can also use Instanceof for instance type guarding, so try it out if you’re smart.

Index type and mapping type

The index type

Before you read this section, you need to be ready for a mental shift and really realize that type programming is actually programming, because from here on out, we’re really going to be manipulating generics as variables.

Just as you often iterate over an object when writing business code, you often iterate over an interface in type programming. So you can reuse some of your programming ideas. Start by implementing a simple function that returns some key of an object:

// Suppose key is the obj key name
function pickSingleValue(obj, key) {
  return obj[key];
}
Copy the code

What do I need to define if I want to type it?

  • parameterobj
  • parameterkey
  • The return value

There is a connection between these three:

  • keyMust beobjOne of the key value names in and must bestringType (normally we would only use strings as object keys)
  • The value returned must be the key value in obj

Therefore, we get the following preliminary results:

function pickSingleValue<T> (obj: T, key: keyof T) {
  return obj[key];
}
Copy the code

Keyof is the syntax for a ** indexed type query, which returns the literal union type of the key value of the type argument that follows it, for example:

interface foo {
  a: number;
  b: string;
}

type A = keyof foo; // "a" | "b"
Copy the code

Is it like object.keys ()? The difference is that it returns a union type.

Joint Type Union Type usually use | syntax, on behalf of multiple possible values, is actually at the beginning, we had used. The main use case for union types is the conditional type section, which will be covered in a full section later.

There is no return value, so if you haven’t seen this syntax before, you might get stuck. Let’s think about for… In syntax, when iterating over an object we might write:

const fooObj = { a: 1.b: "1" };

for (const key in fooObj) {
  console.log(key);
  console.log(fooObj[key]);
}
Copy the code

If we get the key, we can get the corresponding value, so the value type is simpler:

function pickSingleValue<T> (obj: T, key: keyof T) :T[keyof T] {
  return obj[key];
}
Copy the code

This part may not be easy to understand in one step, but explain:

interface T {
a: number;
b: string;
}

type TKeys = keyof T; // "a" | "b"

type PropAType = T["a"]; // number
Copy the code

You can use the key name to retrieve the key of an object, which in turn can retrieve the key (that is, the type) of the interface

But there are obvious improvements: Keyof appears twice, and the generic T should really be restricted to object types. For the first point, just like we do in normal programming: store multiple occurrences in one variable. Remember, in type programming, generics are variables.

function pickSingleValue<T extends object.U extends keyof T> (obj: T, key: U) :T[U] {
  return obj[key];
}
Copy the code

Here’s something new: extends… What is it? You can think of T extends Object temporarily as if T is restricted to an object type. U extends Keyof T extends to a generic type. U must be an associative type of the key names of the generic type T (in the form of literal types, such as T, an object whose key names include a, B, and c, So U value can only be “a” “b” “c”, one of the “a” | | “b” “c”). We’ll cover the details in the conditional Types chapter.

Suppose that instead of fetching a single value, we want to fetch a series of values, that is, argument 2 will be an array consisting of the key names of argument 1:

function pick<T extends object.U extends keyof T> (obj: T, keys: U[]) :T[U] []{
  return keys.map((key) = > obj[key]);
}

// pick(obj, ['a', 'b'])
Copy the code

There are two important changes:

  • Keys: U[] We know that U is the union type of the key names of T, so we can use this method to represent an array whose internal elements are all T keys. For details, see the section on distributed Conditional types below.

  • T[U][] this is actually the same principle as above. The first is T[U], which represents the Key of parameter 1 (like Object[Key]). I think this is a good example of the combinativity of TS type programming.

Index Signature Index Signature

In JavaScript, we usually use arR [1] to index arrays and obj[key] to index objects. In plain English, an index is how you get a member of an object, and in type programming, index signatures are used to quickly create an interface with the same internal field types, such as

interface Foo {
  [keys: string] :string;
}
Copy the code

So interface Foo is actually equivalent to an interface with all strings and no members.

Equivalent to Record

, see tool type.
,>

It is important to note that the JS can access the object properties at the same time through the Numbers and strings, so keyof Foo would result in a string | number.

const o: Foo = {
1: "Wuhu!}; o[1] === o["1"]; // true
Copy the code

But once an interface’s index signature type is number, objects that use it can no longer be accessed through string indexes, such as O [‘1’], will throw an error, and elements implicitly have type “any” because the index expression is not of type “number”.

Mapped Types

Before we start mapping types, let’s think about the JavaScript map method for arrays. By using a map, we get a new array from an array according to a given mapping. In type programming, we map a new type definition from a type definition (including but not limited to interfaces and type aliases). It is usually modified on the basis of an old type, for example:

  • Modify the key type of the original interface
  • Add modifiers for the original interface key value type, such asreadonlyWith the optional?

Start with a simple scenario:

interface A {
  a: boolean;
  b: string;
  c: number;
  d: () = > void;
}
Copy the code

Now we have A requirement to implement an interface whose fields are exactly the same as interface A, but whose types are all strings. What do you do? Just restate one and write it by hand? That’s crazy. We’re smart programmers.

If you want to copy an object (assuming no nesting, regardless of the location of the reference type variable), the usual way to do this is to first create a new empty object, and then iterate over the original object’s key-value pairs to populate the new object. The same is true for interfaces:

type StringifyA<T> = {
  [K in keyof T]: string;
};
Copy the code

Does that sound familiar? The important thing is that the in operator, which you can easily read as for… in/for… The “of” approach is simple, so we can easily copy a new type alias.

type ClonedA<T> = {
  [K in keyof T]: T[K];
};
Copy the code

With this in mind, you’ve already touched on some low-level implementations of tool types:

You can think of utility types as public functions that you normally put in the utils folder, providing a wrapper around common logic (in this case, type programming logic), such as the two type interfaces above. For more tool types, see the tool Types chapter.

Partial: Partial: Partial: Partial: Partial: Partial

// Make all fields under the interface optional
type Partial<T> = {
  [K inkeyof T]? : T[k]; };Copy the code

key? : value for this field is optional, in most cases is equal to the key: value | is undefined.

Conditional Types

In programming, we often use If statements and ternary expressions. I personally prefer the latter, even If:

if (condition) {
  execute()
}
Copy the code

If statements like this without else, I like to write:

condition ? execute() : void 0;
Copy the code

The syntax for conditional types is, in effect, a ternary expression. Let’s look at the simplest example:

T extends U ? X : Y
Copy the code

If the extends extends here is a bit confusing, for now it can be simplified to say that all properties in U have properties in T.

Why are there conditional types? You can see that conditional types are often used in conjunction with generics, and I think you get the idea, given how generics are used and why it’s worth delaying inference. For scenarios where the type cannot be determined in real time, use conditional types to dynamically determine the final type at runtime (the runtime may be inaccurate, or it can be interpreted as dynamically determining the type constraints that need to be satisfied based on the parameters passed in when the function you provide is being used by someone else).

In a programming statement, the value of a variable is dynamically assigned according to the condition:

let unknownVar: string;

unknownVar = condition ? "Amoy front end" : "Taobao FED";

type LiteralType<T> = T extends string ? "foo" : "bar";
Copy the code

Condition type understand actually also very intuitive, the only need to have a certain understanding of the cost of the type system is when conditions will gather enough information to determine the type, that is to say, sometimes not immediately complete type judgment, such as tool library provides functions, requires the user to type are not complete in the incoming parameters when using conditions of judgment.

Before we get to that, let’s look at a common scenario for conditional types: generic constraints, which are actually our example of index types above:

function pickSingleValue<T extends object.U extends keyof T> (obj: T, key: U) :T[U] {
  return obj[key];
}
Copy the code

Both T extends Object and U extends Keyof T are generic constraints, binding T to an object type and U to a literal union type of T key name, respectively. Tip: 1 | 2 | 3). We often use generic constraints to narrow down type constraints. Simply put, generics are inherently arbitrary, and all types can be passed in explicitly (Array

) or derived implicitly (foo(1)). This is not desirable, as we sometimes detect function arguments:

function checkArgFirst(arg){
  if(typeofarg ! = ="number") {throw new Error("arg must be number type!")}}Copy the code

In TS, we through generic constraint, requires the incoming generics can only be fixed type, such as T extends {} constraint generics to object types, T extends number | string will be generic constraint to type Numbers and strings.

Take an example of using a condition type as a function return value type:

declare function strOrNum<T extends boolean> (
  x: T
) :T extends true ? string : number;
Copy the code

In this case, the derivation of the conditional type is delayed because the type system does not have enough information to complete the judgment.

The derivation can only be completed if the required information (in this case, the type of the input parameter X) is given.

const strReturnType = strOrNum(true);
const numReturnType = strOrNum(false);
Copy the code

Similarly, just as ternary expressions can be nested, condition types can also be nested. If you look at some framework source code, you will also find that there are many nested condition types. Otherwise, condition types can be squeezed into a very narrow range of type constraints, providing precise condition types, such as:

type TypeName<T> = T extends string
  ? "string"
  : T extends number
  ? "number"
  : T extends boolean
  ? "boolean"
  : T extends undefined
  ? "undefined"
  : T extends Function
  ? "function"
  : "object";
Copy the code

Distributive Conditional Types

A distributed condition type is not actually a special condition type, but rather one of its characteristics (so it is more accurate to say that a condition type is distributed). Let’s get straight to the concept: for check types that are bare type parameters, the condition type is automatically distributed to the union type at instantiation time.

The original: Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation

Let’s take a few key words and then use examples to clarify the concept:

  • Bare type parameters (type parameters are generics, as described in the generics section at the beginning of this article)
  • instantiation
  • Distribute to the federated type
// Use the TypeName alias as above

// "string" | "function"
type T1 = TypeName<string | (() = > void) >;// "string" | "object"
type T2 = TypeName<string | string[] >;// "object"
type T3 = TypeName<string[] | number[] >;Copy the code

We found that in the above example, the conditions in the derived results are joint types (T3 is actually, only be merged because the result is the same), and is actually a type parameter is, in turn, the condition judgment, the reuse | combination of results.

Did you get something? In the example above, generics are all bare. If they are covered, how will their conditional type judgment change? Let’s take another example:

type Naked<T> = T extends boolean ? "Y" : "N";
type Wrapped<T> = [T] extends [boolean]?"Y" : "N";

// "N" | "Y"
type Distributed = Naked<number | boolean>;

// "N"
type NotDistributed = Wrapped<number | boolean>;
Copy the code
  • Among them, the Distributed type alias, its type parameters (the number | Boolean) will be the right distribution, namely

    To distribute to Naked < number > | Naked < Boolean >, and then to judge, so the result is “N” | “Y”.

  • And NotDistributed type alias, at first glance feel TS should automatically according to the distributed array, the result should be “N” | “Y”? But in fact, its type parameters (the number | Boolean) does not have a distribution process, directly [number | Boolean] extends Boolean judgment, so the result is “N”.

Now we can talk about these concepts:

  • A bare parameter that is not wrapped in [] is no longer called a bare parameter when wrapped in an array.

  • Instantiation, in fact, is the process of determining the condition type. As we said earlier, the condition type needs to collect enough inferred information before this process can be carried out. The instantiation process for the two examples here is actually different, as described in the next section.

  • Distribute to union type:

    • For TypeName, its internal type parameter T is not wrapped, so TypeName < string | () = > (void) > will be distributed to TypeName < string > | TypeName < () = > (void) >, Then judge again, and finally distributed to “string” | “function”.

    • Abstract the concrete process:

      ( A | B | C ) extends T ? X : Y
      / / equivalent to
      (A extends T ? X : Y) | (B extends T ? X : Y) | (B extends T ? X : Y)
      
      // After using [] packages, no additional distribution logic is performed.
      [A | B | C] extends [T] ? X : Y
      Copy the code

      In a nutshell: union type parameters that are not wrapped in [] are distributed when the condition type is evaluated, and judged separately.

There is no good or bad between these two behaviors, the only difference is whether to distribute the joint type. If you need to distribute the distributed condition type, be careful to keep your type parameters bare. If you want to avoid this behavior, wrap your type parameters with [] (note that you need both sides of the extends keyword).

Infer the keyword

In condition typing, we showed how to delay type determination by conditional judgment, but using condition typing alone has its drawbacks: it doesn’t get type information from conditions. For example, T extends Array ? For the example of “foo” : “bar”, we can’t get the actual type of PrimitiveType from the conditional Array .

Such scenarios are quite common, such as getting the type of function return value and unboxing Promise/array, so in this section we will introduce the infer keyword.

Infer is the abbreviation of inference. It is commonly used to modify generics as type parameters, such as infer R, which means types to be inferred. Ordinarily infer is not used directly, but is placed in the underlying tool type, along with the condition type. If condition types provide the ability to delay inferences, adding infer provides the ability to delay inferences based on conditions.

Look at a simple example of the utility type ReturnType used to get the return value type of a function:

const foo = (): string= > {
  return "linbudu";
};

type ReturnType<T> = T extends(... args:any[]) => infer R ? R : never;

// string
type FooReturnType = ReturnType<typeof foo>;
Copy the code
  • (… Args: any[]) => Infer R is a whole, where the position of return value type of function is occupied by Infer R.

  • When ReturnType is called, type parameters T and R are explicitly assigned (T is typeof foo, and infer R is assigned to string as a whole, i.e., the ReturnType of the function). If T satisfies the constraints of the conditional type, the value of R is returned. Here R is the actual type of the return value of the function.

  • In fact, for the sake of rigor, we should constrain the generic T to be a function type, i.e. :

    // The first extends constraint that generics that can be passed in can only be function types
    // The second extends is a conditional judgment
    type ReturnType<T extends(... args:any[]) = >any> = T extends(... args:any[]) => infer R ? R : never;
    Copy the code

Infer may not be a good way to use infer. We can use a common example of front-end development, where the page is initialized to show placeholder interactions such as Loading/skeleton screen and render real data after the request is returned. Infer is the same idea. After the type system obtains enough information (usually from delayed inference of conditions), it can deduce the type parameters followed by infer and usually return the result of this inference.

Similarly, we can use this idea to get function input types, constructor input types of classes, and even types inside promises, which we’ll talk about later.

Also, in the case of function overloads in TS, infer (ReturnType above) does not perform the derivation for all overloads, only the last overload (because in general the last overload is usually the most extensive case) will be used.

Tool Type Tool Type

This chapter is probably the best value for money part of this article, because even if you read this section without knowing much about how these tool types are implemented, you won’t be able to use it properly, just as Lodash doesn’t require you to know the principles of every function you use.

This section includes the types of built-in TS tools and the types of extended tools in the community. I personally recommend that you record some tool types after completing the study, such as the tool types that you think are valuable, that you may use in your current or future business, or that you just think are fun. Create a new.d.ts file in your own project (or /utils/tool-types.ts) to store it.

Before moving on, make sure you have the knowledge above, which is the foundation of the tool type.

Built-in tool types

Above we have implemented one of the most used built-in tool types:

type Partial<T> = {
  [K inkeyof T]? : T[k]; };Copy the code

It is used to make all fields in an interface optional. In addition to the index type and mapping type, it only uses? Optional modifier, so I’ll just whip out my cheat sheet now:

  • Remove optional modifiers:-?, the position and?consistent
  • Read-only modifiers:readonly, the position in the key name, such asreadonly key: string
  • Remove read-only modifiers:-readonly, the position withreadonly.

Congratulations, you get Required and Readonly (the tool type that removes the Readonly modifier is not built in, as we’ll see later) :

type Required<T> = {
  [K inkeyof T]-? : T[K]; };type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};
Copy the code

Above we implement a pick function:

function pick<T extends object.U extends keyof T> (obj: T, keys: U[]) :T[U] []{
  return keys.map((key) = > obj[key]);
}
Copy the code

Similarly, suppose we now need to pick some fields from an interface:

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

// Expected usage
/ / expectation result A | / "A" type A "b" type
type Part = Pick<A, "a" | "b">;
Copy the code

It’s still a mapping type, except now the mapping source of the mapping type is the type parameter K passed to Pick.

8. Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit

8. Pick and Omit incoming keys

Here’s another tip: the never type, which represents a type that never occurs, is often used to narrow down union types or interfaces, or as a backstop for conditional type judgments. For details, please refer to uHP’s zhihu answer. We will not introduce it here.

The above scenario can be simplified as:

/ / | "3" "4" | "5"
type LeftFields = Exclude<"1" | "2" | "3" | "4" | "5"."1" | "2">;
Copy the code

Exclude: The first parameter should be the filter, and the second should be the filter condition. Try this first:

In fact, the distributed conditional type feature is used here. Assuming that Exclude accepts both type parameters T and U, the type in the T union type is evaluated against the type U in turn. If the type parameter is in U, it is eliminated (assigned to never).

Grounding gas version: “1” in the “1” | “2” inside it (” 1 “extends” 1 “|” 2 “- > true)? If it is, discard it (assign it to never), if it is not, leave it.

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

Therefore, it is simple to Omit the members of the original interface and apply Pick to eliminate the incoming members of the association type.

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Copy the code

Exclude excludes the key name. Exclude excludes the key name. Exclude excludes the key name.

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

Record

is usually used to generate new interfaces with Keys of the union Type and Keys of Type, such as:
,>

type MyNav = "a" | "b" | "b";
interface INavWidgets {
  widgets: string[]; title? :string; keepAlive? :boolean;
}
const router: Record<MyNav, INavWidgets> = {
  a: { widget: [""]},b: { widget: [""]},c: { widget: [""]}};Copy the code

Select the value of each key and set it to Type Type

// K extends Keyof any constraint K must be of the union type
type Record<K extends keyof any, T> = {
  [P in K]: T;
};
Copy the code

Note that Record also supports Record

. String extends keyof any is also true. Because the end result of keyof must be a combination of strings (except when numbers are used as key names…). .
,>

In the infer section earlier we implemented a ReturnType to retrieve the return value of a function:

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

In fact, change the infer position, such as at the input parameter, and it becomes Parameters to fetch the parameter type:

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

If you are a little more adventurous and replace a normal function with a class constructor, you get ConstructorParameters that get the type of the class constructor’s input parameter:

type ConstructorParameters<
  T extends new(... args:any) = >any
> = T extends new(... args: infer P) =>any ? P : never;
Copy the code

Add the new keyword to make it an instantiable type declaration, where the generics are constrained to be classes.

This is the constructor input parameter type to get the class. If we put the type to infer into its return, think about what is the return value of a new class? The instance! So we get the InstanceType InstanceType:

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

From these examples, you should get a sense that type programming does not have a particularly arcane syntax. It tests your knowledge of the basics of indexing, mapping, conditional typing, and the ability to generalize from one example to another. The types of community tools we’ll look at below are essentially a combination of basic types, starting with common scenarios and complementing what’s not officially covered.

Community tool types

Most of the tools in this section come from Utility-types, and the authors of this section also include react-Redux-typescript guide and typesafe-Actions.

Also recommended is the Type-fest library, which is a bit more down to earth. The work of its author… , I guarantee you’ve used it directly or indirectly (if you don’t believe me, check it out, I was really shocked when I first saw it).

We’ll start by encapsulating the base type alias and the corresponding type guard:

export type Primitive =
  | string
  | number
  | bigint
  | boolean
  | symbol
  | null
  | undefined;

export const isPrimitive = (val: unknown): val is Primitive => {
  if (val === null || val === undefined) {
    return true;
  }

  const typeDef = typeof val;

  const primitiveNonNullishTypes = [
    "string"."number"."bigint"."boolean"."symbol",];returnprimitiveNonNullishTypes.indexOf(typeDef) ! = = -1;
};

export type Nullish = null | undefined;

export type NonUndefined<A> = A extends undefined ? never : A;
// In fact, TS also has this tool type built in
type NonNullable<T> = T extends null | undefined ? never : T;
Copy the code

Falsy and isFalsy we’ve already shown up here.

To warm up our infer memory, let’s look at a common scenario that extracts the actual type of promises:

const foo = (): Promise<string> = > {return new Promise((resolve, reject) = > {
    resolve("linbudu");
  });
};

// Promise<string>
type FooReturnType = ReturnType<typeof foo>;

// string
type NakedFooReturnType = PromiseType<FooReturnType>;
Copy the code

If you are already comfortable with infer, it is actually quite easy to write. Just use a infer parameter as a generic Promise:

export type PromiseType<T extends Promise<any>> = T extends Promise<infer U>
  ? U
  : never;
Copy the code

Infer R is used to wait for the type system to deduce the specific type of R.

Recursive tool type

Previously we wrote a Partial Readonly Required and other utility types that modify interface fields, but they all have limitations. What if there is nesting in the interface?

type Partial<T> = {
  [P inkeyof T]? : T[P]; };Copy the code

Get the logic straight:

  • If it’s not an object type, it just adds?The modifier
  • If it is an object type, it iterates through the inside of the object
  • Repeat the process.

We’ve seen this a couple of times. T extends Object. How do you traverse the interior of an object? It’s actually recursion.

export type DeepPartial<T> = {
  [P inkeyof T]? : T[P]extends object ? DeepPartial<T[P]> : T[P];
};
Copy the code

The internal implementation of utility-types is actually more complex than this, taking into account the case of arrays, which is simplified here for ease of understanding, as is the case for later utility types.

So deepreobly and DeepRequired are simple:

export type DeepMutable<T> = {
  -readonly [P in keyof T]: T[P] extends object ? DeepMutable<T[P]> : T[P];
};

/ / DeepReadonly namely
export type DeepImmutable<T> = {
  +readonly [P in keyof T]: T[P] extends object ? DeepImmutable<T[P]> : T[P];
};

export type DeepRequired<T> = {
  [P inkeyof T]-? : T[P]extends object | undefined ? DeepRequired<T[P]> : T[P];
};
Copy the code

Especially pay attention to the DeepRequired, its condition is T type judgment [P] extends object | undefined, because nested object types may be optional (undefined), if only use object, may lead to incorrect results.

Another way to save worry is not to judge the condition type, and directly recurse all attributes ~

Returns the tool type of the key name

In some cases we may need a utility type that returns a combination of interface field keys and then uses that combination for further operations (such as Pick or Omit).

  • Optional/Required/read-only/non-read-only fields
  • Field of (non-) object/(non-) function/type

Here’s the simplest FunctionTypeKeys:

export type FunctTypeKeys<T extends object> = {
  [K inkeyof T]-? : T[K]extends Function ? K : never;
}[keyof T];
Copy the code

{[K in keyof T]: … }[keyof T] [keyof T]

interface IWithFuncKeys {
  a: string;
  b: number;
  c: boolean;
  d: () = > void;
}

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

type UseIt1 = WTFIsThis<IWithFuncKeys>;
Copy the code

It is easy to deduce that UseIt1 is actually:

type UseIt1 = {
  a: never;
  b: never;
  c: never;
  d: "d";
};
Copy the code

UseIt saves all fields. The key value of a field that meets the requirement is a literal type (that is, the key name). The key value of a field that does not meet the requirement is never.

Add the following:

// "d"
type UseIt2 = UseIt1[keyof UseIt1];
Copy the code

The process is similar to permutation: values of type never do not appear in union types

To get rid of / / never type is automatically string | number
type WithNever = string | never | number;
Copy the code

So {[K in keyof T]:… }[keyof T] this is actually written to return the key name (in preparation, a combination of key names).

Optional OptionalKeys and required RequiredKeys. Here’s a quick example:

type WTFAMI1 = {} extends { prop: number}?"Y" : "N";
type WTFAMI2 = {} extends{ prop? :number}?"Y" : "N";
Copy the code

If we can get around it, it’s easy to get the answer. It’s easy if you don’t get around it for a moment. For the previous case, prop is required, so the empty object {} does not satisfy extends {prop: number}, as it does if prop is optional.

Therefore, we use this idea to get optional/required key names.

  • {} extends Pick<T, K>If theKIs an optional field, then leave it (OptionalKeys, or remove it if it is RequiredKeys).
  • How to eliminate? Of course is to usenever.
export type RequiredKeys<T> = {
  [K inkeyof T]-? : {}extends Pick<T, K> ? never : K;
}[keyof T];
Copy the code

OptionalKeys are left as follows:

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

Alter table IMmutableKeys; alter table IMmutableKeys;

interface MutableKeys {
  readonlyKeys: never;
  notReadonlyKeys: "notReadonlyKeys";
}
Copy the code

Then get the field name that is not never.

First, define a tool type IfEqual to compare whether the two types are the same, even before and after modification, which is read-only and non-read-only.

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
  • Don’t be<T>() => T extends X ? 1:2Interference, which can be understood as a wrapper for comparison, distinguishes read-only from non-read-only properties. namely(<T>() => T extends X ? 1:2)This part is only available in type parametersXExactly the same, two(<T>() => T extends X ? 1:2)‘will be congruent, this consistency requires read-only, optional and other modifications to be consistent.
  • In practice (in the non-read-only case, for example), we pass in the interface for X and remove the read-only attribute for Y-readonly, so that all keys are compared once against keys with read-only attributes removed. Pass in the field name for A, and the field name for B is never, so we don’t have to do that.

Example:

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

A few tricky points:

  • The generic Q is not actually used here, just a field placeholder for the mapping type.
  • X and Y also have distributed condition types to compare before and after field readonly removal.

The same goes for:

export type IMmutableKeys<T extends object> = {
  [P inkeyof T]-? : Equal< { [Qin P]: T[P] },
    { -readonly [Q in P]: T[P] },
    never,
    P
  >;
}[keyof T];
Copy the code
  • This is not rightreadonlyModifier operations, but rather a judgment statement of the type of an exchange condition.

Value type based Pick and Omit

The previous implementation of Pick and Omit is based on key names. Suppose we need to select and Omit according to value types?

T[K] extends ValueType T[K] extends ValueType

export type PickByValueType<T, ValueType> = Pick<
  T,
  { [Key inkeyof T]-? : T[Key]extends ValueType ? Key : never }[keyof T]
>;

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

Condition types take on too much…

Overview of Tool Types

To summarize the types of tools we wrote above:

  • Fully modified interface:Partial Readonly(Immutable) Mutable Required, and the corresponding recursive version.
  • Clipping interface:Pick Omit PickByValueType OmitByValueType
  • Based on the infer:ReturnType ParamType PromiseType
  • Gets the specified condition field:FunctionKeys OptionalKeys RequiredKeys.

Note that sometimes a single tool type is not enough for you, and you may need multiple tool types to work together, such as FunctionKeys + Pick to get a field of type function in an interface.

In addition, some of the above tool types could actually be implemented more elegantly with the ability to remap.

Due to the lack of space (this article is 1.3 million words long), the type-Fest tool type that I would have liked to post is regrettablished, but I recommend that you read the source code. More down-to-earth and interesting than utility-types above.

Some of the new features in TypeScript 4.x

This section is a new addition to the previous release and includes some of the new features introduced in 4.1-4.4 (Beta) that are relevant to this article, including template literal typing and remapping.

Template literal types

The introduction of template literal types in TypeScript 4.1 allows us to construct literal types using the ${} syntax, such as:

type World = 'world';

// "hello world"
type Greeting = `hello ${World}`;
Copy the code

Template literals also support distributed conditional types, such as:

export type SizeRecord<Size extends string> = `${Size}-Record`

// "Small-Record"
type SmallSizeRecord = SizeRecord<"Small">
// "Middle-Record"
type MiddleSizeRecord = SizeRecord<"Middle">
// "Huge-Record"
type HugeSizeRecord = SizeRecord<"Huge">


// "Small-Record" | "Middle-Record" | "Huge-Record"
type UnionSizeRecord = SizeRecord<"Small" | "Middle" | "Huge">
Copy the code

Another interesting point is that union types can be passed in the template slot (${}), and if there are multiple slots in the same template, each union type will be arranged and combined separately.

// "Small-Record" | "Small-Report" | "Middle-Record" | "Middle-Report" | "Huge-Record" | "Huge-Report"
type SizeRecordOrReport = `The ${"Small" | "Middle" | "Huge"}-The ${"Record" | "Report"}`;
Copy the code

With that comes four new tool types:

type Uppercase<S extends string> = intrinsic;

type Lowercase<S extends string> = intrinsic;

type Capitalize<S extends string> = intrinsic;

type Uncapitalize<S extends string> = intrinsic;
Copy the code

What they do is literally, I won’t explain it. For PR see 40336, Anders Hejlsberg is the lead architect of C# and Delphi, and one of the authors of TS.

Intrinsic indicates that these utility types are implemented internally by the TS compiler and that we cannot change literal values through type programming, but I think it is possible that TS programming will support calling Lodash methods in the future.

TS implementation code:

function applyStringMapping(symbol: Symbol, str: string) {
switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
  case IntrinsicTypeKind.Uppercase: return str.toUpperCase();
  case IntrinsicTypeKind.Lowercase: return str.toLowerCase();
  case IntrinsicTypeKind.Capitalize: return str.charAt(0).toUpperCase() + str.slice(1);
  case IntrinsicTypeKind.Uncapitalize: return str.charAt(0).toLowerCase() + str.slice(1);
}
return str;
}
Copy the code

You might be thinking, what if I want to intercept part of a template literal? There is no way to call the slice method. Infer can be used to infer parts of a literal, such as:

type CutStr<Str extends string> = Str extends `${infer Part}budu` ? Part : never

// "lin"
type Tmp = CutStr<"linbudu">
Copy the code

[${infer Member1}, ${infer Member2}, ${infer Member}]

type ExtractMember<Str extends string> = Str extends ` [${infer Member1}.${infer Member2}.${infer Member3}] ` ? [Member1, Member2, Member3] : unknown;

/ / / "1", "2", "3"]
type Tmp = ExtractMember<"[1, 2, 3]." ">
Copy the code

Notice that the template slots here are used and separated. If multiple infer slots are placed right next to each other, the previous infer will receive only a single character, and the last infer will receive all remaining characters (if any). For example, we changed the example above to something like this:

type ExtractMember<Str extends string> = Str extends ` [${infer Member1}${infer Member2}${infer Member3}] ` ? [Member1, Member2, Member3] : unknown;

// ["1", ",", "2, 3"]
type Tmp = ExtractMember<"[1, 2, 3]." ">
Copy the code

This feature allows us to use multiple adjacent infer + slots to perform recursive operations on the value obtained from the last infer, as in:

type JoinArrayMember<T extends unknown[], D extends string> =
  T extends[]?' ' :
  T extends [any]?`${T[0]}` :
  T extends [any. infer U] ?`${T[0]}${D}${JoinArrayMember<U, D>}` :
  string;

/ / ""
type Tmp1 = JoinArrayMember<[], '. '>;
/ / "1"
type Tmp3 = JoinArrayMember<[1].'. '>;
/ / "2"
type Tmp2 = JoinArrayMember<[1.2.3.4].'. '>;
Copy the code

Add the first member of the array at a time. Do nothing on the last member and return an empty string on the last match ([]).

Or vice versa? Return 1.2.3.4 to array form?

type SplitArrayMember<S extends string, D extends string> =
  string extends S ? string[] :
  S extends ' ' ? [] :
  S extends `${infer T}${D}${infer U}` ? [T, ...SplitArrayMember<U, D>] :
  [S];

type Tmp11 = SplitArrayMember<'foo'.'. '>;  // ['foo']
type Tmp12 = SplitArrayMember<'foo.bar.baz'.'. '>;  // ['foo', 'bar', 'baz']
type Tmp13 = SplitArrayMember<'foo.bar'.' '>;  // ['f', 'o', 'o', '.', 'b', 'a', 'r']
type Tmp14 = SplitArrayMember<any.'. '>;  // stri 

Copy the code

Finally, seeing forms like A.B.C, you should think of Lodash’s get method, which quickly gets nested attributes in the form of GET ({},” A.B.C “). But how do you provide a type declaration? With template literal types in place, you can simply combine infer + conditional types.

type PropType<T, Path extends string> =
    string extends Path ? unknown :
    Path extends keyof T ? T[Path] :
    Path extends `${infer K}.${infer R}` ? K extends keyof T ? PropType<T[K], R> : unknown :
    unknown;

declare function getPropValue<T.P extends string> (obj: T, path: P) :PropType<T.P>;
declare const s: string;

const obj = { a: { b: {c: 42.d: 'hello' }}};
getPropValue(obj, 'a');  // { b: {c: number, d: string } }
getPropValue(obj, 'a.b');  // {c: number, d: string }
getPropValue(obj, 'a.b.d');  // string
getPropValue(obj, 'a.b.x');  // unknown
getPropValue(obj, s);  // unknown
Copy the code

Heavy mapping

This ability in TS 4.1 introduction, provides the redirection in mapping type mapping source to a new type of ability, the new type can be a tool of returned results, literal template type, etc., are used to solve when using mapping type, we want to filter/copy of the new interface members, usually to convert the original interface member keys as a new method parameters, Such as:

type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`] :() = > T[K]
};

interface Person {
    name: string;
    age: number;
    location: string;
}

type LazyPerson = Getters<Person>;
Copy the code

Results after conversion:

type LazyPerson = {
    getName: () = > string;
    getAge: () = > number;
    getLocation: () = > string;
}
Copy the code

The string here & k because of heavy mapping transformation method (that is, as the latter part of) must be assigned to a string | number | symbol, and k from the keyof, may contain symbol type, This cannot be handed over to template literals.

If the conversion method returns never, the member is removed, so we can use this method to filter out members.

type RemoveKindField<T> = {
    [K in keyof T as Exclude<K, "kind">]: T[K]
};

interface Circle {
    kind: "circle";
    radius: number;
}

// type KindlessCircle = {
// radius: number;
// }
type KindlessCircle = RemoveKindField<Circle>;
Copy the code

Finally, when used with template literals, due to their permutation and composition nature, you get multiple members from permutations if the remapped conversion method is a union of template literal types.

type DoubleProp<T> = { [P in keyof T & string as `${P}1 ` | `${P}2 `]: T[P] }
type Tmp = DoubleProp<{ a: string.b: number} >.// { a1: string, a2: string, b1: number, b2: number }
Copy the code

The end of the

This article is really very long, because it is not recommended to be swallowed at one time to read, it is recommended to select a few paragraphs have a certain length of continuous time, to break it up and knead the good read. Writing isn’t easy, especially if it’s this long, but it’s worth it if it helps you take TypeScript to the next level.

If you’ve never been interested in type programming before, it may take you a while to adjust to a change in thinking after reading this. Again, realize that type programming is programming by nature. Of course, you can start practicing this gradually, for example, from today on your current project, from generics to type guarding, from index/map types to conditional types, from using tool types to wrapping tool types, and becoming a master of TypeScript.