Original link: github.com/whinc/blog/…

Writing TypeScript (TS) applications is a type-fighting process. You need to use TS’s typing tools to describe your goals in different combinations. The more accurate the description, the more accurate the type constraints and hints, and the fewer potential errors. Conversely, the more vague the description (such as any), the less type assistance TS provides and the more potential for error. How to write an accurate type description requires mastering the basic concepts of TS, as well as the common techniques and typing tools available in the TypeScript manual and in this article.

The commonly used skill

This section focuses on some of the basic type tools that are the cornerstone of all advanced types.

typeof T– Gets the type of the JS value

Typeof gets the types of JS variables. It acts as a bridge between the JS value space and the TS type space, allowing us to extract types from existing variables for further processing.

const person = {
    name: "jim",
    age: 99
}
 
type Person = typeof person
// type Person = {
// name: string;
// age: number;
// }
Copy the code

keyof T– Gets the key of the type

Keyof can obtain the target type key, returns the string | number | symbol of subtypes.

interface Person {
    name: string
    age: number
}
 
type K = keyof Person
// type K = "name" | "age"
Copy the code

Further reading:

  • TypeScript 2.1 – keyof and Lookup Types
  • TypeScript 2.8 – Improved keyof with intersection types
  • TypeScript 2.9 – Support number and symbol named properties with keyof and mapped types

T[K]– Index type, which gets the value of the type

Dynamically get the type of the target type attribute, similar to the object value operation in JS, but here get the type of the value.

interface Person {
    name: string
    age: number
}
 
type T1 = Person['name'] // string
type T2 = Person['age'] // number
type T3 = Person[keyof Person] // string | number
Copy the code

Further reading

  • Index types
  • Index types and index signatures

[P in keyof T]: T[P]– Type mapping, converting types

Create a new type based on the old type. In the process of new type construction, we can rewrite the attribute name and value of the old type, so as to achieve type conversion. The in operator represents the key traversing the target type.

interface Person {
    name: string
    age: number
}
 
// Make the attribute of Person optional
type PersonPartical = { [P inkeyof Person]? : Person[P] }// type PersonPartical = {
// name? : string | undefined;
// age? : number | undefined;
// }

// Make the property of Person read-only
type PersonReadonly = { readonly [P in keyof Person]: Person[P] }
// type PersonReadonly = {
// readonly name: string;
// readonly age: number;
// }

// The above two operations are too common, TS has built the corresponding type tools
// For example, the above optional and read-only can be written as
type PersonPartical = Partial<Person>
type PersonReadonly = Readonly<Person>
Copy the code

Further reading

  • Mapped types

T extends U ? X : Y– Condition type

Extends, in addition to being used when inheriting classes, is also used to determine whether one type is a parent of another type and perform different type branches based on the results. This makes TS type useful in programming.

Extends condition judgment rules are as follows: if T can be assigned to U back to X, Y, otherwise if TS can’t determine whether T can be assigned to U, it returns X | Y.

type isString<T> = T extends string ? true : false
 
type T1 = isString<number>  // false
type T2 = isString<string>  // true
Copy the code

Further reading

  • Conditional Types

infer T– Type inference

In the clause of the extends condition type, infer T can be used to capture the type at a given location (inferred by the TS compiler), and captured type variables can be used in the clause following infer. With the extends condition type, intercepts a partial type of the target that matches the condition.

type ParseInt = (n: string) = > number
// if T is a function, R captures its return value type and returns R, otherwise returns any
type ReturnType<T extends(... args:any) = >any> = T extends(... args:any) => infer R ? R : any
type R = ReturnType<ParseInt>   // number
 
type GetType<T> = T extends (infer E)[] ? E : never
type E = GetType<['a'.100] >Copy the code

Further reading

  • Type inference in conditional types

never

The result of never being union with type T (which is any other type except unknown) is type T. This feature of never can be used for type elimination, such as converting a type to never before being union with other types.

type a = string
type b = number
type c = never
type d = a | b |c
// type d = string | number
 
type Exclude<T, U> = T extends U ? never : T;
type T = Exclude<string | number.string>   // number
Copy the code

Further reading

  • Nerver

Type of tool

TS has some common type conversion tools built in. Mastering these tool types not only simplifies type definitions, but also allows you to build more complex type conversions based on them.

Below are all types of tools built into TS. I have added annotations and examples for easy understanding. You can see the examples first and test whether you can write the corresponding type implementation (Playground) by yourself.

/** * sets all attributes of T to optional ** Partial<{name: string}> // {name? : string | undefined} */
type Partial<T> = {
    [P inkeyof T]? : T[P]; };/** * make all attributes of type T Required ** Required<{name? : string}> // {name: string} */
type Required<T> = {
    [P inkeyof T]-? : T[P]; };/** * Readonly<{name: string}> // {Readonly name: string} */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
 
** Pick<{name: string, age: number}, 'age'> // {age: number} */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
 
/** * const map: Record
      
        = {a: 1, b: 2} */
      ,>
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
 
Type the type T to remove / * * * U * * Exclude < 'a' | 'b', 'a' > 'b' / * /
type Exclude<T, U> = T extends U ? never : T;
 
/ pick out the type from the type T * * * U * * Extract < string | number, number > / / number * /
type Extract<T, U> = T extends U ? T : never;
 
** Omit<{name: string, age: number}, 'age'> // {name: number} */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
 
/ rejecting the null, and undefined type T * * * * * NonNullable subtype < string | null | undefined > / / string * /
type NonNullable<T> = T extends null | undefined ? never : T;
 
** Parameters<(name: string, age: number) => void> // [string, number] */
type Parameters<T extends(... args:any) = >any> = T extends(... args: infer P) =>any ? P : never;
 
Class Person {constructor(name: string, age: {}} * ConstructorParameters
      
        // [string, number] Person also represents the typeof an instance of a class, such as a constructor or a static method, such as a member variable or a member method. New Person represents an instance of a class
      
type ConstructorParameters<T extends new(... args:any) = >any> = T extends new(... args: infer P) =>any ? P : never;
 
/** * get the ReturnType of function ** ReturnType<() => string> // string */
type ReturnType<T extends(... args:any) = >any> = T extends(... args:any) => infer R ? R : any;
 
Class Person {constructor(name: string, age:...); / / class Person {constructor(name: string, age:...) number) { } } * InstanceType
      
        // Person */
      
type InstanceType<T extends new(... args:any) = >any> = T extends new(... args:any) => infer R ? R : any;
Copy the code

In addition to the built-in TS typing tools, there are also third-party typing tools that provide more type conversion tools, such as TS-Toolbelt – TS version of the “LoDash” library.

Here are some examples from TS-Toolbelt, and see its website for more tool types.

import type { Object } from 'ts-toolbelt'

// Make some attributes of the object optional
type T1 = Object.Optional<{ a: string; b: number }, 'a'>
// type T1 = {
// a? : string | undefined;
// b: number;
// }

// Merge two objects. The attribute of the first object with undefined is overwritten by the corresponding attribute of the second object
type T2 = Object.MergeUp<{ a: 'a1', b? :'b1' }, { a: 'a2', b: 'b2'} >// type T2 = {
// a: "a1";
// b: "b1" | "b2";
// }
Copy the code

Case analysis

Once you have mastered the basic concepts, you may still not be able to write an accurate type description because these concepts are only used as a single concept and require further practice to be fully understood. Below collected some TS type conversion cases (topic), you can learn some solution ideas and code implementation.

No.1

The problem

Assume that all values of the object are array types, for example:

const data = {
  a: ['x'.'y'.'z'],
  b: [1.2.3]}as const
Copy the code

Requires the type of the array element in the above object value, for example:

type TElement = "x" | "y" | "z" | 3 | 1 | 2
Copy the code

Their thinking

First we get the value type of the object, and then we get the type of the array element by the array subscript.

Reference code

type GetValueElementType<T extends { [key: string]: ReadonlyArray<any> }> = T[keyof T][number]
type TElement  = GetValueElementType<typeof data>
Copy the code

extension

What if the values of the objects are not all array types?

For example:

const data = {
  a: ['x'.'y'.'z'],
  b: [1.2.3],
  c: 100
} as const
Copy the code

Get the value type of the object, filter out the array type, and finally get the element type of the array

Implementation 1: Extends determines the value type of an object, and gets the element type by array subscript
type GetValueElementType<T> = { [K in keyof T]: T[K] extends ReadonlyArray<any>? T[K][number] : never }[keyof T]
 
 
Implementation 2: Determines the value type of an object using extends and obtains the array element type by infer
type GetValueElementType<T> = { [K in keyof T]: T[K] extends ReadonlyArray<infer E> ? E : never }[keyof T]
Copy the code

No.2

The problem

Suppose you have an EffectModule class that contains member variables and member methods as follows:

interfaceAction<T> { payload? : T;type: string;
}

class EffectModule {
  count = 1;
  message = "hello!";

  delay(input: Promise<number>) {
    return input.then(i= > ({
      payload: `hello ${i}! `.type: 'delay'
    }));
  }

  setMessage(action: Action<Date>) {
    return{ payload: action.payload! .getMilliseconds(),type: "set-message"}; }}Copy the code

Now there’s a function called connect that takes an instance of EffectModule and turns it into another object that has only the method of the same name on it,

type Connected = {
  delay(input: number): Action<string>
  setMessage(action: Date): Action<number>}const effectModule = new EffectModule()
const connected: Connected = connect(effectModule)
Copy the code

After the connect function, the method’s type signature becomes:

asyncMethod<U, R>(input: Promise<U>): Promise<Action<R>>  
/ / into
asyncMethod<U, R>(input: U): Action<R> 

syncMethod<U, R>(action: Action<U>): Action<R>
/ / into
syncMethod<U, R>(action: U): Action<R>
Copy the code

We need to implement the following Connect function type, replace any with the solution of the problem, so that the compiler can compile smoothly, and return the same type as Connected.

type Connect = (module: EffectModule) => any
Copy the code

This question comes from an interview question of LeeCode China recruitment.

Their thinking

  1. Filter outEffectModuleInstance member method
  2. throughT extends UCheck the method signature a. If the method signature meets the conditions, useinferCapture the generic parameters in the Promise and Action and return the correct method type signature b. Otherwise, return the method’s original type signature

Reference code

// Get the attribute name of the function whose value is in the object
type FilterFunctionNames<T extends {}> = {[P in keyof T]: T[P] extends Function ? P: never}[keyof T]
// Convert the function type signature
type TransformFunctions<T extends {}> = {
    [P in keyof T]: T[P] extends (arg: Promise<infer U>) => Promise<Action<infer R>>
    ? (arg: U) = > Action<R>
    : T[P] extends (arg1: Action<infer U>) => Action<infer R>
    ? (arg: U) = > Action<R>
    : never
}
// 1. Filter out the instance attribute names whose value is the function type
// 2. Use Pick to select members whose value is a function type to form a new object
// 3. Iterate over the key/value of the object and convert the type signatures that meet the conditions into target signatures
type Connect = (module: EffectModule) = > TransformFunctions<Pick<EffectModule, FilterFunctionNames<EffectModule>>>
Copy the code

The implementation of the TransformFunctions above can also be simplified. For example, change the return value type Promise

to Promise

, but the former is more accurate. The former constrains that the return value must be of type Promise + Action, while the latter only constrains that the return value is of type Promise.

type TransformFunctions<T extends {}> = {
    [P in keyof T]: T[P] extends (arg: Promise<infer U>) => Promise<infer R>
    ? (arg: U) = > R
    : T[P] extends (arg1: Action<infer U>) => infer R
    ? (arg: U) = > R
    : never
}
Copy the code

Case to be continued…

reference

  • Inside TypeScript
  • TypeScript Manual
  • TypeScript tips are picked up
  • Utility Types