The extends keyword appears very frequently in TS programming and behaves differently in different type scenarios, so here’s a summary:

  • Inheritance/extension
  • The constraint
  • distribution

Usage scenarios

In the class class

class Animal { public kind = 'animal' constructor(kind) { this.kind = kind } say() { console.log(`I am ${this.kind}`) } } class Cat extends Animal { constructor(kind) { super(kind) } bark() { console.log('miao miao ~~~') } } const cat = new  Cat('cat') cat.kind // => 'cat' cat.say() // I am catCopy the code

Animal say (cat); Animal say (cat); Animal say (cat)

Generic constraint

function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length);
  // Property 'length' does not exist on type 'Type'.
  return arg;
}
Copy the code
interface Lengthwise {
  length: number;
}
 
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length); // Now we know it has a .length property, so no more error
  return arg;
}

// error Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
loggingIdentity(3);

// ok
loggingIdentity({ length: 10, value: 3 });
Copy the code

Conditions in the

SomeType extends OtherType ? TrueType : FalseType;
Copy the code

When a type on the left side of extends can be assigned to a type on the right, you get that type in the first branch (the “true” branch); Otherwise, you get the type in a later branch (the “false” branch).

interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}
 
type Example1 = Dog extends Animal ? number : string; // number
 
type Example2 = RegExp extends Animal ? number : string; // string
Copy the code

A deeper understanding of the left type can be assigned to the right type:

type Human = {
  name: string;
}
type lookasHuman = {
  name: string;
}
type Bool = lookasHuman extends Human ? 'yes' : 'no'; // Bool => 'yes'
Copy the code
type Human = {
  name: string;
}
type lookasHuman = {
  name: string;
  age: number;
}
type Bool = lookasHuman extends Human ? 'yes' : 'no'; // Bool => 'yes'
Copy the code

The code runs with yes, which means that lookasHuman extends Human is true, meaning that left-side types can be assigned to right-side types. In other words, lookasHuman satisfies all the constraints of Human. A extends B here means that type A can be assigned to type B, not that type A is A subset of type B

type Human = {
  name: string;
  age: number
}
type lookasHuman = {
  name: string;
}
type Bool = lookasHuman extends Human ? 'yes' : 'no'; // Bool => 'no'
Copy the code

No age attribute in lookasHuman above does not satisfy the Human constraint

Higher order type

type A1 = 'x' extends 'x' ? string : number // string
type A2 = 'x' | 'y' extends 'x' ? string : number // number
type A3 = 'y' | 'x' extends 'x' ? string : number // number
type A4 = 'y' extends 'x' | 'y' ? string : number // string
type A5 = 'y' | 'x' extends 'x' | 'y' ? string : number // string

type P<T> = T extends 'x' ? string : number
type B = P<'x' | 'y'> // string | number
Copy the code

The extends of A1 is a common use of a condition type

Extends from A2 to A5: Extends is a subset of the union type if it precedes or follows the union type. A extends B here means that type A is A subset of type B

Now look at the extends of B:

Distributive Conditional Types

When conditional types act ona generic type, they become distributive When given a union type They become distribution types.)

For conditional types that use the extends keyword (that is, the ternary expression type above), if the argument before extends is a generic type, then when the argument is passed in an associative typeDistributive lawCalculate the final result. The distributive property says, Split the union items of the union type into individual items, substitute the condition types respectively, and then substitute each item into the results, and then combine them together to get the final judgment result.

If we plug a union type into ToArray, Then the conditional type will be applied to each member of that union.

type ToArray<Type> = Type extends string ? string : number;
 
type StrArrOrNumArr = ToArray<string | number>;
Copy the code

Extends above the front is a generic variable Type to the Type of the incoming string | number of joint Type, and then be distributed:

ToArray<string | number> = ToArray | ToArray

Separately into:

ToArray : Type extends string ? string : number; // string

ToArray: Type extends string ? string : number; // number

And then will get the result of each item together, get the string | number

The allocation condition type must meet the following two requirements

  • Parameters are generic types
  • The parameter is the union type

never

Type A = never extends 'x'? string : number; // string type P<T> = T extends 'x' ? string : number; type B = P<never> // neverCopy the code

In the above example, the result of B and A is different. It seems that never is not A union type, so we can substitute the definition of the condition type.

In fact, the conditional assignment type is still at work here. Never considered empty joint type, that is to say, there is no joint type of joint, so meet the distributive law of the above, however, because there is no joint can be allocated, so P < T > expression actually no execution, so the definition of B is similar Yu Yongyuan function does not return, is never type.

Prevent assignments in conditional judgments

Type P<T> = [T] extends ['x']? string : number; type A1 = P<'x' | 'y'> // number type A2 = P<never> // stringCopy the code

In the definition of a conditional judgment type, generic parameters are used[]Block the assignment of the condition judgment type. In this case, the type of the passed parameter T is treated as a whole and no longer assigned.

Application in advanced types

  • Exclude

    Exclude is an advanced type in TS. It excludes all union items that occur in the second union type from the first union type argument, leaving only parameters that do not occur.

type A = Exclude<'key1' | 'key2', 'key2'> // 'key1'
Copy the code

Definition of Exclude ****

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

This definition takes advantage of the assignment principle in conditional types to try to break instances apart and see what happens:

Type A = ` Exclude < 'key1' | 'key2', 'key2' > ` / / equivalent to type A = ` Exclude < 'key1', 'key2' > ` | ` Exclude < 'key2', 'key2'>` // => type A = ('key1' extends 'key2' ? never : 'key1') | ('key'2 extends 'key2' ? Never: 'key2') / / = > / / never is all types of subtype type A = 'key1' | never = 'key1'Copy the code
  • Extract

    The advanced Extract type does the opposite of Exclude. It extracts the second parameter’s joint from the first parameter’s joint. Of course, the second parameter can contain items that the first parameter does not.

type Extract<T, U> = T extends U ? T : never
type A = Extract<'key1' | 'key2', 'key1'> // 'key1'
Copy the code
  • Pick

    Extends’s condition judgment, in addition to defining condition types, can also be used in generic expressions to constrain generic parameters

Type Pick<T, K extends Keyof T> = {[P in K]: T[P]} interface A {name: string; age: number; sex: number; } type A1 = Pick < A, 'name' | 'age' > / / error: type "A" key "|" noSuchKey "" does not meet the constraint" keyof A "type A2 = Pick < A, 'name' | 'noSuchKey >Copy the code

Pick mean, from T, the interface will be involved in the joint type K of the selected item, forming a new interface, which extends K keyof T K is used to constraint conditions, namely, the incoming parameter K must make this condition is true, otherwise ts will be an error, that is to say, K T the properties of the joint item must come from the interface.