TypeScript advanced types and usage

This article introduces the usage scenarios of advanced TypeScript types in detail, which can provide some help for the daily use of TypeScript.

preface

This article has been included in Github: https://github.com/beichensky/Blog In the middle, pass by and point a Star

1, Advanced type

& cross type

Cross type is to combine multiple types into one type. This allows us to stack existing types together into one type, which contains all the required types of features.

  • Syntax: T & U

    Its return type should conform to both T type and U type

  • Usage: suppose there are two interfaces: one is Ant ant interface and the other is Fly interface. Now there is an Ant that can Fly:

interface Ant {
    name: string;
    weight: number;
}

interface Fly {
    flyHeight: number;
    speed: number;
}

// If any attribute is missing, an error will be reported
const flyAnt: Ant & Fly = {
    name: 'Ants hey',
    weight: 0.2,
    flyHeight: 20,
    speed: 1,
};

Union type (|)

Union types are very related to cross types, but they are completely different in use.

  • Syntax: T | U

    Its return type is any one of the multiple types of the connection

  • Usage: suppose you declare a data, which can be either string or number

let stringOrNumber:  string | number = 0

stringOrNumber = ''

Take another look at the following example. The parameter type of the start function is Bird | Fish. If you want to call the start function directly, you can only call the methods of Bird and Fish, otherwise the compilation will report an error

class Bird {
    fly() {
        console.log('Bird flying');
    }
    layEggs() {
        console.log('Bird layEggs');
    }
}

class Fish {
    swim() {
        console.log('Fish swimming');
    }
    layEggs() {
        console.log('Fish layEggs');
    }
}

const bird = new Bird();
const fish = new Fish();

function start(pet: Bird | Fish) {
    // It's OK to call layEggs, because both Bird and Fish have layEggs methods
    pet.layEggs();

    // Property 'fly' does not exist on type 'Bird | Fish'
    // pet.fly();

    // An error will be reported: Property 'swim' does not exist on type 'Bird | Fish'
    // pet.swim();
}

start(bird);

start(fish);

2, Keywords

Type constraints (extensions)

Syntax: T extends K

The extensions here are not the inheritance of classes and interfaces, but the judgment and constraints on types, which means to judge whether T can be assigned to K

You can constrain incoming types in generics

const copy = (value: string | number): string | number => value

// Only string or number can be passed in
copy(10)

// An error will be reported: Argument of type 'boolean' is not assignable to parameter of type 'string | number'
// copy(false)

You can also judge whether t can be assigned to U. if possible, return T; otherwise, return never

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

Type mapping (in)

It will traverse the key of the specified interface or the union type

interface Person {
    name: string
    age: number
    gender: number
}

// Convert all properties of T to read-only type
type ReadOnlyType<T> = {
    readonly [P in keyof T]: T
}

// type ReadOnlyPerson = {
//     readonly name: Person;
//     readonly age: Person;
//     readonly gender: Person;
// }
type ReadOnlyPerson = ReadOnlyType<Person>

Type predicate (is)

  • Syntax: parameterName is Type

    parameterName must be a parameter name from the current function signature. Judge whether parameterName is Type.

Specific application scenarios can follow the following code ideas:

After reading the union type example, you may consider: if you want to call Bird's fly method and Fish's swim method in the start function, what should you do?

The first thought may be to directly check whether the member exists, and then call:

function start(pet: Bird | Fish) {
    // It's OK to call layEggs, because both Bird and Fish have layEggs methods
    pet.layEggs();

    if ((pet as Bird).fly) {
        (pet as Bird).fly();
    } else if ((pet as Fish).swim) {
        (pet as Fish).swim();
    }
}

However, it is troublesome to perform type conversion during judgment and call. You may want to write a tool function to judge:

function isBird(bird: Bird | Fish): boolean {
    return !!(bird as Bird).fly;
}

function isFish(fish: Bird | Fish): boolean {
    return !!(fish as Fish).swim;
}

function start(pet: Bird | Fish) {
    // It's OK to call layEggs, because both Bird and Fish have layEggs methods
    pet.layEggs();

    if (isBird(pet)) {
        (pet as Bird).fly();
    } else if (isFish(pet)) {
        (pet as Fish).swim();
    }
}

It seems a little concise, but when calling a method, we still need to carry out type conversion, otherwise we will still report an error. What good way is there to enable us to call the method directly after judging the type without type conversion?

OK, there must be. The type predicate is comes in handy

  • Usage:
function isBird(bird: Bird | Fish): bird is Bird {
    return !!(bird as Bird).fly
}

function start(pet: Bird | Fish) {
    // It's OK to call layEggs, because both Bird and Fish have layEggs methods
    pet.layEggs();

    if (isBird(pet)) {
        pet.fly();
    } else {
        pet.swim();
    }
};

Whenever isFish is called with some variables, TypeScript will reduce the variable to that specific type, as long as the type is compatible with the original type of the variable.

TypeScript not only knows that pet is a Fish type in the if branch; It is also clear that in the else branch, it must not be Fish type, but Bird type

Type to be inferred (infer)

Infor P can be used to mark a generic type, indicating that the generic type is a type to be inferred and can be used directly

For example, the following example of obtaining function parameter types:

type ParamType<T> = T extends (param: infer P) => any ? P : T;

type FunctionType = (value: number) => boolean

type Param = ParamType<FunctionType>;   // type Param = number

type OtherParam = ParamType<symbol>;   // type Param = symbol

Judge whether T can be assigned to (param: infer P) = > any, and infer the parameter as generic P. if it can be assigned, return the parameter type P, otherwise return the incoming type

Another example of obtaining the return type of a function:

type ReturnValueType<T> = T extends (param: any) => infer U ? U : T;

type FunctionType = (value: number) => boolean

type Return = ReturnValueType<FunctionType>;   // type Return = boolean

type OtherReturn = ReturnValueType<number>;   // type OtherReturn = number

Judge whether T can be assigned to (param: any) = > infer U, and infer the return value type as generic U. if it can be assigned, return the return value type P, otherwise return the incoming type

Original type protection (typeof)

  • Syntax: typeof v === "typename" or typeof V== "typename"

It is used to judge whether the data type is an original type (number, string, boolean, symbol) and protect the type

'typename 'must be' number ',' string ',' boolean ', or' symbol '. But TypeScript does not prevent you from comparing with other strings, and the language does not recognize those expressions as type protected.

Look at the following example. The print function will print different results according to the parameter type. How to judge whether the parameter is string or number?

function print(value: number | string) {
    // If it is a string type
    // console.log(value.split('').join(', '))

    // If it is of type number
    // console.log(value.toFixed(2))
}

There are two common judgment methods:

  1. The string type is determined according to whether the split attribute is included, and the number type is determined according to whether the toFixed method is included

    Disadvantages: type conversion is required for both judgment and call

  2. Use type predicate is

    Disadvantages: it's too troublesome to write a tool function every time

  • Usage: it's time for typeof to show its skill
function print(value: number | string) {
    if (typeof value === 'string') {
        console.log(value.split('').join(', '))
    } else {
        console.log(value.toFixed(2))
    }
}

After using typeof for type determination, TypeScript will reduce the variable to the specific type, as long as the type is compatible with the original type of the variable.

Type protection (instanceof)

It is similar to typeof, but works differently. instanceof type protection is a way to refine types through constructors.

The right side of instanceof requires a constructor, and TypeScript will be refined as follows:

  • The type of the prototype attribute of this constructor, if its type is not any
  • Construct the union of the types returned by the signature

Let's also demonstrate with the code in the type predicate is example:

Initial code:

function start(pet: Bird | Fish) {
    // It's OK to call layEggs, because both Bird and Fish have layEggs methods
    pet.layEggs();

    if ((pet as Bird).fly) {
        (pet as Bird).fly();
    } else if ((pet as Fish).swim) {
        (pet as Fish).swim();
    }
}

Code after using instanceof:

function start(pet: Bird | Fish) {
    // It's OK to call layEggs, because both Bird and Fish have layEggs methods
    pet.layEggs();

    if (pet instanceof Bird) {
        pet.fly();
    } else {
        pet.swim();
    }
}

The same effect can be achieved

Index type query operator (keyof)

  • Syntax: keyof T

For any type T, the result of keyof T is the union of known public attribute names on t

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

type PersonProps = keyof Person; // 'name' | 'age'

Here, the type returned by keyof Person is the same as the joint type of 'name' | 'age', and can be completely replaced with each other

  • Usage: keyof can only return public property names known on a type
class Animal {
    type: string;
    weight: number;
    private speed: number;
}

type AnimalProps = keyof Animal; // "type" | "weight"

For example, we often get a property value of an object, but we are not sure which property it is. At this time, we can use extends and typeof to restrict the property name. The passed parameters can only be the property name of the object

const person = {
    name: 'Jack',
    age: 20
}

function getPersonValue<T extends keyof typeof person>(fieldName: keyof typeof person) {
    return person[fieldName]
}

const nameValue = getPersonValue('name')
const ageValue = getPersonValue('age')

// An error will be reported: Argument of type '"gender"' is not assignable to parameter of type '"name" | "age"'
// getPersonValue('gender')

Index access operator (T[K])

  • Syntax: T[K]

It is similar to the method of using object index in js, except that the value of object attribute is returned in js, and the type of attribute P corresponding to T is returned in ts

  • Usage:
interface Person {
    name: string
    age: number
    weight: number | string
    gender: 'man' | 'women'
}

type NameType = Person['name']  // string

type WeightType = Person['weight']  // string | number

type GenderType = Person['gender']  // "man" | "women"

3, Mapping type

Read only type (readonly < T >)

  • definition:
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}

Used to set all properties of type T to read-only.

  • Usage:
interface Person {
    name: string
    age: number
}

const person: Readonly<Person> = {
    name: 'Lucy',
    age: 22
}

// Can't assign to 'name' because it is a read only property
person.name = 'Lily'

Readonly is read-only. The attribute marked with readonly can only be assigned in the declaration or the constructor of the class, and then it will not be changed (i.e. read-only attribute)

Read only array (readonlyarray < T >)

  • definition:
interface ReadonlyArray<T> {
    /** Iterator of values in the array. */
    [Symbol.iterator](): IterableIterator<T>;

    /**
     * Returns an iterable of key, value pairs for every entry in the array
     */
    entries(): IterableIterator<[number, T]>;

    /**
     * Returns an iterable of keys in the array
     */
    keys(): IterableIterator<number>;

    /**
     * Returns an iterable of values in the array
     */
    values(): IterableIterator<T>;
}

Variables can only be assigned when the array is initialized, and the array cannot be modified after that

  • use:
interface Person {
    name: string
}

const personList: ReadonlyArray<Person> = [{ name: 'Jack' }, { name: 'Rose' }]

// An error will be reported: Property 'push' does not exist on type 'readonly Person []'
// personList.push({ name: 'Lucy' })

// However, if the internal element is a reference type, the element itself can be modified
personList[0].name = 'Lily'

Optional type (partial < T >)

It is used to set all attributes of type T to optional status. First, get all attributes of type T through keyof T,
Then traverse through the in operator, and finally add?, after the attribute?, Make the attribute optional.

  • definition:
type Partial<T> = {
    [P in keyof T]?: T[P];
}
  • Usage:
interface Person {
    name: string
    age: number
}

// Error will be reported: Type '{}' is missing the following properties from type 'Person': name, age
// let person: Person = {}

// The new type returned after Partial mapping, name and age, have become optional attributes
let person: Partial<Person> = {}

person = { name: 'pengzu', age: 800 }

person = { name: 'z' }

person = { age: 18 }

Required type (required < T >)

Contrary to Partial

It is used to set all attributes of type T as required. First, use keyof T to get all attributes of type T,
Then traverse through the in operator, and finally after the attribute? Add - to make the attribute mandatory.

  • definition:
type Required<T> = {
    [P in keyof T]-?: T[P];
}
  • use:
interface Person {
    name?: string
    age?: number
}

// After using the Required mapping, the new type returned, name and age, have become Required attributes
// Error will be reported: type '{}' is missing the following properties from type 'required < person >': name, age
let person: Required<Person> = {}

Extract attributes (pick < T >)

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

Extract some properties from the T type as the new return type.

  • Usage: for example, when sending a network request, we only need to pass some attributes in the type, which can be realized through Pick.
interface Goods {
    type: string
    goodsName: string
    price: number
}

// As network request parameters, only goodsName and price are required
type RequestGoodsParams = Pick<Goods, 'goodsName' | 'price'>
// Return type:
// type RequestGoodsParams = {
//     goodsName: string;
//     price: number;
// }
const params: RequestGoodsParams = {
    goodsName: '',
    price: 10
}

Exclude attributes (omit < T >)

  • Definition: type omit < T, K extends keyof T > = pick < T, exclude < keyof T, k > >

    Contrary to Pick, it is used to exclude some attributes from T type

  • Usage: for example, a box has length, width and height, and a cube has the same length, width and height, so it only needs to be long. At this time, Omit can be used to generate the type of cube

interface Rectangular {
    length: number
    height: number
    width: number
}

type Square = Omit<Rectangular, 'height' | 'width'>
// Return type:
// type Square = {
//     length: number;
// }

const temp: Square = { length: 5 }

Extraction type (extract < T, u >)

  • Syntax: extract < T, u >

    Extract the types in T that can be assigned to U

  • Definition: type extract < T, u > = t extends u? T : never;

  • Usage:

type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

type T02 = Extract<string | number | (() => void), Function>;  // () => void

Exclusion type (exclude < T, u >)

  • Syntax: exclude < T, u >

    Contrary to Extract usage, types that can be assigned to U are excluded from T

  • Definition: type exclude < T, u > = t extends u? never : T

  • Usage:

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"

type T01 = Exclude<string | number | (() => void), Function>;  // string | number

Attribute mapping (record < K, t >)

  • definition:
type Record<K extends string | number | symbol, T> = {
    [P in K]: T;
}

Receive two generic types. K must be a type that can be assigned to string | number | symbol. K is traversed through the in operator. The type of each attribute must be T

  • Usage: for example, if we want to convert an array of Person type into an object mapping, we can use Record to specify the type of mapping object
interface Person {
    name: string
    age: number
}

const personList = [
    { name: 'Jack', age: 26 },
    { name: 'Lucy', age: 22 },
    { name: 'Rose', age: 18 },
]

const personMap: Record<string, Person> = {}

personList.map((person) => {
    personMap[person.name] = person
})

For example, when passing a parameter, if you want the parameter to be an object, but you don't know the specific type, you can use Record as the parameter type

function doSomething(obj: Record<string, any>) {
}

Non nullable type (nonnullable < T >)

  • Definition: type nonnullable < T > = t extends null | undefined? never : T

null, undefined and never types are excluded from T, and void and unknow n types are not excluded

type T01 = NonNullable<string | number | undefined>;  // string | number

type T02 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]

type T03 = NonNullable<{name?: string, age: number} | string[] | null | undefined>;  // {name?: string, age: number} | string[]

Constructor parameter type (constructorparameters < typeof T >)

Returns the tuple type composed of constructor parameter types in class

  • definition:
/**
 * Obtain the parameters of a constructor function type in a tuple
 */
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
  • use:
class Person {
    name: string
    age: number
    weight: number
    gender: 'man' | 'women'

    constructor(name: string, age: number, gender: 'man' | 'women') {
        this.name = name
        this.age = age;
        this.gender = gender
    }
}

type ConstructorType = ConstructorParameters<typeof Person>  //  [name: string, age: number, gender: "man" | "women"]

const params: ConstructorType = ['Jack', 20, 'man']

Instance type (instancetype < T >)

Gets the return type of the class constructor

  • definition:
/**
 * Obtain the return type of a constructor function type
 */
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;
  • use:
class Person {
    name: string
    age: number
    weight: number
    gender: 'man' | 'women'

    constructor(name: string, age: number, gender: 'man' | 'women') {
        this.name = name
        this.age = age;
        this.gender = gender
    }
}

type Instance = InstanceType<typeof Person>  // Person

const params: Instance = {
    name: 'Jack',
    age: 20,
    weight: 120,
    gender: 'man'
}

Function parameter type (parameters < T >)

Gets the tuple of the parameter types of the function

  • definition:
/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
  • Usage:
type FunctionType = (name: string, age: number) => boolean

type FunctionParamsType = Parameters<FunctionType>  // [name: string, age: number]

const params:  FunctionParamsType = ['Jack', 20]

Function return value type (ReturnType < T >)

Gets the return value type of the function

  • definition:
/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
  • use:
type FunctionType = (name: string, age: number) => boolean | string

type FunctionReturnType = ReturnType<FunctionType>  // boolean | string

4, Summary

  • Advanced type

    usagedescribe
    &Cross type, merging multiple types into one type, intersection
    \Union type, which combines multiple types into one type, can be any one of multiple types, and can be combined
  • keyword

    usagedescribe
    T extends UType constraint to determine whether T can be assigned to U
    P in TType mapping, traversing all types of T
    parameterName is TypeType predicate to judge whether the function parameter parameterName is type
    infer PThe type to be inferred can be used by marking type P with infer
    typeof v === "typename"Primitive type protection determines whether the data type is an primitive type (number, string, boolean, symbol)
    instanceof vType protection, which determines whether the data type is the prototype attribute type of the constructor
    keyofIndex type query operator, which returns the public property name known on the type
    T[K]The index access operator returns the type of attribute P corresponding to T
  • mapping type

    usagedescribe
    ReadonlyMake all attributes in T read-only
    ReadonlyArrayReturns a read-only array of type T
    ReadonlyMap<T, U>Returns a read-only Map composed of T and U types
    PartialMake all attributes in T optional types
    RequiredMake all attributes in T mandatory
    Pick<T, K extends keyof T>Extract some attributes from T
    Omit<T, K extends keyof T>Exclude some attributes from T
    Exclude<T, U>Eliminate the types that can be assigned to U from T
    Extract<T, U>Extract the types in T that can be assigned to U
    Record<K, T>Returns a type with a property name of K and a property value of T
    NonNullableEliminate null and undefined from T
    ConstructorParametersGets a tuple of T's constructor parameter types
    InstanceTypeGets the instance type of T
    ParametersGets a tuple of function parameter types
    ReturnTypeGets the return value type of the function

Write it at the back

If there is something wrong or not rigorous, you are welcome to put forward your valuable opinions. Thank you very much.

If you like or help, welcome Star, which is also a kind of encouragement and support for the author

Keywords: Javascript Front-end TypeScript

Added by colmtourque on Tue, 18 Jan 2022 08:29:03 +0200