接上一篇通关集合
这一次我们来总结一下题目中通用又难以理解的点:
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做了以下几件事
-
该表达式中的任何文字类型都不应该被扩展(例如,不应该从“hello”变成字符串)
-
对象字面值获得只读属性
-
数组字面值变成只读元组
协变与逆变
建议阅读:
整体来说,TypeScript中的类型兼容性是基于结构子类型的。结构类型是一种仅根据类型的成员来关联类型的方法
即:
let a: { name: string; age: number }
let b: { name: string }
b = a
// error: 类型 "{ name: string; }" 中缺少属性 "age",但类型 "{ name: string; age: number; }" 中需要该属性
a = b
a
比 b
更加具体,b
比a
更加宽泛,即 a
是b
的子类,b
是a
的超类
协变:
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
函数接收的类型为A
,A
类型没有声明具有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
函数接收的参数 A
是 D
的子类,即 A
比 D
更加具体,那么他必然是安全的
这里说明一下,双变指的是 可以是超类或者子类,例如,我这里把 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
注意:
- 当函数为方法时,执行的还是双变检查
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
- 回调函数强制为逆变检查
回调函数强制为逆变检查,即使
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
-
利用
U extends any ? (x: U) => void : never
构造分布式的(x: U1) => void | (x: U2) => void
-
利用函数参数为逆变位置得到交叉类型
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
参考文档:
元组、数组、对象的 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
对象只读属性不影响类型兼容:
数组和元组的只读:
只读元组泛型去掉只读属性
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
占位符则第一个占位符匹配源字符串中的一个字符
映射类型的深入理解
要理解映射类型首先要了解索引查询,它建立在索引查询之上,
索引查询 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 }
分布式
当条件类型作用于泛型类型时,并且泛型实例为联合类型时,它们会变成分布式的
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 }>
获取类联合类型最后一个类型元素
这里也算是一个有意思的技巧,话不多说直接上示例
// 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
-
再利用多签名类型(例如函数重载)进行条件推断时,将只从最后一个签名进行推断 参考文档
最后感谢你看到这里,相信你肯定也有不小的收获,喜欢的话可以给一个赞
转载请注明作者及出处!