在第一次使用ts的时候,是自己做的一个小东西。那时候ts使用的很少(至少在我的身边)。然而现在学习 TypeScript 的小伙伴越来越多了。现在越来越多的公司使用ts,已经成为了一种标配。同时发现自己的ts水平实在欠佳,经过一段时间的学习,做出总结,如果不对的地方,欢迎指出。
起始
| TypeScript | JavaScript |
|---|---|
| JavaScript 的超集用于解决大型项目的代码复杂性 | 一种脚本语言,用于创建动态网页 |
| 可以在编译期间发现并纠正错误 | 作为一种解释型语言,只能在运行时发现错误 |
| 强类型,支持静态和动态类型 | 弱类型,没有静态类型选项 |
| 最终被编译成 JavaScript 代码,使浏览器可以理解 | 可以直接在浏览器中使用 |
| 支持模块、泛型和接口 | 不支持模块,泛型或接口 |
最近的一些学习,彻底搞懂了ts的高级类型,和一些自己独有的语法。在项目中,你肯定遇到过一个接口有很多属性,必填非必填有很多。这时候,你想某一项或者几项变成非必填或者必填就很麻烦。在ts的高级类型中没有一个可以做到这件事情的类型,可能会重写这样的一个新的接口。不过当彻底搞懂ts高级类型之后,就可以自己写出来这样的一个高级类型;
type PortionPartial<T, K extends keyof T> = Omit<T, K> & {
[P in K]?: T[P]
};
type RequiredPartial<T, K extends keyof T> = Omit<T, K> & {
[P in K]-?: T[P]
};
PortionPartial可以将T接口中的k属性变为可选,可以一次传入多个属性。 RequiredPartial可以将T接口中的k属性变为必选,可以一次传入多个属性。
这就解决了之前说过的问题,可以在编辑器中尝试使用,可以达到你想要的效果。如果你的ts功底不好,那可能看起来有点吃力。但是当你看完这篇文章,就可以轻松自己写出想要的高级类型。
正文
ts有很多高级类型,比如Omit<T, K extends keyof any>可以将T接口中的K属性剔除,返回一个新的类型。Pick<T, K extends keyof T>可以将T接口中的T属性拿出返回一个新的类型......
ts的所有的高级类型都离不开泛型的支持,可以在编辑器中看到,所有的高级类型都有泛型的身影。
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候指定类型的一种特性。
泛型是一个很重要的知识概念,这篇文章主要讲解ts的高级类型,所以泛型不会的同学可以自行搜索,网上对于泛型的文章还是很多的。
语法in extends infer...
在高级类型的解析之前,首先有一些ts的语法需要了解,比如in、extends、infer等等,这些东西在平常项目中可能用处比较少,但是在一些源码库和ts高级类型中却是必不可少的。
typeof
const people = {
name: "张三",
age: 33
}
type People = typeof people;
//People = {name: string;age: number;}
将一个属性转换为和这个属性相对应的类型。
keyof
type KeyofType = keyof People;
// KeyofType = "name" | "age"
keyof可以将一个接口中的属性名全部取出组成一个具体的字符串对象。
在使用keyof时,any是一个特殊的存在。any被keyof解释之后是string | number | symbol
type OneType<T> = T extends keyof any ? T : never;
// keyof any === type OneType = string | number | symbol
type OneResultType = OneType<boolean>
// never
extends
在泛型中extends不再是继承的意义,而是一种约束。
如果 T 不是一个联合类型,表示如果 T 是 U 的子集,那么返回 X 否则返回 Y。
export type TExtends<T, U> = T extends U ? number : never;
// T(number)是 U(number | string)的子集,所以返回number
type TExtendsExample1 = TExtends<number, number | string>; // number
// T(boolean) 不是 U(number | string)的子集,所以返回never
type TExtendsExample2 = TExtends<boolean, number | string>; // never
如果 T 是一个联合类型,表示如果 T 中的类型是 U 的子集,那么返回 X 否则返回 Y。这个过程可以理解为对T中的类型进行一次遍历,每个类型都执行一次 extends。
type NonNullable<T> = T extends null | undefined ? never : T;
// T(number | string) 不是 U(null | undefined) 的子集,所以返回 T
type TNonNullableExample1 = NonNullable<number | string>; // number | string
// T(string | null) 中 string 不是 U 的子集返回 string,null 是 U 的子集,返回 never
type TNonNullableExample2 = NonNullable<string | null>; // string
infer
infer 推导类型 通常与extends配合使用出现在三种地方
1 出现在extends条件语句后的函数类型的参数类型位置上
2 出现在extends条件语句后的函数类型的返回值类型位置上
3 出现在类型的泛型的具体化类型上
type fn = (param: number) => string
type fn2 = (param: number, param2: string) => string
type InferType<T> = T extends (param: infer P) => unknown ? P : T
// 在类型InferType中会判断传入的类型是不是接受一个属性,返回一个值得函数,如果成立则返回这个函数属性的类型,否则返回传入的T类型
// 传入fn:因为与定义的(param: infer P) => unknown类型匹配,则p代表推导出的fn参数的类型,进行返回则得到number类型
// 传入fn2:因为fn2需要的参数比InferType中需要的参数更多则约束不成立,得到传入的fn2类型
//(在extends中 函数的情况下 如果前置的函数类型参数比后置的函数类型参数少,其他条件相同的情况下成立)
type InferTypeTwo<T> = T extends (param: number) => infer P ? P : T
// 传入fn:类型匹配成立,则p代表推导出的fn的返回值的类型,进行返回则得到string类型
// 传入fn2:类型匹配不成立,得到传入的fn2类型
在类型InferType中会判断传入的类型是不是接受一个属性,返回一个值的函数,如果成立则返回这个函数属性的类型,否则返回传入的T类型
in
在泛型中可以遍历一个联合属性或者接口,遍历接口时会将接口中的值全部拿出。
type CreateInterfaceType<K extends keyof any, T> = {
[P in K]: T;
};
type ObjType = CreateInterfaceType<"name" | "age", boolean>;
// {name: boolean;age: boolean;}
高级类型
Extract
type Extract<T, U> = T extends U ? T : never;
type Type1 = Extract<string, object>
// never
type Type2 = Extract<"string" | "number" | "name" | "age", "number" | "string" | "boolean" | "name">
// "string" | "number" | "name"
type Type3 = Extracts<{username: string, username2: string}, {username: string, username2: string, username3: string}>
// never
将传入的T类型中在U类型同样存在的属性取出,在没有匹配项时返回never。
Exclude
作为和Extract相反的存在
type Exclude<T, U> = T extends U ? never : T;
type Type4 = Exclude<"string" | "number" | "name" | "age", "number" | "string" | "boolean">
// "name" | "age"
将传入的T类型中在U类型不存在的属性取出,若全部匹配则返回never。
Record
type Record<K extends keyof any, T> = {
[P in K]: T;
};
K必须是string | number | symbol中的一个,返回一个接口,属性名将K遍历得到,T作为属性值出现。 在之前肯定有人和我一样定义一个obj的时候
intefeace Obj {
[key: string]: string
}
以后就可以直接写Record<string, string>达到一样的结果。
在定义式
Record<number, string>可以完全匹配给Record<string, string>,反之不成立。
Partial
type Partial<T> = {
[P in keyof T]?: T[P];
};
将传入的泛型接口中所有属性值定义为非必填。
Required
type Required<T> = {
[P in keyof T]-?: T[P];
};
将传入的泛型接口中所有的属性定义为必填。
这里用的是-?去掉非必填的?,并非重新定义为
[P in keyof T]-?: T[P]。否则会失去效果。
例如
type RequiredTest<T> = {
[P in keyof T]: T[P];
};
// 这样返回的类型会毫无改变,因为比如{username?: string}在非必填属性取到的属性是string | undefined;
type TestA = RequiredTest<{name: string, age?: number}>
// 得到的类型并非我们想要的,而是{name: string;age?: number | undefined;}
ReadOnly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type TestA = Readonly<{name: string}>
const testA: TestA = {
name: "name",
}
testA.name = "asdf"// 报错:无法分配到 "name" ,因为它是只读属性。
将属性变为只读从而无法修改。其实在属性初始化时,可以将其使用强转符as强转为const也可以达到一样的效果
const testB = {
name: "name"
} as const;
testB.name = "name2"// 报错:无法分配到 "name" ,因为它是只读属性。
pick
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
将K遍历a构成一个新的接口,将T中对应的属性拿出。作为复杂的高级类型Omit的一个成员
Omit
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
interface Test {
name?: string;
age?: string;
six?: string
}
type OmitTest = Omit<Test, "name">
//{age?: string;six?: string;}
omit作为一个比较复杂高级类型,Exclude的功能是将第一个联合类型遍历,剔除第二个属性上同时存在的联合类型,得到了"age"|"six",之后使用pick遍历得到的联合类型,将T接口中存在属性取出返回新的接口。
结束
ts的语法与高级类型远不止文章中提高的这一步,到这里的时候,剩下的高级类型解读已经没有任何压力。ts作为一门静态语言,很好的在代码的编译过程帮你找出粗心导致的问题。相信未来ts会被越来越多的应用,vue3源码已经全面使用,在ts的功底之上,阅读源码会方便很多。
最后!
再放一次文章开始的两个自己写的高级类型,有用!!!
type PortionPartial<T, K extends keyof T> = Omit<T, K> & {
[P in K]?: T[P]
};
type RequiredPartial<T, K extends keyof T> = Omit<T, K> & {
[P in K]-?: T[P]
};