1. Type guard

{ const convertToUpperCase = (strOrArray: string | string[]) => { if (typeof strOrArray === 'string') { return strOrArray.toUpperCase(); } else if (Array.isArray(strOrArray)) { return strOrArray.map(item => item.toUpperCase()); }}}Copy the code

The purpose of a type guard is to trigger type narrowing. In fact, it can also be used to distinguish between different members of a set of types.

How do you distinguish union types

Common type guards include switch, literal identity, Typeof, Instanceof, in, and custom type guards

switch

{
  const convert = (c: 'a' | 1) => {
    switch (c) {
      case 1:
        return c.toFixed(); // c is 1
      case 'a':
        return c.toLowerCase(); // c is 'a'
    }
  }
  const feat = (c: { animal: 'panda'; name: 'China' } | { feat: 'video'; name: 'Japan' }) => {
    switch (c.name) {
      case 'China':
        return c.animal; // c is "{ animal: 'panda'; name: 'China' }"
      case 'Japan':
        return c.feat; // c is "{ feat: 'video'; name: 'Japan' }"
    }
  };
}
Copy the code

Literals are identical

const convert = (c: 'a' | 1) => {
    if (c === 1) {
      return c.toFixed(); // c is 1
    } else if (c === 'a') {
        return c.toLowerCase(); // c is 'a'
    }
  }
Copy the code

typeof

const convert = (c: 'a' | 1) => {
    if (typeof c === 'number') {
      return c.toFixed(); // c is 1
    } else if (typeof c === 'string') {
        return c.toLowerCase(); // c is 'a'
    }
  }
Copy the code

instanceof

{ class Dog { wang = 'wangwang'; } class Cat { miao = 'miaomiao'; } const getName = (animal: Dog | Cat) => { if (animal instanceof Dog) { return animal.wang; } else if (animal instanceof Cat) { return animal.miao; }}}Copy the code

in

const getName = (animal: Dog | Cat) => {
    if ('wang' in animal) { // ok
      return animal.wang; // ok
    } else if ('miao' in animal) { // ok
      return animal.miao; // ok
    }
}
Copy the code

Custom type guards

const isDog = function (animal: Dog | Cat): animal is Dog { return 'wang' in animal; } const getName = (animal: Dog | Cat) => { if (isDog(animal)) { return animal.wang; }}Copy the code

How to distinguish an enumeration type?

{ enum A { one, two } enum B { one, two } const cpWithNumber = (param: A) => { if (param === 1) { // bad return param; } } const cpWithOtherEnum = (param: A) => { if (param === B.two as unknown as A) { // ALERT bad return param; } } const cpWithSelf = (param: A) => { if (param === A.two) { // good return param; }}}Copy the code

Invalid type guard

const getName = <T extends Dog | Cat>(animal: T) => { if ('wang' in animal) { return animal.wang; // ts(2339) } return animal.miao; // ts(2339) }; / / const getName = < T extends Dog | Cat > (animal: T) = > {the if (isDog (animal)) {/ / instanceOf can return animal. Wang; // ok } return (animal as Cat).miao; // ts(2339) };Copy the code

2. Type compatibility

Special case

any

The any type can be assigned to any type other than never, and vice versa. That is, any is compatible with all types except never, and is compatible with all types

never

The property of never is that it can be assigned to any other type, but in turn cannot be assigned to any other type, including any

unknown

The property of unknown is almost the reverse of the property of never, that is, we cannot assign unknown to any type other than any, and in turn any type can be assigned to unknown

Void, null, undefined

Void, null, and undefined are also particularly compatible. For example, void can only be assigned to any and unknown, whereas only any, never, and undefined can be assigned to void

enum

Numeric enumerations and numeric types are compatible. Different enumerations are incompatible

Type compatibility

subtypes

From the perspective of subtypes, all subtypes are compatible with their parent types

{

  const one = 1;

  let num: number = one; // ok

  interface IPar {

    name: string;

  }

  interface IChild extends IPar {

    id: number;

  }

  let Par: IPar;

  let Child: IChild;

  Par = Child; // ok

  class CPar {

    cname = '';

  }

  class CChild extends CPar {

    cid = 1;

  }

  let ParInst: CPar;

  let ChildInst: CChild;

  ParInst = ChildInst; // ok

  let mixedNum: 1 | 2 | 3 = one; // ok

}

Copy the code
  let ICPar: IPar | CPar;
  let ICChild: IChild | CChild;
  ICPar = ICChild; // ok
Copy the code

Structure type

Another criterion for type compatibility is structure type, which means that two types are compatible if they have the same structure. For example, interface types or classes that have attributes or methods of the same type can be assigned to each other.

{
  class C1 {
    name = '1';
  }
  class C2 {
    name = '2';
  }
  interface I1 {
    name: string;
  }
  interface I2 {
    name: string;
  }
  let InstC1: C1;
  let InstC2: C2;
  let O1: I1;
  let O2: I2;
  InstC1 = InstC2; // ok
  O1 = O2; // ok
  InstC1 = O1; // ok
  O2 = InstC2; // ok
}
Copy the code

Two interface types or classes are compatible if one of them not only has all of the attributes and methods of the other type, but also contains other attributes and methods (like subclasses derived from the other type).

{
  interface I1 {
    name: string;
  }
  interface I2 {
    id: number;
    name: string;
  }
  class C2 {
    id = 1;
    name = '1';
  }
  let O1: I1;
  let O2: I2;
  let InstC2: C2;
  O1 = O2;
  O1 = InstC2;
}
Copy the code

Although the variable O2 containing the redundant attribute ID can be assigned to the variable O1, if we directly assign an object literal of the exact structure of variable O2 to the variable O1, we will get a TS (2322) type incompatible error (line 2 in the following example), This is the freshness feature of object literals.

O1 = {
    id: 2, // ts(2322)
    name: 'name'
  };
  let O3 = {
    id: 2,
    name: 'name'
  };
  O1 = O3; // ok
  O1 = {
    id: 2,
    name: 'name'
  } as I2; // ok
Copy the code

Class compatibility features: In fact, when determining whether two classes are compatible, we can completely ignore the compatibility of their constructors and static properties and methods, and simply compare the compatibility of the properties and methods of the class instance. If two classes contain private, protected properties and methods, they are compatible only if the properties and methods originate from the same class.

{
  class C1 {
    name = '1';
    private id = 1;
    protected age = 30;
  }
  class C2 {
    name = '2';
    private id = 1;
    protected age = 30;
  }
  let InstC1: C1;
  let InstC2: C2;
  InstC1 = InstC2; // ts(2322)
  InstC2 = InstC1; // ts(2322)
}
{
  class CPar {
    private id = 1;
    protected age = 30;
  }
  class C1 extends CPar {
    constructor(inital: string) {
      super();
    }
    name = '1';
    static gender = 'man';
  }
  class C2 extends CPar {
    constructor(inital: number) {
      super();
    }
    name = '2';
    static gender = 'woman';
  }
  let InstC1: C1;
  let InstC2: C2;
  InstC1 = InstC2; // ok
  InstC2 = InstC1; // ok
}
Copy the code

Inheritable and implementable

Type compatibility also determines whether interface types and classes can inherit from another interface type or class through extends, and whether classes can implement interfaces through implements.

{ interface I1 { name: number; } interface I2 extends I1 { // ts(2430) name: string; } class C1 { name = '1'; private id = 1; } class C2 extends C1 { // ts(2415) name = '2'; private id = 1; } class C3 implements I1 { name = ''; // ts(2416)}} // Interface type I1 and interface type I2 contain different types of name attribute incompatible, so interface type I2 cannot inherit interface type I1. // Classes C1 and C2 do not meet the class compatibility conditions, so class C2 cannot inherit from class C1. Interface type I1 and class C3 contain different types of name attributes, so class C3 cannot implement interface type I1.Copy the code

The generic

The compatibility of generic types, generic classes, really refers to the compatibility of instantiating them into an exact type.

variant

Infer subtype relationships between more complex types based on them based on the subtype relationships between types.

Use the expression of a function in mathematics. For example, the Dog type, we can use F(Dog) to represent a constructed complex type; F(Animal) represents a complex type based on Animal construction.

  • (1) Covariation
{ type isChild<Child, Par> = Child extends Par ? true : false; interface Animal { name: string; } interface Dog extends Animal { woof: () => void; } type Covariance<T> = T; type isCovariant = isChild<Covariance<Dog>, Covariance<Animal>>; // Covariant<Dog> is a subtype of Covariant<Animal>, so isCovariant is true.Copy the code
  • (2) Invert

Contravariant means that if Dog is a child of Animal, then F(Dog) is the parent of F(Animal), which is the opposite of covariant.

In the TypeScript rigor we prefer, function parameter types are contravariant,

type Contravariance<T> = (param: T) => void;
type isNotContravariance = isChild<Contravariance<Dog>, Contravariance<Animal>>; // false;
type isContravariance = isChild<Contravariance<Animal>, Contravariance<Dog>>; // true;
Copy the code
  • (3) Bidirectional covariance

Bidirectional covariant means that if Dog is a subtype of Animal, then F(Dog) is a subtype and parent type of F(Animal), both covariant and contravariant.

In real life, in TypeScript non-strict mode, function parameter types are bidirectional covariant. As mentioned earlier, functions are only safe if their parameters are contravariant, and this course has been emphasizing the use of strict modes, so bidirectional covariance is not a safe or useful feature, so we are unlikely to encounter such a practical scenario.

  • (4) unchanged

Immutable means that as long as they are not exactly the same type, they must be incompatible. So even if Dog is a subtype of Animal, if F Dog is not a subtype of F Animal, then F Animal is not a subtype of F Dog.

interface Cat extends Animal { miao: () => void; } const cat: Cat = { name: 'Cat', miao: () => void 0, }; const dog: Dog = { name: 'Dog', woof: () => void 0, }; let dogs: Dog[] = [dog]; animals = dogs; // ok animals.push(cat); // ok dogs.forEach(visitDog); // Type OK, but an error is thrown at runtimeCopy the code

For mutable arrays, immutable seems like a safer and more reasonable setup. However, mutable and immutable arrays are covariant in TypeScript, which is a pitfall to be aware of.

Function parameter types are contravariant and return value types are covariant

Function type compatibility

(1) Return value

We have already said that return value types are covariant, so the subtype relationship of a function is the same as the subtype relationship of the return value, provided that the parameter types are compatible. That is, if the return value type is compatible, the function is compatible.

(2) Parameter type

We also mentioned that parameter types are contravariant, so in the case of the same number of parameters and compatible return value types, the relationship between function subtype and parameter subtype is reversed (contravariant).

(3) Number of parameters

Function compatibility depends on the number of parameters in the same index position and the compatibility of return value types

{
  let lessParams = (one: number) => void 0;
  let moreParams = (one: number, two: string) => void 0;
  lessParams = moreParams; // ts(2322)
  moreParams = lessParams; // ok
}
Copy the code

(4) Optional and residual parameters

Optional parameters are compatible with other parameters and non-optional parameters.

let optionalParams = (one? : number, tow? : number) => void 0; let requiredParams = (one: number, tow: number) => void 0; let restParams = (... args: number[]) => void 0; requiredParams = optionalParams; // ok restParams = optionalParams; // ok optionalParams = restParams; // ts(2322) optionalParams = requiredParams; // ts(2322) restParams = requiredParams; // ok requiredParams = restParams; // okCopy the code

3. Enhance the type system

How do you safely use JavaScript libraries in TypeScript? The key step is to use a DECLARE keyword in TypeScript.

Using the DECLARE keyword, we can declare global variables, methods, classes, and objects.

Declare a variable

At run time, front-end code

Declare variables of syntax: declare (var | let | const) variable name: variable types

declare var val1: string;
declare let val2: number;
declare const val3: boolean;
val1 = '1';
val1 = '2';
val2 = 1;
val2 = '2'; // TS2322: Type 'string' is not assignable to type 'number'.
val3 = true; // TS2588: Cannot assign to 'val3' because it is a constant.
Copy the code

Declare functions

The syntax for declaring functions is the same as that for declaring variable types, except that the declare keyword is followed by the function keyword

declare function toString(x: number): string;
const x = toString(1); // => string
Copy the code

When using the DECLARE keyword, we do not need to write concrete implementations of declared variables, functions, and classes (because variables, functions, and classes are already implemented in other libraries), but simply declare their types

// TS1183: An implementation cannot be declared in ambient contexts. declare function toString(x: number) { return String(x); }; // TypeScript error messages indicate that context declarations do not need to be implemented. That is, all types declared by declare only need to indicate the type, not the implementation.Copy the code

Class declarations

To declare a class, we only need to declare the type of the class’s properties and methods.

declare class Person {
  public name: string;
  private age: number;
  constructor(name: string);
  getAge(): number;
}
const person = new Person('Mike');
person.name; // => string
person.age; // TS2341: Property 'age' is private and only accessible within class 'Person'.
person.getAge(); // => number
Copy the code

The statement enumeration

To declare an enumeration, you define only the type of the enumeration, not the value of the enumeration

declare enum Direction {
  Up,
  Down,
  Left,
  Right,
}
const directions = [Direction.Up, Direction.Down, Direction.Left, Direction.Right];
Copy the code

Note: Declaration enumerations are used only as a compile-time check, after compilation, the contents of the declaration file are removed from the compilation results

Declare the module

Declare module syntax: Declare Module ‘module name’ {}.

// lodash.d.ts
declare module 'lodash' {
  export function first<T extends unknown>(array: T[]): T;
}
// index.ts
import { first } from 'lodash';
first([1, 2, 3]); // => number;
Copy the code

The declaration file is a file with the suffix.d.ts. Inside the module declaration, we only need to use export to export the corresponding library classes and functions.

Declare file

declare module '*.jpg' {
  const src: string;
  export default src;
}
declare module '*.png' {
  const src: string;
  export default src;
}
Copy the code

declare namespace

Unlike declarative modules, namespaces are used to represent global object variables with many child properties or methods

declare namespace $ { const version: number; function ajax(settings? : any): void; } $.version; // => number $.ajax(); // Declare the globally imported jQuery variable $, so you can directly use the version attribute of the $variable and the Ajax methodCopy the code

Declaration file

Files with the suffix.d.ts are declaration files. When declaring the file, we only need to define the API interface exposed by the tripartite library

type

  • Type alias declaration;

  • Interface declaration;

  • Class declaration;

  • Enumeration declaration;

  • Type declaration for the import.

Each of the above declarations creates a type name.

value

A value is a value that an expression can assign at run time.

You can create a value in the following six ways:

  • Var, let, const declarations;

  • Declarations of namespace and Module containing values;

  • Enumeration declaration;

  • Class declaration;

  • Imported values;

  • Function declaration.

The namespace

We can also declare types in namespaces. For example, the const x: A.B.C declaration, where type C is in the A.B namespace.

Use declaration files

After installing TypeScript dependencies, we typically install a lib.d.ts declaration file that contains declarations of various global variables in the JavaScript runtime and DOM

// typescript/lib/lib.d.ts
/// <reference no-default-lib="true"/>
/// <reference lib="es5" />
/// <reference lib="dom" />
/// <reference lib="webworker.importscripts" />
/// <reference lib="scripthost" />
Copy the code

Using the @ types

Definitely Typed is the most popular high-quality TypeScript declaration file library

Search here for the type declaration of the library you want to import

Problem: Because it is maintained by the community, if the original partite library is upgraded, the type definitions of the exported partite library need to go through PR and publication processes before being upgraded. As a result, it cannot be completely synchronized with the original library. In TypeScript, we can solve this problem temporarily by combining types and extending type definitions.

Type of merger

In TypeScript, identical interfaces and namespaces are merged according to certain rules.

Combined interface

The simplest and most common declaration merge is interface merge

interface Person { name: string; } interface Person { age: number; } // equivalent to interface Person {name: string; age: number; } // It is important to note that non-function members of the interface should be type compatibleCopy the code

For function members, each function declaration with the same name is treated as an overload of that function. The interface declared later has higher priority

interface Obj { identity(val: any): any; } interface Obj { identity(val: number): number; } interface Obj { identity(val: boolean): boolean; } // equivalent to interface Obj {identity(val: Boolean): Boolean; identity(val: number): number; identity(val: any): any; } const obj: Obj = { identity(val: any) { return val; }}; const t1 = obj.identity(1); // => number const t2 = obj.identity(true); // => boolean const t3 = obj.identity("t3"); // => anyCopy the code

Merge the namespace

Merging namespaces is similar to merging interfaces in that namespace merging also merges the attributes of its exported members. The difference is that non-exported members are visible only within the original namespace.

namespace Person {
  const age = 18;
  export function getAge() {
    return age;
  }
}
namespace Person {
  export function getMyAge() {
    return age; // TS2304: Cannot find name 'age'.
  }
}
Copy the code

Do not merge

For classes, the special object that is both a value and a type cannot be merged.

Expansion module

// person.ts export class Person {} // index.ts import { Person } from './person'; declare module './person' { interface Person { greet: () => void; } } Person.prototype.greet = () => { console.log('Hi! '); }; // Declare the Person attribute in the imported module Person. TypeScript merges the type of the original module. This way we can extend the type of the imported module. We also added the greet method on the prototype chain to the imported Person class.Copy the code
// person.ts export class Person {} // index.ts import { Person } from './person'; declare module './person' { interface Person { greet: () => void; } } - declare module './person' { - interface Person { - greet: () => void; - } - } + // TS2339: Property 'greet' does not exist on type 'Person'. Person.prototype.greet = () => { console.log('Hi! '); }; [Error] ts(2339) does not exist greet attribute. [error] Ts (2339) does not exist greet attribute. // For imported tripartite modules, we can also use this method to extend the properties of the original module.Copy the code

Expansion of the global

Global modules refer to modules that can be used without import, such as global window, document, etc.

Extending global objects is the same as extending modules

declare global { interface Array<T extends unknown> { getLen(): number; } } Array.prototype.getLen = function () { return this.length; }; // Global Array objects are declared to have a getLen method, so TypeScript does not error when implementing getLen methods for Array objects.Copy the code

4. Master the official tool types

Operation Interface Type

Partial

Partial utility types can make all attributes of a type optional and the utility type returns all subsets of the given type

type Partial<T> = { [P in keyof T]? : T[P]; }; interface Person { name: string; age? : number; weight? : number; } type PartialPerson = Partial<Person>; // Equivalent to interface PartialPerson {name? : string; age? : number; weight? : number; }Copy the code

Required

In contrast to the Partial utility type, the Required utility type can make all attributes of a given type mandatory

type Required<T> = { [P in keyof T]-? : T[P]; }; type RequiredPerson = Required<Person>; // equivalent to interface RequiredPerson {name: string; age: number; weight: number; }Copy the code

The mapping type uses a – symbol after the key value, – and? Combined to remove optional attributes of the type, so that all attributes of a given type become required.

Readonly

Readonly Utility types can make all attributes of a given type read-only, which means that attributes of a given type cannot be reassigned

type Readonly<T> = { readonly [P in keyof T]: T[P]; }; type ReadonlyPerson = Readonly<Person>; Interface ReadonlyPerson {readonly name: string; readonly age? : number; readonly weight? : number; }Copy the code

After Readonly, the name, age, and weight attributes of ReadonlyPerson become Readonly read-only.

Pick

The Pick utility type can Pick the specified key value from a given type and compose a new type

type Pick<T, K extends keyof T> = { [P in K]: T[P]; }; type NewPerson = Pick<Person, 'name' | 'age'>; Interface NewPerson {name: string; age? : number; }Copy the code

Omit

In contrast to the Pick type, the Omit tool type functions to return a new type after the specified key value has been removed

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; type NewPerson = Omit<Person, 'weight'>; Interface NewPerson {name: string; age? : number; }Copy the code

All tool types that operate on interface types use mapping types. By mapping types, we can remap the attributes of the original type to form the desired type.

The joint type

Exclude

In introducing the Omit type implementation, we use the Exclude type. By using the Exclude type, we remove the specified attribute from all attributes of the interface. Therefore, the effect of Exclude is to remove the specified type from the union type.

type Exclude<T, U> = T extends U ? never : T; type T = Exclude<'a' | 'b' | 'c', 'a'>; // => 'b' | 'c' type NewPerson = Omit<Person, 'weight'>; NewPerson = Pick<Person, Exclude<keyof Person, 'weight'>>; // ExcludeKeys = Exclude<keyof Person, 'weight'>; // => 'name' | 'age'Copy the code

The implementation of Exclude uses a conditional type. T is not returned if type T can be assigned to type U, otherwise it is returned, and we remove the specified type from the union type.

Extract

The Extract type acts as the opposite of Exclude in that Extract is primarily used to Extract the specified type from the union type, similar to the Pick type in the operation interface type.

type Extract<T, U> = T extends U ? T : never;
type T = Extract<'a' | 'b' | 'c', 'a'>; // => 'a'
Copy the code

The Extract type is equivalent to fetching the intersection of two union types.

You can implement a tool type that gets the intersection of interface types based on Extract

type Intersect<T, U> = { [K in Extract<keyof T, keyof U>]: T[K]; }; interface Person { name: string; age? : number; weight? : number; } interface NewPerson { name: string; age? : number; } type T = Intersect<Person, NewPerson>; Type T = {name: string; age? : number; };Copy the code

NonNullable

NonNullable removes null or undefined from the union type

type NonNullable<T> = T extends null | undefined ? never : T; / / equivalent to use Exclude type NonNullable < T > = Exclude < T, null | undefined >; type T = NonNullable<string | number | undefined | null>; // => string | numberCopy the code

Record

The purpose of Record is to generate the interface type, and then we use the generic parameters passed in as the properties and values of the interface type, respectively.

type Record<K extends keyof any, T> = { [P in K]: T; }; type MenuKey = 'home' | 'about' | 'more'; interface Menu { label: string; hidden? : boolean; } const menus: Record<MenuKey, Menu> = {about: {label: 'about'}, home: {label: 'home'}, more: {label: 'more ', hidden: true }, };Copy the code

The implementation here qualifies that the first generic parameter inherits from keyof any.

In TypeScript, keyof any refers to properties that can be used as object keys

type T = keyof any; // => string | number | symbol
Copy the code

Function types

ConstructorParameters

ConstructorParameters can be used to obtain the construction parameters of constructors, whereas an implementation of the ConstructorParameters type requires the infer keyword to infer the type of the construction parameters.

The infer keyword can be thought of as simple pattern matching. Infer returns the matched type if the actual parameter type matches infer.

type ConstructorParameters<T extends new (... args: any) => any> = T extends new ( ... args: infer P ) => any ? P : never; class Person { constructor(name: string, age? : number) {} } type T = ConstructorParameters<typeof Person>; // [name: string, age?: number]Copy the code

Parameters

Parameters function like ConstructorParameters in that they can be used to take the Parameters of a function and return an ordered pair

type Parameters<T extends (... args: any) => any> = T extends (... args: infer P) => any ? P : never; type T0 = Parameters<() => void>; // [] type T1 = Parameters<(x: number, y? : string) => void>; // [x: number, y?: string]Copy the code

ReturnType

ReturnType is used to get the ReturnType of the function

type ReturnType<T extends (... args: any) => any> = T extends (... args: any) => infer R ? R : any; type T0 = ReturnType<() => void>; // => void type T1 = ReturnType<() => string>; // => stringCopy the code

ThisParameterType

ThisParameterType can be used to get the this parameter type of the function.

type ThisParameterType<T> = T extends (this: infer U, ... args: any[]) => any ? U : unknown; type T = ThisParameterType<(this: Number, x: number) => void>; // NumberCopy the code

ThisType

ThisType allows you to specify the type of this in an object literal. ThisType does not return the converted type, but rather specifies the type of this via ThisType’s generic argument

Note: If you want to use this utility type, you need to enable the TypeScript configuration for noImplicitThis.

type ObjectDescriptor<D, M> = { data? : D; methods? : M & ThisType<D & M>; // Methods this is of type D & M}; function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M { let data: object = desc.data || {}; let methods: object = desc.methods || {}; return { ... data, ... methods } as D & M; } const obj = makeObject({ data: { x: 0, y: 0 }, methods: { moveBy(dx: number, dy: number) { this.x += dx; // this => D & M this.y += dy; // this => D & M }, }, }); obj.x = 10; obj.y = 20; obj.moveBy(5, 5);Copy the code

OmitThisParameter

The OmitThisParameter utility type is primarily used to remove this from function types. If the function type passed in does not explicitly declare this, the original function type is returned.

type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (... args: infer A) => infer R ? (... args: A) => R : T; type T = OmitThisParameter<(this: Number, x: number) => string>; // (x: number) => stringCopy the code

String type

Template string

TypeScript supports template string literals as of version 4.1. To this end, TypeScript also provides four built-in action string types: Uppercase, Lowercase, Capitalize, and Uncapitalize

Type Uppercase<S extends String > = intrinsic; Lowercase<S extends String > = intrinsic; // Convert the first letter of a string literal to Capitalize type Capitalize<S extends String > = intrinsic; // Convert the first letter of a string literal to lowercase. Type Uncapitalize<S extends String > = intrinsic; type T0 = Uppercase<'Hello'>; // => 'HELLO' type T1 = Lowercase<T0>; // => 'hello' type T2 = Capitalize<T1>; // => 'Hello' type T3 = Uncapitalize<T2>; // => 'hello'Copy the code

5. Type programming: Create your own tool type

Essentially a custom complex type constructor (generics, to be exact)

The generic

type isSubTying<Child, Par> = Child extends Par ? true : false;
type isXX2 = isSubTyping<1, number>; // true
type isYY2 = isSubTyping<'string', string>; // true
type isZZ2 = isSubTyping<true, boolean>; // true
Copy the code

Conditions in the

The type of condition for a ternary operation

TypeScript supports conditional types that use ternary operations, and ternary operations also support nesting

  type isSubTyping<Child, Par> = Child extends Par ? true : false;
  type isAssertable<T, S> = T extends S ? true :  S extends T ? true : false;
  type isNumAssertable = isAssertable<1, number>; // true
  type isStrAssertable = isAssertable<string, 'string'>; // true
  type isNotAssertable = isAssertable<1, boolean>; // false
Copy the code

Allocation condition type

If the input parameter is a union type, it is broken down into separate (atomic) types (members) and then type operations are performed.

type BooleanOrString = string | boolean; type StringOrNumberArray<E> = E extends string | number ? E[] : E; type WhatIsThis = StringOrNumberArray<BooleanOrString>; // boolean | string[] type BooleanOrStringGot = BooleanOrString extends string | number ? BooleanOrString[] : BooleanOrString; / / string | Boolean / / BooleanOrString being treated as a whole, so BooleanOrStringGot is of type string | Boolean.Copy the code

It is also possible to unassign a type by forcing it to be treated as a whole

type StringOrNumberArray<E> = [E] extends [string | number] ? E[] : E; type WhatIsThis = StringOrNumberArray<string | boolean>; / / string | using Boolean / / [] will into E wrapped up, even if the parameter is joint type string | Boolean, will also be treated as a whole, so returns the string | Boolean.Copy the code

Note: There is a “pitfall” when generics that contain conditional types accept never as their input parameter

 type GetSNums = never extends number ? number[] : never extends string ? string[] : never; // number[];
  type GetNever = StringOrNumberArray<never>; // never
Copy the code

Never is an underlying type that cannot be assigned, and if it occurs atomically as an input parameter to the left of the extends keyword, then the instantiated type is also Never.

  type UsefulNeverX<T> = T extends {} ? T[] : [];
  type UselessNeverX<T, S> = S extends {} ? S[] : [];
  type UselessNeverY<T, S> = S extends {} ? T[] : [];
  type UselessNeverZ<T> = [T] extends [{}] ? T[] : [];
  type ThisIsNeverX = UsefulNeverX<never>; // never
  type ThisIsNotNeverX = UselessNeverX<never, string>; // string[]
  type ThisIsNotNeverY = UselessNeverY<never, string>; // never[]
  type ThisIsNotNeverZ = UselessNeverZ<never>; // never[]
Copy the code

Type infer from conditional types

Use the type inference operator from a conditional type to infer the components of a type input parameter, such as the type of the element in an array type input parameter.

{
  type ElementTypeOfArray<T> = T extends (infer E)[] ? E : never;
  type isNumber = ElementTypeOfArray<number[]>; // number
  type isNever = ElementTypeOfArray<number>; // never
}
Copy the code

Infer can also be used to derive arbitrary member types by creating arbitrary type inference parameters

{
  type ElementTypeOfObj<T> = T extends { name: infer E; id: infer I } ? [E, I] : never;
  type isArray = ElementTypeOfObj<{ name: 'name'; id: 1; age: 30 }>; // ['name', 1]
  type isNever = ElementTypeOfObj<number>; // never
}
Copy the code

Index access type

The index access type is actually more like a way of retrieving materials. First, we can extract the type of any member of the object (interface type) on demand by attribute name, index, index signature (note: only [index name] syntax is used).

 interface MixedObject {
    animal: {
      type: 'animal' | 'dog' | 'cat';
      age: number;
    };
    [name: number]: {
      type: string;
      age: number;
      nickname: string;
    };
    [name: string]: {
      type: string;
      age: number;
    };
 }
type animal = MixedObject['animal'];
type animalType = MixedObject['animal']['type'];
type numberIndex = MixedObject[number];
type numberIndex0 = MixedObject[0];
type stringIndex = MixedObject[string];
type stringIndex0 = MixedObject['string'];
Copy the code

keyof

You can use the keyof keyword to extract the object attribute name, index name, and type of index signature

  type MixedObjectKeys = keyof MixedObject; // string | number
  type animalKeys = keyof animal; // 'type' | 'age'
  type numberIndexKeys = keyof numberIndex; // "type" | "age" | "nickname"
Copy the code

typeof

If we use Typeof in an expression context, it is the type used to get expression values, and if we use typeof in a type context, it is the type used to get variables or attributes. Of course, in TypeScript, typeof’s primary purpose is to get the typeof a variable or property in a type context

{ let StrA = 'a'; const unions = typeof StrA; / / unions type is "string" | "number" | "bigint" | "Boolean" | "symbol" | "undefined" | "object" | "function" const STR: typeof StrA = 'string'; // The STRS type is string type DerivedFromStrA = typeof StrA; // string }Copy the code

Mapping type

You can use index signature syntax and the IN keyword to limit the scope of object attributes

  type SpecifiedKeys = 'id' | 'name';
  type TargetType = {
    [key in SpecifiedKeys]: any;
  }; // { id: any; name: any; }
  type TargetGeneric<O extends string | number | symbol> = {
    [key in O]: any;
  }
  type TargetInstance = TargetGeneric<SpecifiedKeys>; // { id: any; name: any; }
Copy the code

Note: We can only use in in type alias definitions. If we use in interfaces, we will be prompted with a TS (1169) error

interface ITargetInterface {
    [key in SpecifiedKeys]: any; // ts(1169)
}
Copy the code

When defining a type, we can use a combination of in and keyof, and create a new type based on an existing type. Read-only, optional features that make the new type conform to the existing type are called mapped types.

Note: In and keyof can also only be used together in type alias definitions.

interface SourceInterface { readonly id: number; name? : string; } type TargetType = { [key in keyof SourceInterface]: SourceInterface[key]; }; // { readonly id: number; name? : string | undefined } type TargetGenericType<S> = { [key in keyof S]: S[key]; }; type TargetInstance = TargetGenericType<SourceInterface>; // { readonly id: number; name? : string | undefined }Copy the code

You can use readonly,? Modifiers are used to describe readability and optionability of attributes. You can also add or remove specified modifiers by prefixing them with + and – (default: + and Add).

type TargetGenericTypeReadonly<S> = { readonly [key in keyof S]: S[key]; } type TargetGenericTypeReadonlyInstance = TargetGenericTypeReadonly<SourceInterface>; // { readonly id: number; readonly name? : string | undefined } type TargetGenericTypeOptional<S> = { [key in keyof S]? : S[key]; } type TargetGenericTypeOptionalInstance = TargetGenericTypeOptional<SourceInterface>; // { readonly id? : number; readonly name? : string | undefined } type TargetGenericTypeRemoveReadonly<S> = { -readonly [key in keyof S]: S[key]; } type TargetGenericTypeRemoveReadonlyInstance = TargetGenericTypeRemoveReadonly<SourceInterface>; // { id: number; name? : string | undefined } type TargetGenericTypeRemoveOptional<S> = { [key in keyof S]-? : S[key]; } type TargetGenericTypeRemoveOptionalInstance = TargetGenericTypeRemoveOptional<SourceInterface>; // { readonly id: number; name: string }Copy the code

Remap keys using AS

Since TypeScript 4.1, we can use type assertions in index signatures for mapped types

type TargetGenericTypeAssertiony<S> = { [key in keyof S as Exclude<key, 'id'>]: S[key]; } type TargetGenericTypeAssertionyInstance = TargetGenericTypeAssertiony<SourceInterface>; // { name? : string | undefined; }Copy the code

To build the wheels

Exclude

  type ExcludeSpecifiedNumber = Exclude<1 | 2, 1>; // 2
  type ExcludeSpecifiedString = Exclude<'id' | 'name', 'id'>; // 'name
  type ExcludeSpecifiedBoolean = Exclude<boolean, true>; // false
Copy the code

The code implementation is as follows:

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

ReturnTypeOfResolved

The difference between ReturnTypeOfResolved and official ReturnType: if the ReturnType of input parameter F is an instance of a generic Promise, return the input that the Promise received.

// type ReturnType<T extends (... args: any) => any> = T extends (... args: any) => infer R ? R : any; type ReturnTypeOfResolved<F extends (... args: any) => any> = F extends (... args: any[]) => Promise<infer R> ? R : ReturnType<F>; type isNumber = ReturnTypeOfResolved<() => number>; // number type isString = ReturnTypeOfResolved<() => Promise<string>>; // stringCopy the code

Merge

A generic Merge<A, B> that merges type inputs A and B into one type based on the mapping type

 type Merge<A, B> = {
    [key in keyof A | keyof B]: key extends keyof A
      ? key extends keyof B
        ? A[key] | B[key]
        : A[key]
      : key extends keyof B
      ? B[key]
      : never;
  };
  type Merged = Merge<{ id: number; name: string }, { id: string; age: number }>;
Copy the code

Equal

Custom utility type Equal<S, T>, which can be used to determine whether input arguments S and T are of the same type. Boolean literal type true if the same, false otherwise.

Other series of articles

A how-to guide to TypeScript