以函数的方式来看typescript 类型运算
- 尖括号 <>
- TypeScript 类型运算符
- 1、extends
- 2、keyof
- 3、infer
- 4、in
- 题目示例
对于初接触typescript的前端开发者来说,我们可能会对typescript的类型运算感到很陌生和难以理解。现在想说的是,其实我们可以换另外一种方式来看这个问题,或许会感觉不一样。那就是可以尝试使用我们熟悉的函数来辅助理解。
尖括号 <>
如果用函数来做对比,尖括号<>可以看做是函数的小括号(),<a,b>,我们可以和函数的(a,b)做对比,也就是两个参数
TypeScript 类型运算符
当然,typescript类型运算毕竟不是javascript的函数,在这之前,我们得先对其运算符有个了解
1、extends
extends , 我们可以拿他来和javascript的instanceof 来做对比理解
比如 声明一个变量 var arr ,要判断arr是不是数组,可以使用 arr instanceof Array
同理,假设有个类型 TestType
type TestType = {
name:string,
age:number
}
想判断某个类型是不是 TestType类型,我们可以如下使用
type TestType = {
name:string,
age?:number
}
type testType1 = 1
type testType2 = {name:string}
type testType3 = {name:string, age:number}
type testType4 = {name:string, age:number, isChild: boolean}
type testType5 = { age:number}
type res1 = testType1 extends TestType ? number|string : boolean // boolean
type res2 = testType2 extends TestType ? number|string : boolean // number|string
type res3 = testType3 extends TestType ? number|string : boolean // number|string
type res4 = testType4 extends TestType ? number|string : boolean // number|string
type res5 = testType5 extends TestType ? number|string : boolean // boolean
小技巧:使用编辑器的提示可以直接看运算结果
2、keyof
获取 一个 类型的所有 key 的并返回生成的联合类型,类似javascript的Object.keys获取对象的键名一样
type TestType = {
name:string,
age?:number
}
interface TestKeyofType {
name: string,
value: number
}
type T1 = keyof TestType // "name" | "age"
type T2= keyof TestKeyofType // "name" | "value"
3、infer
用来在extends语句中,在true 的条件分支中,推断一个类型的变量
// 如果泛型T是{ name: infer R, age?: infer R }的子集,则返回infer R获取到的类型,否则返回boolean
// infer R 相当于推断出R的类型
type Func<T> = T extends { name: infer R, age?: infer R } ? R : boolean
type Func2<T> = T extends ()=> infer R ? R : boolean
type Func3<T> = T extends infer R ? R : boolean
let func1: Func<number> // boolean;
let func2: Func<""> // boolean
let func3: Func<() => Promise<number>> // boolean
let func4: Func<{ name:string, age:number, value: boolean }> // string|number
4、in
in 用来遍历类型的key,可以和for in 遍历对象做对比
题目示例
1、实现一个IsEqual工具类型,用于比较两个类型是否相等。具体的使用示例如下所示
type IsEqual<A, B> = // 你的实现代码
// 测试用例
type E0 = IsEqual<1, 2>; // false
type E1 = IsEqual<{ a: 1 }, { a: 1 }> // true
type E2 = IsEqual<[1], []>; // false
分析:如果这是一个函数,那就变成:实现一个IsEqual工具函数,用于比较两个值是否相等。实现
function isEqual(a, b) {
// return a === b ? true : false
return a === b
}
换成类型运算,那就判断a,b两个类型是否相等。使用extends
type IsEqual<A, B> = A extends B ? B extends A ? true : false : false
type IsEqual2<A, B> = [A, B] extends [B, A] ? true : false
type result = IsEqual<2, 3>
type result2 = IsEqual<3, 3>
type result3 = IsEqual<2, 3>
type result4 = IsEqual<3, 3>
2、 实现一个UnionToIntersection工具类型,用于把联合类型转换为交叉类型。具体的使用示例如下所示:
type UnionToIntersection<U> = // 你的实现代码
// 测试用例
type U0 = UnionToIntersection<string | number> // never
type U1 = UnionToIntersection<{ name: string } | { age: number }> // { name: string; } & { age: number; }
首先看下联合类型:即** string | number | boolean | {name:string,value:boolean}** 用 “|”,来连接的类型,可以看着取类型并集
然后看下交叉类型:及** string & number & boolean & {name:string,value:boolean}**用 “&”,来连接的类型,取类型交集
这个用函数比喻貌似进行不下去了。
直接先看下参考答案吧
export type UnionToIntersection<Union> = (
Union extends unknown ? (distributedUnion: Union) => void : never
) extends (mergedIntersection: infer Intersection) => void
? Intersection
: never;
这里涉及一个重要的知识点,逆变与协变。关于这个,本人目前理解也不透彻`,参考文章:https://zhuanlan.zhihu.com/p/454202284
尽管不能对逆变和协变做深入剖析,但是我们仍然先要记住下面两点:使用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
type Bar2<T> = T extends { a: infer U, b: infer U } ? U : never
type T201 = Bar2<{ a: string, b: string }> // string
type T211 = Bar2<{ a: string, b: number }> // string | number
这里剩下的疑问是 Union extends unknown 的作用。疑惑点在于 Union extends unknown 永远是true,但是我们如果换其他形式条件,结果却不一样。如下
export type UnionToIntersection<Union> = (Union extends unknown ? (distributedUnion: Union) => void : false) extends (mergedIntersection: infer Intersection) => void ? Intersection : false
export type UnionToIntersection2<Union> = ((distributedUnion: Union) => void) extends (mergedIntersection: infer Intersection) => void ? Intersection : false
export type UnionToIntersection3<Union> = (Union extends string ? (distributedUnion: Union) => void : false) extends (mergedIntersection: infer Intersection) => void ? Intersection : false
export type UnionToIntersection4<Union> = (1 extends number ? (distributedUnion: Union) => void : false) extends (mergedIntersection: infer Intersection) => void ? Intersection : false
type U0 = UnionToIntersection<string | number> // never
type U1 = UnionToIntersection<{ name: string } | { age: number }> // { name: string; } & { age: number; }
type U02 = UnionToIntersection2<string | number> // string | number
type U12 = UnionToIntersection2<{ name: string } | { age: number }> // { name: string; } & { age: number; }
type U03 = UnionToIntersection3<string | number> // false
type U13 = UnionToIntersection3<{ name: string } | { age: number }> // false
type U04 = UnionToIntersection4<string | number> // string | number
type U14 = UnionToIntersection4<{ name: string } | { age: number }> // { name: string; } & { age: number; }
type Test = (string | number) extends unknown ? true : false
type Tes3 = 1 extends number ? true : false
type Test2 = ({ name: string } | { age: number }) extends unknown ? true : false
Union extends unknown 的作用的左右需待后续查明
3 实现一个Merge工具类型,用于把两个类型合并成一个新的类型。第二种类型(SecondType)的Keys将会覆盖第一种类型(FirstType)的Keys。具体的使用示例如下所示:
type Foo = {
a: number;
b: string;
};
type Bar = {
b: number;
};
type Merge<FirstType, SecondType> = // 你的实现代码
const ab: Merge<Foo, Bar> = { a: 1, b: 2 };
这里我们倒是可以用对象的合并来类比一下,假设有如下对象
var foo = {
a:"number",
b:"string"
}
var bar = {
b:"number"
}
做合并如下:
var mergeObject = {
...foo,
...bar
} // {a:"number",b:"number"}
对于typescript来说,要实现合并,首先可以使用的基础方法就是交叉。交叉可以取到所有的类型键名,即Foo&Bar可以得到含有a,b键名的类型
第二、如果直接使用交叉Foo&Bar,我们得到的结果如下
type Foo = {
a: number;
b: string;
};
type Bar = {
b: number;
};
type MergeType = Foo & Bar
这样得到的MergeType类型和如下一致
type MergeType2 = {
a:number,
b:string&number
}
a没问题,是我们想要的,b由于即使string又是number,最终只会得到never。而我们的要求是后面覆盖前面,即b应该是number
第三、剩下的那也好理解了,遍历Foo&Bar的交叉类型,使用keyof 取得键名,先判断Bar里面有没有,有就取 Bar的值,没有就判断Foo有没有,有就取Foo的值
最终结果
type Merge<FirstType, SecondType> = {
[K in keyof (FirstType & SecondType)]: K extends keyof SecondType
? SecondType[K]
: K extends keyof FirstType
? FirstType[K]
: never;
};
4 实现一个RemoveIndexSignature工具类型,用于移除已有类型中的索引签名。具体的使用示例如下所示:
interface Foo {
[key: string]: any;
[key: number]: any;
bar(): void;
}
type RemoveIndexSignature<T> = // 你的实现代码
type FooWithOnlyBar = RemoveIndexSignature<Foo>; //{ bar: () => void; }
索引签名:用来解决无法预估的多余数据的一种方案,例如[k:string]:any
这个如果和对象类比,就是删除对应的属性
由于属性名未知,要删除我们就要用遍历,typescript中返回never就表示没有,也可以理解为被删除
使用 keyof获得键名的联合类型、用in遍历联合类型, 使用extends判断是否是字符串或者数字,是返回相应的类型,不是返回never
type RemoveIndexSignature<T> = {
[K in (keyof T) as number extends K ? never : string extends K ? never : K]: T[K];
};
使用as强转的原因,是最后面的:k报错:TS2313: Type parameter ‘k’ has a circular constraint.。如果改成如下
type keyType2<T> = {
[k in (keyof T) extends k ? never : "k"]: any
}
则不会报错。关于报 Type parameter ‘k’ has a circular constraint 的具体原因目前没查到,未知
5 实现RequireAllOrNone工具类型,用于满足以下功能。即当设置age属性时,gender属性也会变成必填。具体的使用示例如下所示:
interface Person {
name: string;
age?: number;
gender?: number;
}
type RequireAllOrNone<T, K extends keyof T> = // 你的实现代码
const p1: RequireAllOrNone<Person, 'age' | 'gender'> = {
name: "lolo"
};
const p2: RequireAllOrNone<Person, 'age' | 'gender'> = {
name: "lolo",
age: 7,
gender: 1
};
参考答案如下:
type RequireAllOrNone<T, K extends keyof T> = Omit<T, K> & (Required<Pick<T, K>> | Partial<Record<K, never>>)
1、了解工具函数 Omit,接收两个参数,第一个是要操作的类型,第二个是要删除的属性。可以理解为:删除某个类型的某个属性
假设这是一个对象删除属性的工具函数,那我们可如下实现
// pramNames: "a"|"b"|"c"
function omit(obj, pramNames){
const pramNamesArr = pramNames.split("|")
for(let i in obj){
if(pramNamesArr.includes(i)){
delete obj[i]
}
}
}
2、Pick:接收两个参数,第一个是要操作的类型,第二个是要提取出来的属性。可以理解为将第一个参数的部分类型(由第二个参数指定)的属性取出来,返回只含有这部分属性的一个新类型。
同样,假设这是一个js的工具函数,我们也可以很快就实现这个工具
// pramNames: "a"|"b"|"c"
function pick(obj, pramNames){
const pramNamesArr = pramNames.split("|")
const newObj = {}
for(let i in obj){
if(pramNamesArr.includes(i)){
newObj[i] = obj[i]
}
}
return newObj
}
3、Required 可选转必选
4、Partial 必选转可选
5、Record 接收两个参数,第一个参数是类型,第二个参数是要转化的类型值。作用就是将第一个参数的所有类型值转化成第二个参数指定的类型值
我们来看第一步:Omit<T, K> ,先将要处理的类型全部删除
interface Person {
name: string;
age?: number;
gender?: number;
}
type RequireAllOrNone<T, K extends keyof T> = Omit<T, K>
const p1: RequireAllOrNone<Person, "age" | "gender"> = {
name: "lolo",
}
const p1: RequireAllOrNone<Person, "age" | "gender"> = {
name: "lolo",
age: 1
}
这段代码p1正常,p2报错。英文age,gender属性已经被删除
得到的类型结果是
interface Person1 {
name: string;
}
第二步:Required<Pick<T, K>>
interface Person {
name: string;
age?: number;
gender?: number;
}
type RequireAllOrNone2<T, K extends keyof T> = Required<Pick<T, K>>
const p3: RequireAllOrNone2<Person, "age" | "gender"> = {
name: "lolo",
age: 1
}
const p4: RequireAllOrNone2<Person, "age" | "gender"> = {
age: 1,
gender: 18
}
上面是将传进来的"age"和"gender"挑出来做一个新的类型,且全部为必选。所以最终的类型没有name了
得到的类型结果是
interface Person2 {
age: number;
gender: number;
}
看 第三步 , Partial<Record<K, never>>
interface Person {
name: string;
age?: number;
gender?: number;
}
type RequireAllOrNone3<T, K extends keyof T> = Partial<Record<K, never>>
const p5: RequireAllOrNone3<Person, "age" | "gender"> = {
name: "lolo",
age: 1
}
const p6: RequireAllOrNone3<Person, "age" | "gender"> = {
age: undefined,
gender: undefined
}
const p7: RequireAllOrNone3<Person, "age" | "gender"> = {
name: "lolo",
age: undefined,
gender: undefined
}
上面p5、p7报错,p6正常,因为最终得出的类型是
interface Person3 {
age?: undefined;
gender?: undefined;
}
最后组合一块
type Persons = Person1 & (Person2 | Person3)
这里实现有age就有gender,用到的是联合类型的匹配逻辑,及如果age给到数字,就好自动匹配到Person2的类型,这时候就要求有gender,如果没有age,那就是undefinde,自动匹配Person3