TS泛型学习
函数泛型的初登场
在揭开函数泛型的面纱之前,首先粗略的关注一下在函数中使用类型:
function returnValue(value: string): string{
return value
}
定义一个函数,对其传入的参数和返回值的类型进行约束。在调用该函数时,传入的参数必须为string类型。
returnValue('111')
returnValue(111)//传入非string类型,TS编译时报错。
初步了解在TS中函数的定义之后,我们来会会函数泛型。我先写一个泛型函数的例子:
function returnValue<T>(value: T): T{
return value
}
此时returnValue就是一个泛型类型的函数,这里使用T表示泛型,泛型也是一种类型,我们可以认为是任意类型。其实更准确的说,T是一个泛型变量,他的值,由调用该函数时传入的类型决定。该函数的意义就是returnValue的参数value可以时任意类型,并且返回的的值的类型也是任意类型。
上述是函数泛型的声明,那么如何调用呢?调用该函数的方式有两种:
//方法一:
returnValue<string>('1111') //此时泛型变量的T就是string类型,因此需要传入字符串,如果传入数字,TS编译时报错,因为类型不匹配。
//方法二:
returnValue('1111')//这中方式是后续要讲到的类型推导,我们并没有直接给泛型变量T赋值,但是TS通过我们传入的值来推导出T的类型为string。
没错,有一个泛型变量就一定会有两个甚至多个。我们看两个泛型变量的使用:
function returnValue<T,U>(val_0: T, val_1: U):[T,U]{
return [val_0, val_1];
}
//方法调用
returnValue<string, number>('1',1)
当然也可以使用类型推导。
这里补充一个小姬希。如果定义一个函数类型的值呢?
type funcType = (value: string) => void
这里稍微讲解一下,通过type关键字来为(value: string)=> void类型取一个别名。这个类型是一个函数,该函数返回空,并且有一个value参数类型为string。这里没有参数也可以,函数类型的定义多种多样。主要是入参和返回值的类型需要定义。内部逻辑无需care。
接口泛型
在接口中,我们仍然可以使用泛型:
interface Person<T>{
name: string;
action: T
}
let hero: Person<string> = {
name: '中国人民解放军',
action:'保家卫国'
}
在声明Person接口时使用了T这个泛型变量,此时并不能确定Person中action的类型。只有在使用时传入了string,此时Person的类型其实才算确定下来。
当然,这只是简单的接口泛型,还有一些削微复杂一点的,例如:
interface typeA{
name: string;
}
interface typeB<T>{
name: string;
extends: T;
}
let type: typeB<typeA> = {
name: 'hahha',
extends: {
name:'xixi'
}
}
//当执行typeB<typeA>时,其实typeB的类型就是:
interface typeB{
name: string;
extends: {
name: string;
}
}
又来一个栗子加深映像:
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T { //定义一个使用泛型的函数
return arg;
}
let myIdentity: GenericIdentityFn<string> = identity; //具体使用
类泛型
其实类泛型和接口泛型有几分相似,彼时彼刻恰如此时此刻:
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();//具体的使用
let stringNumeric = new GenericNumber<string>();//这里将string传入
在使用类时,为泛型变量赋值。
泛型约束
泛型约束的本质是对任意类型进行约束,让它变成一个并非真正意义上的任意类型,泛型约束主要使用关键字extends。extends虽有继承作用,但和泛型搭配一起就不再是继承,而是约束,例如:
interface type1 {
name: string;
}
function returnValue<T extends type1 >(value: T): T{
return value;
}
我们首先定义接口type1。然后通过<T extends type1 >对泛型进行约束。它的意思是:我们可以向returnValue函数传入任意类型的值,但是这个值必须包含name属性,并且这个属性的类型是string。其实这变相的对T进行了约束。
在没有泛型约束之前,我们可以传入任意类型的值,例如string, number, boolean, Array<T>, object等等类型,但是一旦使用了type1进行约束,那么传入的值只能是一个对象,并且必须要有name属性。
extends的前世今生
刚才我们浅谈了extends在泛型上的作用,主要用于泛型约束。其实它的才能还不止这些。
- 必杀技一: 作用于接口,表示继承。继承就相当于自己有了被继承着的字段。
interface T1 {
name: string,
}
interface T2 {
sex: number,
}
/**
* T3 = {name: string, sex: number, age: number}
*/
interface T3 extends T1, T2 {
age: number,
}
注意,接口支持多重继承,语法为逗号隔开。如果是type实现继承,则可以使用交叉类型type A = B & C & D。
- 必杀技二: 表示条件类型,可用于条件判断。表示条件判断,如果前面的条件满足,则返回问号后的第一个参数,否则第二个。类似于js的三元运算。我们煮个例子:
//type A1 = 1
type A1 = 'x' extends 'x' ? 1 : 2;
//type A2 = 2
type A2 = 'x' | 'y' extends 'x' ? 1 : 2; //这里是一个联合类型,因此前者不能分配给后者。
// type A3 = 1 | 2
type P<T> = T extends 'x' ? 1 : 2;
type A3 = P<'x' | 'y'>
以上板栗,啊呸。以上栗子中extends的作用是进行条件判断。
类型兼容性
TypeScript结构化类型系统的基本原则是:如果x要兼容y,那么至少y具有和x具有相同的属性。
interface Named {
name: string;
}
let x: Named
let y = { name: 'alice', location: 'seattle' }
x = y
在上面例子中,在TS环境中y是否能赋值给x,TS编译器会检查x中的每一个属性,看是否能在y中也找到对应属性。 在这个例子中,y必须包含名字是name的string类型成员。y满足条件,因此赋值正确。
这里注意,其实不止需要具有相同的属性,属性的类型也需要相同。
y有个额外的location属性,但这不会引发错误。 只有目标类型的成员会被一一检查是否兼容。这个比较过程是递归进行的,检查每个成员及子成员。
父子类型 (可赋值性 或者 可分配性)
在TS中,我们如何判断一个类型是父类类型还是子类型。使用的方式是赋值。
如果一个变量需要一个 A 类型的值,如果我们可以用 B 类型的值赋予该变量,那么类型 B 就是类型 A 的子类型,类型 A 就是类型 B 的父类型。而赋值的原则就是上面x和y赋值的原理。
我们假设A:{name: string}, B: { name: string; location: string }
很显然,B可以赋值给A,因为当B赋值给A时,此时TS会遍历A中的每一个属性,看B中是否都有,都有那就是赋值成功,很显然都有,所以可以赋值。
但是A不能赋值给B,因为A的属性只有一个name不满足两个,所以不能赋值成功。此时A就是父类型,B是子类型。
协变
对于协变,我们来看一个栗子:
interface animation {
name: string;
}
interface dog1 extends animation {
action: string;
}
let AAA: Array<animation> = [{name:'aa'}];
let BBB: Array<dog1> = [{name:'dog',action:'eat'}]
AAA = BBB
我们定义两个类型animation 和 dog1,通过代码运行,发现赋值是成功的。
因此类型Array<animation>是类型Array<dog1>的父类型。
由此可见,类型 Array<animation> 和类型 Array<dog1> 保留了类型 animation 和类型 dog1 之间的父子类型关系,对于这种保留了父子类型关系的变型我们称之为协变。
逆变
简单说就是,具有父子关系的多个类型,在通过某种构造关系构造成的新的类型, 如果还具有父子关系则是协变的,而关系逆转了(子变父,父变子)就是逆变的 首先我们来看一个栗子:
interface animation {
name: string;
}
interface dog extends animation {
action: () => void;
}
type anim = (arg: animation) => void
type dogs = (arg: dog) => void
let Eg1: anim
let Eg2: dogs
Eg1 = Eg2 //这里赋值TS编译器报错,不允许赋值
Eg2 = Eg1 //赋值TS编译器允许
为什么Eg1 = Eg2会不允许?根本原因是因为它们对应的类型所造成的,我们根据两这的类型分别将两个变量赋值对应的函数,如下:
Eg1 = function (arg: animation) {
console.log('is called');
}
Eg2 = function (arg: dog) {
arg.action()
}
//与此同时,进行赋值:
Eg1 = Eg2
观察函数,会发现什么问题?当赋值之后,我们调用对应的函数:
let obj = {
name:'hh'
}
Eg1(obj) //函数调用错误,无对应action属性
在声明Eg1函数时,对应的参数类型已经确认,因此只能传入对应animation类型,但是函数体已经发生改变,因此会报错。
正是基于这种原因,TS编译器在赋值时会报错。因此只能Eg2 = Eg1,这样一来animation就成为了dogs的子类型,而这种通过type Fn<T> = (arg: T) => void构造器构造后,父子关系逆转的现象叫做逆变。
双向协变
所谓的双向协变指的是:如果类型 A 是类型 B 的子类型,经过变型后,如果类型 A 既是类型 B 的子类型,又是类型 B 的父类型(反之亦然),我们称这种变型为双向协变。我们在逆变小结中见过,在函数的参数中,只有逆变才能够保证类型安全。因此在TS的严格模式下,函数的参数类型时逆变的。
interface BaseEvent {
timestamp: number;
}
interface MyMouseEvent extends BaseEvent {
x: number;
y: number;
}
function addEventListener(handler: (n: BaseEvent) => void) {
}
addEventListener((e: MyMouseEvent) => {
}); // ts(2345) TS报错。
但类似上述事件处理的代码是我们经常遇到的场景,我们可以将TypeScript 设置为非严格模式(将 strictFunctionTypes 或 strict 设置为 false),此刻函数的参数类型便变成了双向协变,
但由于它不是类型安全的,故此不推荐使用,可通过泛型来保证类型安全,因此上诉代码中的 addEventListener 可修改为:
function addEventListener<E extends BaseEvent>(handler: (n: E) => void) { }
集合类
其实集合类也是泛型的一种,我们举个栗子:
let numberArr: Array<number> = [1,2,3];
其实上面的栗子就是集合类,本质上数组就是一系列值的集合,这些值可以是任意类型。数组只是一个容器,数组中的元素本质上是相同的类型。
在使用Array泛型时,需要指定内部元素类型,不要单独使用:
let arr: Array = [] //泛型类型“Array<T>”需要 1 个类型参数。
类型推导
类型推导是TS的一个重要功能,类型推导可以根据赋值的类型对变量的类型进行定义,例如:
let name = 'xz'
//等价于:
let name: string = 'xz'
myName.toFixed() //toFixed并不存在于string类型上
在TS环境下定义name并赋值'xz',TS会对我们的赋值进行类型推导。TS通过我们的赋值,它会推导出name的类型,即string。当再使用其他的属性时会TS报错。
需要注意的是,类型推导是仅仅在初始化的时候进行推导,一旦推导成功,那么该变量的类型就会被锁定。
除了基本类型,可以是复杂类型:
let obj = {name:'xz'}
obj.age = 18 //类型“{ name: string; }”上不存在属性“age”
对于函数类型的泛型同样也适用:
function id<T>(arg: T): T{
return arg
}
id<string>("lucifer"); // 这是ok的,也是最完整的写法
id("lucifer"); // 基于类型推导,我们可以这样简写
默认参数
除了类型推导,默认参数也是TS系统中的一大特色。这里有几个栗子:
interface defaultType<T = string> {
value: T;
logName: (value: T ) => T //这里定义一个函数类型,和函数使用泛型或者类型区分开。
}
let logName: defaultType< number > = {
value: 1,
logName: (value: number) => value
}
当使用defaultType接口时,如不传入对应类型,TS会使用默认类型。
除了interface可以使用默认参数外,type关键字也可以使用默认类型,例如:
//定义一个函数类型,这个函数类型的入参和出参一致。
type func = (value: string) => string
如果使用泛型来对入参和返回值的类型不进行约束,可以写为:
type func<T> = (value: T) => T
//使用时传入什么类型结果T就为什么类型
let _func: func<number> = function (arg) {
return arg
}
_func(1)
当然,也可以使用默认类型:
type func<T = number> = (value: T) => T
let _func: func = function (arg){
return arg
}
//也可以传入参数:
let _func: func<string> = function (arg){
return arg
}
前面讲了关于泛型的使用,那么我们什么时候使用泛型呢?这里谈谈我对使用泛型的理解,欢迎指正。
- 第一:当接口,函数或者类需要支持很多类型时可以使用泛型。
//例如:
interface cacheData<T> {
data: T,
cacheTime: Date;
expirationTime: Date;
}
定义的数据存储接口并不能确定要存储的数据类型,可能会有很多种,因此可以使用泛型。
- 第二:当接口,函数或者类需要被用到很多地方时可以使用泛型。
泛型嵌套 和 泛型递归
泛型和函数一样,是支持嵌套的,举个例子:
interface car<T>{
height: T;
}
interface bmw<T>{
type: T
}
let myCar: bmw<car<number>> = {
type:{
height: 12,
}
}
// 嵌套后bmw的类型为:
interface bmw {
type: {
height: number;
}
}
除了泛型嵌套,同时还支持泛型的递归操作:
type ListNode<T> = {
data: T;
next: ListNode<T> | null;
};
keyof属性
TS允许我们遍历某种类型的属性,并通过 keyof 操作符提取其属性的名称。该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。举个栗子:
interface Person {
name: string;
age: number;
location: string;
}
type keysPerson = keyof Person // keysPerson = "name" | "age" | "location"
其中"name" | "age" | "location"是联合类型。
联合类型
这里做一个联合类型的小插曲。所谓的联合类型本质上是一个类型,它和其他的基础类型不同,联合类型表示一个值可以是几种类型之一。 我们用竖线|分隔每个类型,所以 number | string | boolean表示一个值可以是 number, string,或 boolean。例如:
type type1 = number | string | boolean
以上面为例,keyPerson是一个联合类型,如果某一个变量使用该联合类型,则赋值的内容必须是对应的类型。例如:
let person: keysPerson = 'name'
person = 123 //类型错误
person = 'alibaba' //类型错误
智能接口提示
我们可以通过TS来做一个智能接口提示,所谓的智能接口提示是指当我们访问一个接口的数据,那么它会有对应字段的提示。具体实现如下:
interface Seal {
name: string;
url: string;
}
interface API {
"/user": { name: string; age: number; phone: string };
"/seals": { seal: Seal[] };
}
const api = <URL extends keyof API>(url: URL): Promise<API[URL]>=> {
return fetch(url).then((res) => res.json());
};
接下来对api这个函数做一个解读,其他的不是很难。我们先忽略掉对应的类型,单纯看这个函数:
const api = (url) => {
return fetch(url).then( (res) => res.json )
}
函数比较简单,主要针对get函数,这里并没有体现参数params,通过传入的url发起请求获取对应数据。
接着我们带上类型来解析这个函数,主要分为三部分:泛型,入参,返回类型。分别对应:<URL extends keyof API>,(url: URL)以及Promise<API[URL]>。
- 泛型:这里并没有像往常一样使用
T作为泛型变量,而是取URL作为了泛型变量,接着通过extends进行泛型约束。具体约束是通过keyof API来获取联合类型'/user' | '/seals'。然后通过联合类型对泛型变量URL进行约束。
<URL extends keyof API>
//等价于
<URL extends '/user' | '/seals'>
通过泛型约束,限定了我们传入到api中的参数只能是'/user' 或者 '/seals'。这里是路由提示。
-
入参 因为有泛型约束的存在,传入的
url不再是任意类型,只能是'/user' 或者 '/seals'。 -
返回类型 首先,返回类型是一个
promise泛型,如果调用时传入的url为/user, 将泛型变量代换之后可得API[/user]。 而API[/user]所对应的类型是:{ name: string; age: number; phone: string },那么返回类型可以等价为:
Promise<{ name: string; age: number; phone: string }>
这里暂时不对Promise类型进行讲解,在后面会有说到。至此api函数讲解完毕,我们可以通过只能接口根据传入的路径获取到对应的数据提示。
Promise<T>
Promise<T>类型是一个TS内置的类型,我们可以直接使用不需要定义,举个栗子:
let res: Promise<{name: string}>
使用时需要传入具体类型参数。那么Promise内部是如何工作的,接下来我们一探究竟。
Promise<T>类型在*/lib.es5.d.ts文件下,下面是对应的代码:
interface Promise<T> {
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
//...
}
因为Promise的方法不止then,因此TS中的Promise函数的其他参数在其他的.ts文件里。这里我只复制了then函数,其他函数和注释过滤掉了。
这里并不会对Promise的其他类型做讲解,只讲解和T有关的类型,其他类型有时间的话我会单独讲一讲Promise在TS中的类型。
interface Promise<T> {
then <TResult1 = T, TResult2 = never> ( (value: T) => TResult1, (reason: any) )
}
过滤掉其他类型之后其实Promise<T>类型内部就比较明朗了。我们传入的Promise<{name: string}>本质上是对(value: T) => TResult1函数的入参和返回值类型的一个定义,因为在创建一个Promise之后回去调用对应then方法,而我们传入的参数就是then方法成功回调函数的入参和返回类型。
interface baseData {
name: string;
age: number;
}
let promise: Promise<baseData> = new Promise((resolve)=>{
resolve({name: 'xz', age: 18})
})
promise.then((res) => {
//然后对res进行操作
})
我们传入的baseData类型就是给res定义的。可以等价于:
promise.then((res: baseData) => {})
和上面智能接口结合,在使用res时就会有数据提示。
参考文献: