Resolución de instancia de gimnasia de tipo TypeScript

El contenido compartido de la reunión semanal compartirá con usted algunos conocimientos de TypeScript mediante el análisis de varios ejemplos prácticos o interesantes de gimnasia tipográfica. También se puede considerar como un pequeño resumen del conocimiento que aprendí después de superar casi 100 desafíos tipográficos . Debido al tiempo limitado para compartir, en realidad puede haber muchas habilidades de TS que no conoce y que no se han mencionado.

¿Qué es el tipo de gimnasia?

  • Funciones de orden superior: pasa una función y devuelve otra función.
  • Componentes de orden superior: pasa un componente y devuelve otro.
  • Tipos de orden superior: pasa un tipo, devuelve otro tipo.

En TypeScript, podemos usar type para definir algunos tipos complejos, y type puede declarar parámetros genéricos para permitir a los usuarios pasar tipos y devolver el nuevo tipo a través de una serie de transformaciones. De hecho, el tipo en TypeScript puede entenderse simplemente como una función en el espacio de tipos:

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;
}
*/
复制代码

La gimnasia tipo es la realización de algunos tipos de orden superior con funciones especiales.

SimpleVue

Implemente un tipo que implemente la comprobación de tipo TS de la API de opciones de Vue. De hecho, los requisitos se pueden dividir en las siguientes preguntas:

  1. No se puede acceder a los atributos en métodos calculados y en el método de datos
  2. Esto en computado puede acceder a las propiedades de los datos.
  3. Esto en los métodos puede acceder a las propiedades de los datos y calcular
  4. Esto en métodos accede al tipo de valor de la propiedad computada es el tipo de valor de retorno del método en computado
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());
    },
  },
});
复制代码

el este parámetro de la función

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,
    };
  },
});
复制代码

Este tipo

ThisType es un tipo de herramienta integrado en TypeScript que se puede usar para marcar este tipo de un método en un tipo de objeto.

P.ej:

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);
复制代码

Después de entender TypeType, podemos resolver el segundo y tercer problema.

la coincidencia de patrones

Al escribir algunos tipos complejos, a menudo necesitamos usar la coincidencia de patrones para permitir que el compilador infiera los subtipos de un tipo por nosotros.

El tipo PromiseValue es una aplicación común y muy simple de coincidencia de patrones.

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

Tenga en cuenta que esta implementación también usa tipos condicionales y el operador inferir.

条件类型让 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;
复制代码

全排列的思路

Desde la niñez hasta la escuela secundaria, básicamente aprendemos permutación y combinación en las clases de matemáticas todos los años. Para resolver el problema de la permutación completa en el sistema de tipos, primero podemos pensar en cómo usar el código JS para lograr la permutación completa y pensar en cómo lograr una permutación completa cuando aplicas un cepillo leetcode. El tipo TS es solo un medio para implementar la lógica, la clave es la idea.

Puedes usar la recursividad para resolver este problema:

n personas están todas alineadas, hay n hoyos, y la columna completa es: el primer hoyo tiene n posibilidades, y todas las permutaciones son Permutación (n) = n * Permutación (n - 1)

Esto se hace en 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 logra la permutación completa

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

Operación:

Haga clic para expandir la respuesta >
// 自底向上,使用递归来循环
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]>; // 使用递归来循环
复制代码

Efecto:

// 斐波那契数列: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
复制代码

Supongo que te gusta

Origin juejin.im/post/7079025138913509406
Recomendado
Clasificación