TS挑战通关技巧总结,助你打通TS奇经八脉

3,997 阅读10分钟

接上一篇通关集合

这一次我们来总结一下题目中通用又难以理解的点:

T[number]、T['length']

T[number] 用来获取元组的元素类型联合

T['length'] 用来获取元组的元素类型联合

元组类型是另一种Array类型,它确切地知道它包含多少元素,以及在特定位置包含哪些类型。

type A = ['a', 'b', 'c']

type C = A['length'] // 3
type B = A[number] // "a" | "b" | "c"

对于数组来说

type A = boolean[]

type C = A['length'] // number
type B = A[number] // boolean

as const

一种特殊的断言语法,

// a:hello
let a = 'hello' as const

// b:number
let b = 'hello'

/*
  c: {
    readonly name: "du";
  }
*/
let c = {
  name: 'du',
} as const

/* 
  d: {
      name: string;
  }
*/
let d = {
  name: 'du',
}

// e: readonly [1, "1"]
let e = [1, '1'] as const

// d: (string | number)[]
let d = [1, '1']

当使用 const assert 时,ts做了以下几件事

  1. 该表达式中的任何文字类型都不应该被扩展(例如,不应该从“hello”变成字符串)

  2. 对象字面值获得只读属性

  3. 数组字面值变成只读元组

参考文档

协变与逆变

建议阅读:

  1. 类型系统中的协变与逆变

  2. 类型兼容性

整体来说,TypeScript中的类型兼容性是基于结构子类型的。结构类型是一种仅根据类型的成员来关联类型的方法

即:

let a: { name: string; age: number }

let b: { name: string }

b = a
// error: 类型 "{ name: string; }" 中缺少属性 "age",但类型 "{ name: string; age: number; }" 中需要该属性
a = b

ab更加具体,ba更加宽泛,即 ab的子类,ba的超类

协变:

type A = { name: string; age: number }
type B = { name: string }
let a: Array<A>

let b: Array<B>

b = a
/*
 不能将类型“B[]”分配给类型“A[]”。
类型 "B" 中缺少属性 "age",但类型 "A" 中需要该属性
  */
a = b

Array 是不可变的,所以类型还是安全的, 因为 B=A 可以,所以Array<B>=Array<A> 可以

逆变: 上边说到安全的是协变,那么不安全呢,如:

type A = { name: string; age: number }
type C = { name: string; age: number; say(): void }

let a: (x: A) => void = (person) => {
  console.log(person)
}

let b: (x: C) => void = (person) => {
  person.say()
}
// a: (x: A) => void
a = b
a({ name: '1', age: 1 })

注意:设置strictFunctionTypes 为false,关闭逆变检查可以跑起来这段代码

运行这段代码,报错:person.say is not a function

此时关闭了逆变检查,a函数接收的类型为AA类型没有声明具有say 方法,

那么逆变检查就是:函数类型赋值时,函数参数为逆变位置,只能被赋予当前类型或者当前类型的超类

如:

type A = { name: string; age: number }
type D = { name: string }
let a: (x: A) => void = (person) => {
  console.log(person)
}

let b: (x: D) => void = (person) => {
  console.log(person.name)
}
// a: (x: A) => void
a = b
a({ name: '1', age: 1 })

此时 a 函数接收的参数 AD 的子类,即 AD 更加具体,那么他必然是安全的

  1. ts 2.6 从双变改为逆变

这里说明一下,双变指的是 可以是超类或者子类,例如,我这里把 strictFunctionTypes 设置为 false

// strictFunctionTypes: false
// 函数参数可以是超类 或者 子类
interface Animal {
  isAnimal: true
}

interface Dog extends Animal {
  isDog: true
}

interface Greyhound extends Dog {
  color: 'grey'
}

let a: (x: Dog) => void

let b: (x: Greyhound) => void
let c: (x: Animal) => void

a = b  //ok
a = c  //ok
// strictFunctionTypes: true
// 函数参数是逆变位置,也就是说需要赋值超类(当然自身也可以)
interface Animal {
  isAnimal: true
}

interface Dog extends Animal {
  isDog: true
}

interface Greyhound extends Dog {
  color: 'grey'
}

let a: (x: Dog) => void

let b: (x: Greyhound) => void
let c: (x: Animal) => void

/*
不能将类型“(x: Greyhound) => void”分配给类型“(x: Dog) => void”。
  参数“x”和“x” 的类型不兼容。
    类型 "Dog" 中缺少属性 "color",但类型 "Greyhound" 中需要该属性
*/
a = b

a = c //ok

注意:

  1. 当函数为方法时,执行的还是双变检查
interface Comparer<T> {
  compare: (a: T, b: T) => number;
}

declare let animalComparer: Comparer<Animal>;

declare let dogComparer: Comparer<Dog>;
// 逆变检查,因为 T 用于函数参数位置
animalComparer = dogComparer; // Error

dogComparer = animalComparer; // Ok
interface Comparer<T> {
  compare(a: T, b: T): number;
}
declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;
// 双变检查 因为 T 用于方法参数位置
animalComparer = dogComparer; // Ok because of bivariance
dogComparer = animalComparer; // Ok
  1. 回调函数强制为逆变检查 回调函数强制为逆变检查,即使strictFunctionTypes设置为false也是不行的

这是ts2.4增加的策略

// strictFunctionTypes:false
interface Animal {
  isAnimal: true
}

interface Dog extends Animal {
  isDog: true
}

interface Greyhound extends Dog {
  color: 'grey'
}
declare let a: (f: (x: Dog) => void) => void
declare let b: (f: (x: Animal) => void) => void
declare let c: (f: (x: Greyhound) => void) => void
// 类型 "Animal" 中缺少属性 "isDog",但类型 "Dog" 中需要该属性
a = b
a = c //ok

Union to Intersection

联合类型转交叉类型算是逆变最常见的应用了

type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (
  x: infer R
) => void
  ? R
  : never
  1. 利用 U extends any ? (x: U) => void : never 构造分布式的(x: U1) => void | (x: U2) => void

  2. 利用函数参数为逆变位置得到交叉类型

infer

参考文档

infer 是一种可以在 extends 条件语句中声明 存储推断类型,然后在 真 分支中使用的语句

最简单的例子就是 RetruenType

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

infer 处于协变位置

infer处于协变位置时,推断出联合类型

type Foo<T> = T extends { a: infer U; b: infer U } ? U : never
type T10 = Foo<{ a: string; b: string }> // string
type T11 = Foo<{ a: string; b: number }> // string | number

infer 的逆变

infer处于逆变位置时,推断出交叉类型

type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
  ? U
  : never
type T20 = Bar<{ a: (x: string) => void; b: (x: string) => void }> // string
type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }> // string & number => never

可变元组

元组类型扩展泛型

元组类型能够扩展泛型类型,通过类型实例化可以用实际元素替换

// 可变的元组元素

type Foo<T extends unknown[]> = [string, ...T, number];

type T1 = Foo<[boolean]>;  // [string, boolean, number]
type T2 = Foo<[number, number]>;  // [string, number, number, number]
type T3 = Foo<[]>;  // [string, number]

// 强类型的元组连接

function concat<T extends unknown[], U extends unknown[]>(t: [...T], u: [...U]): [...T, ...U] {
    return [...t, ...u];
}

const ns = [0, 1, 2, 3];  // number[]

const t1 = concat([1, 2], ['hello']);  // [number, number, string]
const t2 = concat([true], t1);  // [boolean, number, number, string]
const t3 = concat([true], ns);  // [boolean, ...number[]]

// 推断元组类型

declare function foo<T extends string[], U>(...args: [...T, () => void]): T;

foo(() => {});  // []
foo('hello', 'world', () => {});  // ["hello", "world"]
foo('hello', 42, () => {});  // Error, number not assignable to string

// 推断元组复合类型

function curry<T extends unknown[], U extends unknown[], R>(f: (...args: [...T, ...U]) => R, ...a: T) {
    return (...b: U) => f(...a, ...b);
}

const fn1 = (a: number, b: string, c: boolean, d: string[]) => 0;

const c0 = curry(fn1);  // (a: number, b: string, c: boolean, d: string[]) => number
const c1 = curry(fn1, 1);  // (b: string, c: boolean, d: string[]) => number
const c2 = curry(fn1, 1, 'abc');  // (c: boolean, d: string[]) => number
const c3 = curry(fn1, 1, 'abc', true);  // (d: string[]) => number
const c4 = curry(fn1, 1, 'abc', true, ['x', 'y']);  // () => number

任意位置的reset元素

rest 元素可以出现在元组中的任何地方——不仅仅是在最后!

type Strings = [string, string]
type Numbers = number[]
//type Unbounded = [string, string, ...number[], boolean]
type Unbounded = [...Strings, ...Numbers, boolean]

扩展

注意区分的是 元组的reset,是在4.0版本就支持任意位置,但是array的reset是在4.2才支持任意位置的reset

4.0.5:

type Strings = [string, string];
type Numbers = number[]

// [string, string, ...Array<number | boolean>]
type Unbounded = [...Strings, ...Numbers, boolean];

4.2:

type Strings = [string, string];
type Numbers = number[]

// [string, string, ...number[], boolean]
type Unbounded = [...Strings, ...Numbers, boolean];

infer 与 元组

常见的写法

// 获取元组第一个元素
type First<T extends any[]> = T extends [infer F, ...infer R] ? F : never

// 获取元组最后一个元素
type Last<T extends any[]> = T extends [...infer F, infer R] ? R : never

参考文档:

参考pr 4.0 可变元组

参考文档 4.0

参考 pr 4.2 元组类型中的前导和中间rest元素

参考文档 4.2

元组、数组、对象的 readonly

数组,元组加上 readonly 为普通形式父集,对象属性的 redonly 不影响类型兼容

type A = [string]
type RA = Readonly<A>

type B = string[]
type RB = Readonly<B>

type IsExtends<T, Y> = T extends Y ? true : false

type AExtendsRA = IsExtends<A, RA> //true

type RAExtendsA = IsExtends<RA, A> //false

type BExtendsRA = IsExtends<B, RB> // true

type RBExtendsB = IsExtends<RB, B> // false

type C = {
  name: string
}
type RC = Readonly<C>
type CExtendsRC = IsExtends<C, RC> // true
type RCExtendsC = IsExtends<RC, C> // true

对象只读属性不影响类型兼容:

文档

stackoverflow

数组和元组的只读:

文档

pr

只读元组泛型去掉只读属性

declare const a: <T extends readonly any[]>(x: readonly [...T]) => T

// const params: readonly [1, 2, 3, 4]
const params = [1, 2, 3, 4] as const

// const r: [1, 2, 3, 4]
const r = a(params)

这点么有找到相关资料,只是在做体操的时候发现的

模板字符串

type EventName<T extends string> = `${T}Changed`;
type Concat<S1 extends string, S2 extends string> = `${S1}${S2}`;
type ToString<T extends string | number | boolean | bigint> = `${T}`;
type T0 = EventName<'foo'>;  // 'fooChanged'
type T1 = EventName<'foo' | 'bar' | 'baz'>;  // 'fooChanged' | 'barChanged' | 'bazChanged'
type T2 = Concat<'Hello', 'World'>;  // 'HelloWorld'
type T3 = `${'top' | 'bottom'}-${'left' | 'right'}`;  // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
type T4 = ToString<'abc' | 42 | true | -1234n>;  // 'abc' | '42' | 'true' | '-1234'

模板字符串与infer

在做体操过程中真正用到的还是与条件语句结合,使用infer获取源字符串中自己需要的部分

type MatchPair<S extends string> = S extends `[${infer A},${infer B}]` ? [A, B] : unknown;

type T20 = MatchPair<'[1,2]'>;  // ['1', '2']
type T21 = MatchPair<'[foo,bar]'>;  // ['foo', 'bar']
type T22 = MatchPair<' [1,2]'>;  // unknown
type T23 = MatchPair<'[123]'>;  // unknown
type T24 = MatchPair<'[1,2,3,4]'>;  // ['1', '2,3,4']

type FirstTwoAndRest<S extends string> = S extends `${infer A}${infer B}${infer R}` ? [`${A}${B}`, R] : unknown;

type T25 = FirstTwoAndRest<'abcde'>;  // ['ab', 'cde']
type T26 = FirstTwoAndRest<'ab'>;  // ['ab', '']
type T27 = FirstTwoAndRest<'a'>;  // unknown

规则如下:

一个infer占位符后面是一个字面字符跨度,通过推断来源中的零个或多个字符进行匹配,直到该字面字符跨度在来源中第一次出现

一个infer占位符后边紧跟另一个infer占位符则第一个占位符匹配源字符串中的一个字符

参考pr

映射类型的深入理解

要理解映射类型首先要了解索引查询,它建立在索引查询之上,

索引查询 keyof

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

let propName: keyof Person;

相当于

let propName: "name" | "age" | "location";

可以理解为 对象类型的键查询

索引访问

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

let a: Person["age"];

相当于

let a: number;

映射类型

转换 Person 属性全为 boolean

interface Person {
    name: string;
    age: number;
    location: string;
}
type BooleanifiedPerson = {
    [P in keyof Person]: boolean
};

相当于

type BooleanifiedPerson = {
    [P in "name" | "age" | "location"]: boolean
};

拓展

我们可以实现一些变种

将元组转换一个value为true的对象

type TransformTuPle<T extends any[]> = {
  [K in T[number]]: true
}
type A = TransformTuPle<['Kars', 'Esidisi', 'Wamuu', 'Santana']>

相当于

type TransformTuPle<T extends any[]> = {
  [K in "Kars" | "Esidisi" | "Wamuu" | "Santana"]: true
}

Pick

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
type C = Pick<{ name: string; age: number; sex: string }, 'name' | 'age'>

相当于

type Pick<T, 'name' | 'age' extends 'name' | 'age' | 'sex'> = {
    [P in 'name' | 'age']: T[P];
};

注意 K extends keyof T的约束条件是必须的

参考文档

键重新映射

映射类型支持可选的as子句,通过该子句可以指定生成的属性名

type Getters<T> = { [P in keyof T & string as `get${Capitalize<P>}`]: () => T[P] };
type T50 = Getters<{ foo: string, bar: number }>;  

最常见的场景大概就是剔除对象中的属性了:

as子句中指定的类型解析为never时,不会为该键生成任何属性。因此,as子句可以用作过滤器

type Methods<T> = { [P in keyof T as T[P] extends Function ? P : never]: T[P] };
type T60 = Methods<{ foo(): number, bar: boolean }>;  // { foo(): number }

参考pr

分布式

当条件类型作用于泛型类型时,并且泛型实例为联合类型时,它们会变成分布式的

type ToArray<Type> = Type extends any ? Type[] : never
// type StrArrOrNumArr = string[] | number[]
type StrArrOrNumArr = ToArray<string | number>

参考文档

映射类型也是分布式的

type Map<T> = {
  [P in keyof T]: T[P]
}
type A = {
  name: string
}
type B = {
  age: number
}
// type C = Map<A> | Map<B>
type C = Map<A | B>

判断是否为同一类型

有一种常见的情况就是根据是否为某种类型做一些操作,比如PickByType

这里就需要用到判断是否为同一类型

type Equal<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false

type T0 = Equal<string, number> //false
type T1 = Equal<string | number, number> // false
type T2 = Equal<{ name: string }, { name: string; age: number }> // false
type T2 = Equal<{ name: string }, { name?: string }> // false

但是还记得上文提到的 readonly不影响对象属性的类型兼容性

type Equal<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false
type T2 = Equal<{ name: string }, { readonly name: string }> // true

此时是判断不出来的,因为readonly不影响对象属性的类型兼容性

ps:(除了 readonly,any是所有类型的超类 和 子类(子类除了never),所以这里前提条件是排除 any, 至于排除any的方法,在挑战通关中有答案)

这里就需要用到另一种方法判断了

// https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650
type IfEquals<X, Y> = (<T>() => T extends X ? 1 : 2) extends <
  T
>() => T extends Y ? 1 : 2
  ? true
  : false

还要再再再再说一点,这种判断方法 不允许交集类型与具有相同属性的对象类型相同

// false
type A = IfEquals<{ x: 1 } & { y: 2 }, { x: 1; y: 2 }>

解决办法是在判断之前合并一下

type Merge<T> = {
  [P in keyof T]: T[P]
}
// true
type A = IfEquals<Merge<{ x: 1 } & { y: 2 }>, { x: 1; y: 2 }>

参考:github.com/Microsoft/T…

获取类联合类型最后一个类型元素

这里也算是一个有意思的技巧,话不多说直接上示例

// https://github.com/type-challenges/type-challenges/issues/737
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (
  x: infer U
) => any
  ? U
  : never

// get last Union: LastUnion<1|2> => 2
// ((x: A) => any) & ((x: B) => any) is overloaded function then Conditional types are inferred only from the last overload
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types
type LastUnion<T> = UnionToIntersection<
  T extends any ? (x: T) => any : never
> extends (x: infer L) => any
  ? L
  : never
 
// type A = 2
type A = LastUnion<1 | 2>

思路其实也很简单:

  • 这里先把把每个联合类型的元素转换为函数

    (x: T) => any | (x: T) => any | (x: T) => any

  • 利用分配模式,和函数的逆变性,把联合类型转换为 交集类型

    (x: T) => any & (x: T) => any & (x: T) => any

  • 再利用多签名类型(例如函数重载)进行条件推断时,将只从最后一个签名进行推断 参考文档

最后感谢你看到这里,相信你肯定也有不小的收获,喜欢的话可以给一个赞

转载请注明作者及出处!