4
头图

The theme shared with you today is to do type gymnastics together.

It is mainly divided into 4 parts for introduction:

  1. The background of type gymnastics, through the background to understand why to include type gymnastics in the project;
  2. Understand the main types, operation logic, and type routines of type gymnastics;
  3. Type gymnastics practice, parsing TypeScript built-in advanced types, handwriting ParseQueryString complex types;
  4. Summarize, share the above, and precipitate the conclusion.

1. Background

In the background chapter, what is type, what is type safety, how to achieve type safety, and what is type gymnastics?

to understand what type gymnastics mean.

1. What is a type?

Before understanding what a type is, let's introduce two concepts:

  • Different types of variables occupy different memory sizes

Variables of type boolean will allocate 4 bytes of memory, while variables of type number will allocate 8 bytes of memory. Declaring different types for variables means that they will occupy different memory spaces.

  • Different types of variables can do different operations

The number type can perform operations such as addition, subtraction, multiplication and division, but boolean cannot. Different types of objects in composite types have different methods, such as Date and RegExp. Different types of variables mean different operations on the variable.

To sum up, a simple conclusion can be drawn that a type is an abstract definition of different content provided by a programming language .

2. What is type safety?

After understanding the concept of types, then, what is type safety?

A simple definition is that type safety is doing only what the type allows. For example, for boolean type, addition, subtraction, multiplication and division are not allowed, only true and false are allowed.

When we can achieve type safety, we can greatly reduce potential problems in the code and greatly improve the quality of the code.

3. How to achieve type safety?

So, how to achieve type safety?

Two types of type checking mechanisms are introduced here, namely dynamic type checking and static type checking.

3.1 Dynamic type checking

Javascript is a typical dynamic type check. It has no type information at compile time, and only checks at runtime, resulting in many hidden bugs.

3.2 Static type checking

As a superset of Javascript, TypeScript uses static type checking, which has type information at compile time to check type problems and reduce potential problems at runtime.

4. What is type gymnastics

Some definitions of types are introduced above, which are all familiar background introductions about types. This chapter returns to the theme concept shared this time, type gymnastics.

Before learning about type gymnastics, let's introduce the 3 type systems.

4.1 Simple Type System

Simple type system, it only checks based on the declared type, such as an addition function, which can add integers or decimals, but in a simple type system, two functions need to be declared to do this.

 int add(int a, int b) {
    return a + b
}

double add(double a, double b) {
    return a + b
}

4.2 The generic type system

The generic type system, which supports type parameters, can dynamically define types by passing parameters to parameters, making types more flexible.

 T add<T>(T a, T b) {
    return a + b
}

add(1, 2)
add(1.1, 2.2)

However, it is not applicable in some scenarios that require logical operations on type parameters, such as a function type that returns the value of an attribute of an object.

 function getPropValue<T>(obj: T, key) {
  return obj[key]
}

4.3 Type programming system

Type programming system, it not only supports type parameters, but also performs various logical operations on type parameters. For example, the function type that returns an attribute value of an object mentioned above can be obtained by logical operation through keyof and T[K]. .

 function getPropValue<
  T extends object, 
  Key extends keyof T
>(obj: T, key: Key): T[Key] {
  return obj[key]
}

To sum up the above, type gymnastics is type programming, where various logical operations are performed on type parameters to generate new types .

The reason why it is called gymnastics is because of its complexity. The right side is a function type that parses parameters, and many complex logical operations are used in it. After introducing the operation method of type programming, we will analyze the implementation of this type. .

Learn about types of gymnastics

After you are familiar with the concept of type gymnastics, let's continue to understand what types of type gymnastics are, what operation logics are supported, and what operation routines are there.

1. What types are there?

Types The main types of gymnastics are listed in the figure. TypeScript reuses the basic types and composite types of JS, and adds tuple (Tuple), interface (Interface), enumeration (Enum) and other types, these types should be very common in the daily development process type declaration, do not do Repeat.

 // 元组(Tuple)就是元素个数和类型固定的数组类型
type Tuple = [number, string];

// 接口(Interface)可以描述函数、对象、构造器的结构:
interface IPerson {
    name: string;
    age: number;
}

class Person implements IPerson {
    name: string;
    age: number;
}

const obj: IPerson = {
    name: 'aa',
    age: 18
}

// 枚举(Enum)是一系列值的复合:
enum Transpiler {
    Babel = 'babel',
    Postcss = 'postcss',
    Terser = 'terser',
    Prettier = 'prettier',
    TypeScriptCompiler = 'tsc'
}

const transpiler = Transpiler.TypeScriptCompiler;

2. Operational logic

The focus is on the arithmetic logic supported by type programming.

TypeScript supports 9 operation logics including condition, deduction, union, intersection, and mapping of union types.

  • Condition: T extends U ? X : Y

Conditional judgment is the same as js logic, both return a if the condition is met, otherwise return b.

 // 条件:extends ? :
// 如果 T 是 2 的子类型,那么类型是 true,否则类型是 false。
type isTwo<T> = T extends 2 ? true : false;
// false
type res = isTwo<1>;
  • Constraints: extends

Types are restricted by the constraint syntax extends.

 // 通过 T extends Length 约束了 T 的类型,必须是包含 length 属性,且 length 的类型必须是 number。
interface Length {
    length: number
}

function fn1<T extends Length>(arg: T): number{
    return arg.length
}
  • Derivation: infer

The derivation is a regular match similar to js. When all the formula conditions are met, the variables in the formula can be extracted and returned directly or processed again.

 // 推导:infer
// 提取元组类型的第一个元素:
// extends 约束类型参数只能是数组类型,因为不知道数组元素的具体类型,所以用 unknown。
// extends 判断类型参数 T 是不是 [infer F, ...infer R] 的子类型,如果是就返回 F 变量,如果不是就不返回
type First<T extends unknown[]> = T extends [infer F, ...infer R] ? F : never;
// 1
type res2 = First<[1, 2, 3]>;
  • United:|

A joint representative can be one of several types.

 type Union = 1 | 2 | 3
  • cross:&

A cross represents a combination of types.

 type ObjType = { a: number } & { c: boolean }
  • Index query: keyof T

keyof is used to get all keys of a certain type and its return value is a union type.

 // const a: 'name' | 'age' = 'name'
const a: keyof {
    name: string,
    age: number
} = 'name'
  • Index access: T[K]

T[K] is used to access the index to get the union type of the value corresponding to the index.

 interface I3 {
  name: string,
  age: number
}

type T6 = I3[keyof I3] // string | number
  • Index Traversal: in

in is used to iterate over union types.

 const obj = {
    name: 'tj',
    age: 11
}

type T5 = {
    [P in keyof typeof obj]: any
}

/*
{
  name: any,
  age: any
}
*/
  • Index remapping: as

as is used to modify the key of the map type.

 // 通过索引查询 keyof,索引访问 t[k],索引遍历 in,索引重映射 as,返回全新的 key、value 构成的新的映射类型
type MapType<T> = {
    [
    Key in keyof T
    as `${Key & string}${Key & string}${Key & string}`
    ]: [T[Key], T[Key], T[Key]]
}
// {
//     aaa: [1, 1, 1];
//     bbb: [2, 2, 2];
// }
type res3 = MapType<{ a: 1, b: 2 }>

3. Arithmetic routines

According to the 9 operational logics introduced above, I have summarized 4 types of routines.

  • Pattern matching for extraction;
  • Reconstruct to do transformation;
  • Recursive multiplexing as a loop;
  • The length of the array is counted.

3.1 Pattern matching for extraction

The first type routine is pattern matching for extraction.

Extracting by pattern matching means extending a pattern type through the type, and putting the part to be extracted into the local variable declared through infer.

For example, use pattern matching to extract function parameter types.

 type GetParameters<Func extends Function> =
    Func extends (...args: infer Args) => unknown ? Args : never;

type ParametersResult = GetParameters<(name: string, age: number) => string>

First use extends to restrict that the type parameter must be of type Function.

Then use extends as the parameter type to match the formula, and when the formula is satisfied, extract the variable Args in the formula.

Implements the extraction of function parameter types.

3.2 Reconstruction for transformation

The second type routine is to reconstruct and transform.

Reconstructing and transforming means that if you want to change, you need to reconstruct a new type, and you can filter and transform the original type in the process of constructing a new type.

For example, to realize the reconstruction of a string type.

 type CapitalizeStr<Str extends string> =
    Str extends `${infer First}${infer Rest}`
    ? `${Uppercase<First>}${Rest}` : Str;

type CapitalizeResult = CapitalizeStr<'tang'>

First restrict the parameter type must be a string type.

Then use extends as the parameter type to match the formula, extract the variable First Rest in the formula, and encapsulate it through Uppercase.

Implements a capitalized string literal type.

3.3 Recursive reuse as a loop

The third type of routine is recursive multiplexing to make loops.

TypeScript itself does not support looping, but an indeterminate number of type programming can be done recursively to achieve the effect of looping.

For example, array type inversion can be realized by recursion.

 type ReverseArr<Arr extends unknown[]> =
    Arr extends [infer First, ...infer Rest]
    ? [...ReverseArr<Rest>, First]
    : Arr;


type ReverseArrResult = ReverseArr<[1, 2, 3, 4, 5]>

First limit the parameter must be an array type.

Then use extends to match the formula, if the condition is met, call itself, otherwise return directly.

Implements an array inversion type.

3.4 Counting the length of the array

The fourth type routine is to count the length of the array.

Type programming itself does not support addition, subtraction, multiplication and division, but you can recursively construct an array of a specified length, and then take the length of the array to complete the addition, subtraction, multiplication and division of values.

For example, the addition operation of type programming is realized through the length of the array.

 type BuildArray<
    Length extends number,
    Ele = unknown,
    Arr extends unknown[] = []
    > = Arr['length'] extends Length
    ? Arr
    : BuildArray<Length, Ele, [...Arr, Ele]>;

type Add<Num1 extends number, Num2 extends number> =
    [...BuildArray<Num1>, ...BuildArray<Num2>]['length'];


type AddResult = Add<32, 25>

First create an array type that can generate any length by recursion

Then create an addition type to implement the addition operation by the length of the array.

3. Types of Gymnastics Practice

The third part of the sharing is type gymnastics practice.

The concept of type gymnastics and the commonly used operational logic were shared earlier.

Next, we use these operation logic to parse the advanced types built in TypeScript.

1. Parse TypeScript built-in advanced types

  • partial makes the index optional

Iterate over the indices via the in operator, adding ? for all indices? The prefix implementation makes indexing an optional new map type.

 type TPartial<T> = {
    [P in keyof T]?: T[P];
};

type PartialRes = TPartial<{ name: 'aa', age: 18 }>
  • Required makes the index mandatory

Iterate over indices by in operator, delete for all indices? The prefix implementation makes indexing a mandatory new mapping type.

 type TRequired<T> = {
    [P in keyof T]-?: T[P]
}

type RequiredRes = TRequired<{ name?: 'aa', age?: 18 }>
  • Readonly makes the index read-only

Traversing the index through the in operator, adding the readonly prefix to all indexes implements a new map type that makes the index read-only.

 type TReadonly<T> = {
    readonly [P in keyof T]: T[P]
}

type ReadonlyRes = TReadonly<{ name?: 'aa', age?: 18 }>
  • Pick preserves the filter index

First restrict the second parameter to be the key value of the object, and then traverse the second parameter through the in operator to generate a new map type implementation.

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

type PickRes = TPick<{ name?: 'aa', age?: 18 }, 'name'>
  • Record create mapping type

The union type K is traversed through the in operator, creating a new map type.

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

type RecordRes = TRecord<'aa' | 'bb', string>
  • Exclude removes part of a union type

Through the extends operator, it is judged whether parameter 1 can be assigned to parameter 2, and if so, it returns never to delete part of the union type.

 type TExclude<T, U> = T extends U ? never : T

type ExcludeRes = TExclude<'aa' | 'bb', 'aa'>
  • Extract preserves part of union type

Contrary to the logic of Exclude, it judges whether parameter 1 can be assigned to parameter 2, and if not, returns never, so as to retain part of the union type.

 type TExtract<T, U> = T extends U ? T : never

type ExtractRes = TExtract<'aa' | 'bb', 'aa'>
  • Omit delete filter index

Through the combination of the advanced type Pick and Exclude, delete the filter index.

 type TOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

type OmitRes = TOmit<{ name: 'aa', age: 18 }, 'name'>
  • Awaited is used to get the valueType of the Promise

Get the value type of Promise of unknown level by recursion.

 type TAwaited<T> =
    T extends null | undefined
        ? T
        : T extends object & { then(onfulfilled: infer F): any }
            ? F extends ((value: infer V, ...args: any) => any)
                ? Awaited<V>
                : never
            : T;


type AwaitedRes = TAwaited<Promise<Promise<Promise<string>>>>

There are many more advanced types, and the implementation ideas are mostly the same as the type routines introduced above, so I won't go into details here.

2. Parse the ParseQueryString complex type

The focus on parsing is to introduce the complexity of type gymnastics in the background chapter, exemplifying the types of functions that parse string arguments.

As shown in the demo, this function is used to parse the specified string format into object format.

 function parseQueryString1(queryStr) {
  if (!queryStr || !queryStr.length) {
    return {}
  }
  const queryObj = {}
  const items = queryStr.split('&')
  items.forEach((item) => {
    const [key, value] = item.split('=')
    if (queryObj[key]) {
      if (Array.isArray(queryObj[key])) {
        queryObj[key].push(value)
      } else {
        queryObj[key] = [queryObj[key], value]
      }
    } else {
      queryObj[key] = value
    }
  })
  return queryObj
}

For example, get the value of a in the string a=1&b=2.

Commonly used type declarations are shown in the following figure:

 function parseQueryString1(queryStr: string): Record<string, any> {
  if (!queryStr || !queryStr.length) {
    return {}
  }
  const queryObj = {}
  const items = queryStr.split('&')
  items.forEach((item) => {
    const [key, value] = item.split('=')
    if (queryObj[key]) {
      if (Array.isArray(queryObj[key])) {
        queryObj[key].push(value)
      } else {
        queryObj[key] = [queryObj[key], value]
      }
    } else {
      queryObj[key] = value
    }
  })
  return queryObj
}

The parameter type is string , the return type is Record<string, any> , then we see, res1.a the type is any Do you know the type of ---73baec57fbbc8804f4f37f88c5885fb6 a is 字面量类型 1 ?

Let's rewrite the function type for parsing string parameters through type gymnastics.

首先限制参数类型是string类型,然后a&b ,如果满足公式, a解析key value的映射type, will b recursive ParseQueryString type, continue parsing until the a&b formula is no longer satisfied.

Finally, you can get a precise function return type, res.a = 1 .

 type ParseParam<Param extends string> =
    Param extends `${infer Key}=${infer Value}`
        ? {
            [K in Key]: Value
        } : Record<string, any>;

type MergeParams<
    OneParam extends Record<string, any>,
    OtherParam extends Record<string, any>
> = {
  readonly [Key in keyof OneParam | keyof OtherParam]:
    Key extends keyof OneParam
        ? OneParam[Key]
        : Key extends keyof OtherParam
            ? OtherParam[Key]
            : never
}

type ParseQueryString<Str extends string> =
    Str extends `${infer Param}&${infer Rest}`
        ? MergeParams<ParseParam<Param>, ParseQueryString<Rest>>
        : ParseParam<Str>;
 function parseQueryString<Str extends string>(queryStr: Str): ParseQueryString<Str> {
    if (!queryStr || !queryStr.length) {
        return {} as any;
    }
    const queryObj = {} as any;
    const items = queryStr.split('&');
    items.forEach(item => {
        const [key, value] = item.split('=');
        if (queryObj[key]) {
            if(Array.isArray(queryObj[key])) {
                queryObj[key].push(value);
            } else {
                queryObj[key] = [queryObj[key], value]
            }
        } else {
            queryObj[key] = value;
        }
    });
    return queryObj as any;
}


const res = parseQueryString('a=1&b=2&c=3');

console.log(res.a) // type 1

4. Summary

In summary, the type of gymnastics is introduced from three aspects.

  • The first point is the background of type gymnastics, understanding what type is, what type safety is, and how to achieve type safety;
  • The second point is to be familiar with the main types of type gymnastics, supported logical operations, and summarize 4 types of routines;
  • The third point is the practice of type gymnastics, which parses the implementation of TypeScript's built-in advanced types and writes some complex function types by hand.

From this, we learned that in scenarios that need to generate types dynamically, it is necessary to use type programming to do some operations. Even if type programming is not required in some scenarios, using type programming can have more accurate type hints and checks, reducing the potential in the code. question.

References + source code

Here are the reference materials and sample source code shared this time, and you are welcome to expand and read.


凹凸实验室
2.3k 声望5.5k 粉丝

凹凸实验室(Aotu.io,英文简称O2) 始建于2015年10月,是一个年轻基情的技术团队。