TypeScript type gymnastics instance resolution

The sharing content of the weekly meeting will share some TypeScript type knowledge with you by analyzing several practical or interesting examples of type gymnastics. It can also be regarded as a small summary of the knowledge I learned after brushing nearly 100 type-challenges . Due to the limited sharing time, there may actually be many TS skills you don't know that have not been mentioned.

What is type gymnastics

  • Higher-order functions: Pass in a function and return another function.
  • Higher-order components: Pass in one component and return another.
  • Higher-order types: pass in a type, return another type.

In TypeScript, we can use type to define some complex types, and type can declare generic parameters to allow users to pass in types and return the new type through a series of transformations. In fact, the type in TypeScript can be simply understood as a function in the type space:

type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

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

type R = MyPartial<Person>;
/*
type R = {
    name?: string;
    age?: number;
}
*/
复制代码

Type gymnastics is the realization of some higher-order types with special functions

SimpleVue

Implement a type that implements the TS type checking of the Vue Options API. In fact, the requirements can be divided into the following questions:

  1. Attributes in computed and methods cannot be accessed in the data method
  2. This in computed can access the properties of data
  3. This in methods can access the properties of data and computed
  4. This in methods accesses the value type of the computed property is the return value type of the method in computed
SimpleVue({
  data() {
    // @ts-expect-error
    this.firstname;
    // @ts-expect-error
    this.getRandom();
    // @ts-expect-error
    this.data();

    return {
      firstname: 'Type',
      lastname: 'Challenges',
      amount: 10,
    };
  },
  computed: {
    fullname() {
      return `${this.firstname} ${this.lastname}`;
    },
  },
  methods: {
    getRandom() {
      return Math.random();
    },
    hi() {
      alert(this.amount);
      alert(this.fullname.toLowerCase());
      alert(this.getRandom());
    },
  },
});
复制代码

the this parameter of the function

declare function SimpleVue(options: {
  // 函数的 this 参数是 TS 函数中的一个特殊参数,用来约束函数的 this 类型
  // 声明 this 参数为空类型
  data: (this: {}) => any;
}): any;

SimpleVue({
  data() {
    // @ts-expect-error
    this.firstname;
    // @ts-expect-error
    this.getRandom();
    // @ts-expect-error
    this.data();

    return {
      firstname: 'Type',
      lastname: 'Challenges',
      amount: 10,
    };
  },
});
复制代码

ThisType

ThisType is a tool type built into TypeScript that can be used to mark the this type of a method in an object type.

E.g:

type ObjectDescriptor<D, M> = {
  data?: D;
  methods?: M & ThisType<D & M>; // Type of 'this' in methods is 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;
}

let obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx; // Strongly typed this
      this.y += dy; // Strongly typed this
    },
  },
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);
复制代码

After understanding TypeType, we can solve the second and third problems.

pattern matching

When writing some complex types, we often need to use pattern matching to let the compiler infer the subtypes of a type for us.

The PromiseValue type is a common and very simple application of pattern matching.

type PromiseValue<P extends Promise<unknown>> = P extends Promise<infer V> ? V : never;
type V = PromiseValue<Promise<number>>; // => number
复制代码

Note that this implementation also uses conditional types and the infer operator.

条件类型让 TS 的类型空间有了条件控制流,使用形式:

// 如果 A 是 B 的子类型,那么返回 C,否则返回 D
A extends B ? C : D
复制代码

infer 运算符用于在模式匹配中定义一个类型变量,这个类型变量的具体类型由编译器根据模式匹配来推断出来。

结合前面提到的函数 this 参数,我们可以使用模式匹配来推出一个函数的 this 类型:

type GetThisType<F extends (...args: any[]) => void> = F extends (
  this: infer TT,
  ...args: any[]
) => void
  ? TT
  : never;

declare function func(this: { name: string }): void;
type TT = GetThisType<typeof func>;
/*
type TT = {
    name: string;
}
*/
复制代码

为了解决第四个问题,我们需要能够推断出一个函数的返回值类型,实现也很简单,就是利用模式匹配让编译器帮我们 infer 出返回值类型:

type GetReturnType<F extends (...args: unknown[]) => unknown> = F extends (
  ...args: unknown[]
) => infer RT
  ? RT
  : never;

type RT = GetReturnType<() => 666>; // => 666
复制代码

实现

type GetReturnType<F extends (...args: unknown[]) => unknown> = F extends (
  ...args: unknown[]
) => infer RT
  ? RT
  : never;

type GetComputed<C extends Record<string, any>> = {
  [K in keyof C]: GetReturnType<C[K]>;
};

declare function SimpleVue<D, C, M>(options: {
  data: (this: {}) => D;
  computed: C & ThisType<C & D>;
  methods: M & ThisType<M & D & GetComputed<C>>;
}): any;

SimpleVue({
  data() {
    // @ts-expect-error
    this.firstname;
    // @ts-expect-error
    this.getRandom();
    // @ts-expect-error
    this.data();

    return {
      firstname: 'Type',
      lastname: 'Challenges',
      amount: 10,
    };
  },
  computed: {
    fullname() {
      return `${this.firstname} ${this.lastname}`;
    },
  },
  methods: {
    getRandom() {
      return Math.random();
    },
    hi() {
      alert(this.amount);
      alert(this.fullname.toLowerCase());
      alert(this.getRandom());
    },
  },
});
复制代码

promiseAll

实现函数 promiseAll 的类型声明,函数的功能和 Promise.all 一样,需要正确处理参数和返回类型:

const p1 = Promise.resolve(1);
const p2 = Promise.resolve(true);
const p3 = Promise.resolve('good!');
const r = promiseAll([p1, p2, p3]);
// r 类型是:Promise<readonly [number, boolean, string]>
复制代码

第一版实现:

type PromiseValue<P extends Promise<unknown>> = P extends Promise<infer V> ? V : never;

declare function promiseAll<T extends readonly Promise<unknown>[]>(
  promises: T,
): Promise<{
  readonly [P in keyof T]: T[P] extends Promise<unknown> ? PromiseValue<T[P]> : never;
}>;

const p1 = Promise.resolve(1);
const p2 = Promise.resolve(true);
const p3 = Promise.resolve('good!');
const r = promiseAll([p1, p2, p3]);
// const r: Promise<readonly (string | number | boolean)[]>
复制代码

可以看到 value 数组类型被推断成了 readonly (string | number | boolean)[],并不是我们想要的 readonly [number, boolean, string]

上下文类型

会出现上面的问题主要还是因为 typescript 类型的自动推导机制的问题,对于 [p1, p2, p3],tsc 在默认情况下就会把它推断成 (Promise<number> | Promise<boolean> | Promise<string>)[]。tsc 的类型推导设计的有一个规律就是默认情况类型推导比较,例如:const n = 1,这个 n 不会被推断成 1 的字面量类型。

为了让 tsc 能将类型推断的更窄,我们需要一些额外的修饰或者说标记让 tsc 将类型推断的更窄。

对于字面量类型大家都知道用 as const:

const obj = {
  name: 'ly',
} as const;

/**
// obj 被推断成
{
    readonly name: "ly";
}
 */
复制代码

对于 promiseAll 这个问题本身,常见的有两种方式。

一种方式是将数组参数使用数组解构的形式:

declare function promiseAll<T extends readonly Promise<unknown>[]>(
  // 写成数组解构的形式,这样编译器就会将 T 识别为元组
  promises: [...T],
): Promise<{
  readonly [P in keyof T]: T[P] extends Promise<unknown> ? PromiseValue<T[P]> : never;
}>;
复制代码

另一种方式就是泛型参数约束的时候联合一个空元组:

// T extends (readonly Promise<unknown>[]) | []
declare function promiseAll<T extends readonly Promise<unknown>[] | []>(
  promises: T,
): Promise<{
  readonly [P in keyof T]: T[P] extends Promise<unknown> ? PromiseValue<T[P]> : never;
}>;
复制代码

全排列

前面提到了使用条件类型实现 条件控制流,接下来我们通过全排列这个例子使用类型递归来实现 循环控制流

我们要实现的效果:

type R1 = Permutation<'A' | 'B' | 'C'>;
// 3 x 2 x 1 种
// => "ABC" | "ACB" | "BAC" | "BCA" | "CAB" | "CBA"

type R2 = Permutation<'A' | 'B' | 'C' | 'D'>;
/*
// 应该是 4 x 3 x 2 x 1 = 24 种
"ABCD" | "ABDC" | "ACBD" | "ACDB" |
"ADBC" | "ADCB" | "BACD" | "BADC" |
"BCAD" | "BCDA" | "BDAC" | "BDCA" |
"CABD" | "CADB" | "CBAD" | "CBDA" |
"CDAB" | "CDBA" | "DABC" | "DACB" |
"DBAC" | "DBCA" | "DCAB" | "DCBA"
*/
复制代码

模板字符串类型

我们都知道 TS 中有字符串字面量类型,字符串字面量类型其实是 string 类型的子类型:

type S = '666'
// S 是字符串字面量类型 '666'

const s = '666';
// s 是 string 类型

'666' extends string  ? true : false; // => true
string extends '666' ? true : false; // => false
复制代码

模板字符串类型是 typescript 4.1 新增的一个类型,由 C#,TypeScript, Delphi 之父 Anders Hejlsberg(安德斯·海尔斯伯格)亲自实现。结合模式匹配,类型递归等特性极大的增强了字符串类型的可玩性。

在 TS 4.1 以前,由于没有模板字符串类型,下面的代码会报错:

function dateFormat(date: Date, formatStr: string, isUtc: boolean) {
  const getPrefix = isUtc ? 'getUTC' : 'get';
  // eslint-disable-next-line unicorn/better-regex
  return formatStr.replace(/%[YmdHMS]/g, (m: string) => {
    let replaceStrNum: number;
    switch (m) {
      case '%Y':
        // Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Date'.
        //No index signature with a parameter of type 'string' was found on type 'Date'
        return String(date[`${getPrefix}FullYear`]()); // no leading zeros required
      case '%m':
        replaceStrNum = 1 + date[`${getPrefix}Month`]();
        break;
      case '%d':
        replaceStrNum = date[`${getPrefix}Date`]();
        break;
      case '%H':
        replaceStrNum = date[`${getPrefix}Hours`]();
        break;
      case '%M':
        replaceStrNum = date[`${getPrefix}Minutes`]();
        break;
      case '%S':
        replaceStrNum = date[`${getPrefix}Seconds`]();
        break;
      default:
        return m.slice(1); // unknown code, remove %
    }
    // add leading zero if required
    return `0${replaceStrNum}`.slice(-2);
  });
}
复制代码

基本使用

使用插值语法,你可以将已有的字符串字面量类型,数字字面量类型插进一个字符串中得到一个新的字符串字面量类型

type World = 'world';
type Greeting = `hello ${World}`; // => type Greeting = "hello world"
复制代码

如果插值是 never,则整个模板字符串返回就是 never:

type N = `I ${never} give up`; // => never
复制代码

当插值本身是 union 类型时,结果也是 union 类型:

type Feeling = 'like' | 'hate';
type R = `I ${Feeling} you`; // => "I like you" | "I hate you"
复制代码

如果插入了多个 union,那么结果就是所有的组合构成的 union。

type AB = 'A' | 'B';
type CD = 'C' | 'D';
type Combination = `${AB}${CD}`; // => "AC" | "AD" | "BC" | "BD"
复制代码

模板字符串类型在模式匹配中的应用

例如我们要实现一个将传入的字符串语句首字母大写:

type R1 = CapitalFirstLetter<'a little story'>; // => "A little story"
type R2 = CapitalFirstLetter<''>; // => ""
复制代码

我们可以这样实现:

type LetterMapper = {
  a: 'A';
  b: 'B';
  c: 'C';
  d: 'D';
  e: 'E';
  f: 'F';
  g: 'G';
  h: 'H';
  i: 'I';
  j: 'J';
  k: 'K';
  l: 'L';
  m: 'M';
  n: 'N';
  o: 'O';
  p: 'P';
  q: 'Q';
  r: 'R';
  s: 'S';
  t: 'T';
  u: 'U';
  v: 'V';
  w: 'W';
  x: 'X';
  y: 'Y';
  z: 'Z';
};

type CapitalFirstLetter<S extends string> = S extends `${infer First}${infer Rest}`
  ? First extends keyof LetterMapper
    ? `${LetterMapper[First]}${Rest}`
    : S
  : S;
复制代码

类型递归

例如我们要实现所有给一个字符串,返回所有字符都被大写的字符串:

type R1 = UpperCase<'a little story'>; // => "A LITTLE STORY"
type R2 = UpperCase<'nb'>; // => "NB"
复制代码

递归的套路就是:

将首字母大写,然后对剩下部分递归

实现就是:

type UpperCase<S extends string> = S extends `${infer First}${infer Rest}`
  ? `${CapitalFirstLetter<First>}${UpperCase<Rest>}`
	// 当 S 是空串便会走这个分支,直接返回空串即可
  : S;
复制代码

Union 的分布式运算

在 TypeScript 中如果条件类型 extends 左侧是一个 Union 便会触发分布式计算规则:

type Distribute<U> = U extends 1 ? 1 : 2;
// 不熟悉的人可能会觉得返回 2, 认为走 false 分支
type R = Test<1 | 2>; // => 1 | 2

// 等同于
type R1 = (1 extends 1 ? 1 : 2) | (2 extends 1 ? 1 : 2);
复制代码

我们可以使用 Union extends Union 来遍历 Union 的每一项:

// 声明一个额外的泛型 E 来标识循环的元素
type AppendDot<U, E = U> = E extends U ? `${E & string}.` : never;
// 使用 Union 来映射
type R1 = AppendDot<'a' | 'b'>; // => "a." | "b."

// 配合 as 来过滤 keys
type Getter<T> = {
    [P in keyof T as P extends `get${infer Rest}` ? P : never]: T[P];
};

const obj = {
    age: 18,
    getName() {
        return 'ly';
    },
    hello() {
        console.log('hello');
    },
};

type R = Getter<typeof obj>;
/*
type R = {
    getName: () => string;
}
 */
复制代码

判断一个类型是否为 never

实现一个类型 IsNever,达到一下效果:

type R1 = IsNever<number>; // => false
type R2 = IsNever<never>; // => true
复制代码

有人会想这还不简单,直接用条件类型判断一下不就行了,刷刷写下下面的代码:

type IsNever<T> = T extends never ? true : false;

type R1 = IsNever<number>; // => false
// 傻眼了
type R2 = IsNever<never>; // => never
复制代码

原因是 never 默认情况语义是空 union,空 union extends 任何类型返回都是 never。其实这点如果看 TS 的源码就是 TS 看到 extends 左侧就直接返回 never 了。

需要使用额外的标记让 tsc 将 never 识别为独立的类型:

// 标记的方式很多
type IsNever<T> = [T] extends [never] ? true : false;
type IsNever<T> = T[] extends never[] ? true : false;
type IsNever<T> = (() => T) extends () => never ? true : false;
复制代码

全排列的思路

From childhood to high school, we basically learn permutation and combination in math classes every year. In order to solve the problem of full permutation in the type system, we can first think about how to use JS code to achieve full permutation, and think about how to achieve full permutation when you brush leetcode. arranged. The TS type is just a means to implement logic, the key is the idea.

You can use recursion to solve this problem:

n people are all queued up, there are n pits, and the full column is: the first pit has n possibilities, and all the permutations are Permutation(n) = n * Permutation(n - 1)

This is done in JS:

function permutation(list) {
  if (list.length === 1) return [list[0]];

  const result = [];
  for (const [index, e] of list.entries()) {
    const rest = [...list];
    rest.splice(index, 1);

    for (const item of permutation(rest)) {
      result.push([e, ...item]);
    }
  }
  return result;
}

console.log(permutation(['a', 'b', 'c']));
/*
  [ 'a', 'b', 'c' ],
  [ 'a', 'c', 'b' ],
  [ 'b', 'a', 'c' ],
  [ 'b', 'c', 'a' ],
  [ 'c', 'a', 'b' ],
  [ 'c', 'b', 'a' ]
*/
复制代码

TS achieves full permutation

type Permutation<U, E = U> = [U] extends [never]
  ? ''
  : E extends U
  ? `${E & string}${Permutation<Exclude<U, E>>}`
  : '';
复制代码

Operation:

Click to expand answer >
// 自底向上,使用递归来循环
type Fibonacci<
    T extends number,
    // 这个数组用来取 length 表示循环下标
    TArray extends ReadonlyArray<unknown> = [unknown, unknown, unknown],
    // 这个数组的 length 就是前一个项的前一项的值
    PrePre extends ReadonlyArray<unknown> = [unknown],
    // 表示前一项的值
    Pre extends ReadonlyArray<unknown> = [unknown],
> = T extends 1
    ? 1
    : T extends 2
    ? 1
    : TArray['length'] extends T // 表示已经循环了 T 次
    ? [...Pre, ...PrePre]['length'] // 前两项相加
    : Fibonacci<T, [...TArray, unknown], Pre, [...Pre, ...PrePre]>; // 使用递归来循环
复制代码

Effect:

// 斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,
type R1 = Fibonacci<1>; // => 1
type R3 = Fibonacci<3>; // => 2
type R8 = Fibonacci<8>; // => 21
复制代码

Guess you like

Origin juejin.im/post/7079025138913509406