du blog
Hello, welcome to my blog
TS挑战通关技巧总结,助你打通TS奇经八脉
created: Aug 30 21updated: Aug 30 21

接上一篇通关集合

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

T[number]、T['length']

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

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

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

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

对于数组来说

1type A = boolean[] 2 3type C = A['length'] // number 4type B = A[number] // boolean

as const

一种特殊的断言语法,

1// a:hello 2let a = 'hello' as const 3 4// b:number 5let b = 'hello' 6 7/* 8 c: { 9 readonly name: "du"; 10 } 11*/ 12let c = { 13 name: 'du', 14} as const 15 16/* 17 d: { 18 name: string; 19 } 20*/ 21let d = { 22 name: 'du', 23} 24 25// e: readonly [1, "1"] 26let e = [1, '1'] as const 27 28// d: (string | number)[] 29let 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的超类

协变:

1type A = { name: string; age: number } 2type B = { name: string } 3let a: Array<A> 4 5let b: Array<B> 6 7b = a 8/* 9 不能将类型“B[]”分配给类型“A[]”。 10类型 "B" 中缺少属性 "age",但类型 "A" 中需要该属性 11 */ 12a = b 13

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

1// strictFunctionTypes: false 2// 函数参数可以是超类 或者 子类 3interface Animal { 4 isAnimal: true 5} 6 7interface Dog extends Animal { 8 isDog: true 9} 10 11interface Greyhound extends Dog { 12 color: 'grey' 13} 14 15let a: (x: Dog) => void 16 17let b: (x: Greyhound) => void 18let c: (x: Animal) => void 19 20a = b //ok 21a = c //ok
1// strictFunctionTypes: true 2// 函数参数是逆变位置,也就是说需要赋值超类(当然自身也可以) 3interface Animal { 4 isAnimal: true 5} 6 7interface Dog extends Animal { 8 isDog: true 9} 10 11interface Greyhound extends Dog { 12 color: 'grey' 13} 14 15let a: (x: Dog) => void 16 17let b: (x: Greyhound) => void 18let c: (x: Animal) => void 19 20/* 21不能将类型“(x: Greyhound) => void”分配给类型“(x: Dog) => void”。 22 参数“x”和“x” 的类型不兼容。 23 类型 "Dog" 中缺少属性 "color",但类型 "Greyhound" 中需要该属性 24*/ 25a = b 26 27a = c //ok

注意:

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

这是ts2.4增加的策略

1// strictFunctionTypes:false 2interface Animal { 3 isAnimal: true 4} 5 6interface Dog extends Animal { 7 isDog: true 8} 9 10interface Greyhound extends Dog { 11 color: 'grey' 12} 13declare let a: (f: (x: Dog) => void) => void 14declare let b: (f: (x: Animal) => void) => void 15declare let c: (f: (x: Greyhound) => void) => void 16// 类型 "Animal" 中缺少属性 "isDog",但类型 "Dog" 中需要该属性 17a = b 18a = c //ok

Union to Intersection

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

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

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

infer

参考文档

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

最简单的例子就是 RetruenType

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

infer 处于协变位置

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

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

infer 的逆变

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

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

可变元组

元组类型扩展泛型

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

1// 可变的元组元素 2 3type Foo<T extends unknown[]> = [string, ...T, number]; 4 5type T1 = Foo<[boolean]>; // [string, boolean, number] 6type T2 = Foo<[number, number]>; // [string, number, number, number] 7type T3 = Foo<[]>; // [string, number] 8 9// 强类型的元组连接 10 11function concat<T extends unknown[], U extends unknown[]>(t: [...T], u: [...U]): [...T, ...U] { 12 return [...t, ...u]; 13} 14 15const ns = [0, 1, 2, 3]; // number[] 16 17const t1 = concat([1, 2], ['hello']); // [number, number, string] 18const t2 = concat([true], t1); // [boolean, number, number, string] 19const t3 = concat([true], ns); // [boolean, ...number[]] 20 21// 推断元组类型 22 23declare function foo<T extends string[], U>(...args: [...T, () => void]): T; 24 25foo(() => {}); // [] 26foo('hello', 'world', () => {}); // ["hello", "world"] 27foo('hello', 42, () => {}); // Error, number not assignable to string 28 29// 推断元组复合类型 30 31function curry<T extends unknown[], U extends unknown[], R>(f: (...args: [...T, ...U]) => R, ...a: T) { 32 return (...b: U) => f(...a, ...b); 33} 34 35const fn1 = (a: number, b: string, c: boolean, d: string[]) => 0; 36 37const c0 = curry(fn1); // (a: number, b: string, c: boolean, d: string[]) => number 38const c1 = curry(fn1, 1); // (b: string, c: boolean, d: string[]) => number 39const c2 = curry(fn1, 1, 'abc'); // (c: boolean, d: string[]) => number 40const c3 = curry(fn1, 1, 'abc', true); // (d: string[]) => number 41const c4 = curry(fn1, 1, 'abc', true, ['x', 'y']); // () => number 42

任意位置的reset元素

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

1type Strings = [string, string] 2type Numbers = number[] 3//type Unbounded = [string, string, ...number[], boolean] 4type Unbounded = [...Strings, ...Numbers, boolean]

扩展

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

4.0.5:

1type Strings = [string, string]; 2type Numbers = number[] 3 4// [string, string, ...Array<number | boolean>] 5type Unbounded = [...Strings, ...Numbers, boolean];

4.2:

1type Strings = [string, string]; 2type Numbers = number[] 3 4// [string, string, ...number[], boolean] 5type 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] ? F : never

参考文档:

参考pr 4.0 可变元组

参考文档 4.0

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

参考文档 4.2

元组、数组、对象的 readonly

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

1type A = [string] 2type RA = Readonly<A> 3 4type B = string[] 5type RB = Readonly<B> 6 7type IsExtends<T, Y> = T extends Y ? true : false 8 9type AExtendsRA = IsExtends<A, RA> //true 10 11type RAExtendsA = IsExtends<RA, A> //false 12 13type BExtendsRA = IsExtends<B, RB> // true 14 15type RBExtendsB = IsExtends<RB, B> // false 16 17type C = { 18 name: string 19} 20type RC = Readonly<C> 21type CExtendsRC = IsExtends<C, RC> // true 22type RCExtendsC = IsExtends<RC, C> // true

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

文档

stackoverflow

数组和元组的只读:

文档

pr

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

1declare const a: <T extends readonly any[]>(x: readonly [...T]) => T 2 3// const params: readonly [1, 2, 3, 4] 4const params = [1, 2, 3, 4] as const 5 6// const r: [1, 2, 3, 4] 7const r = a(params)

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

模板字符串

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

模板字符串与infer

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

1type MatchPair<S extends string> = S extends `[${infer A},${infer B}]` ? [A, B] : unknown; 2 3type T20 = MatchPair<'[1,2]'>; // ['1', '2'] 4type T21 = MatchPair<'[foo,bar]'>; // ['foo', 'bar'] 5type T22 = MatchPair<' [1,2]'>; // unknown 6type T23 = MatchPair<'[123]'>; // unknown 7type T24 = MatchPair<'[1,2,3,4]'>; // ['1', '2,3,4'] 8 9type FirstTwoAndRest<S extends string> = S extends `${infer A}${infer B}${infer R}` ? [`${A}${B}`, R] : unknown; 10 11type T25 = FirstTwoAndRest<'abcde'>; // ['ab', 'cde'] 12type T26 = FirstTwoAndRest<'ab'>; // ['ab', ''] 13type T27 = FirstTwoAndRest<'a'>; // unknown

规则如下:

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

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

参考pr

映射类型的深入理解

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

索引查询 keyof

1interface Person { 2 name: string; 3 age: number; 4 location: string; 5} 6 7let propName: keyof Person;

相当于

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

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

索引访问

1interface Person { 2 name: string; 3 age: number; 4 location: string; 5} 6 7let a: Person["age"];

相当于

1let a: number;

映射类型

转换 Person 属性全为 boolean

1interface Person { 2 name: string; 3 age: number; 4 location: string; 5} 6type BooleanifiedPerson = { 7 [P in keyof Person]: boolean 8};

相当于

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

拓展

我们可以实现一些变种

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

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

相当于

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

Pick

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

相当于

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

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

参考文档

键重新映射

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

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

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

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

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

参考pr

分配式

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

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

参考文档

映射类型也是分配式的

1type Map<T> = { 2 [P in keyof T]: T[P] 3} 4type A = { 5 name: string 6} 7type B = { 8 age: number 9} 10// type C = Map<A> | Map<B> 11type C = Map<A | B>

判断是否为同一类型

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

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

1type Equal<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false 2 3type T0 = Equal<string, number> //false 4type T1 = Equal<string | number, number> // false 5type T2 = Equal<{ name: string }, { name: string; age: number }> // false 6type T2 = Equal<{ name: string }, { name?: string }> // false

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

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

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

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

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

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

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

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

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

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

参考:https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650

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

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

1// https://github.com/type-challenges/type-challenges/issues/737 2type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends ( 3 x: infer U 4) => any 5 ? U 6 : never 7 8// get last Union: LastUnion<1|2> => 2 9// ((x: A) => any) & ((x: B) => any) is overloaded function then Conditional types are inferred only from the last overload 10// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types 11type LastUnion<T> = UnionToIntersection< 12 T extends any ? (x: T) => any : never 13> extends (x: infer L) => any 14 ? L 15 : never 16 17// type A = 2 18type A = LastUnion<1 | 2>

思路其实也很简单:

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

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

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

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

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

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

转载请注明作者及出处!