前言
- 昨天因为要做一次分享,所以对泛型常用知识点做了个整理,欢迎各位大佬指出问题
- 泛型在TS中一向都是非常重要的
- 一个人是否入门TS,首先要看他会不会泛型
- 一个人是不是熟悉TS,要看他能不能熟练的用类型进行编程
- 基本上所有的类型体操题目都要用到泛型
- 所以今天就由浅到深的来聊一聊泛型,再掌握一下做类型体操题目所必需的一些知识点
1. 泛型的基本使用
-
在使用TS开发的过程中,代码的可重用性也是非常重要的
- 比如我们可以封装一些工具函数,通过传入不同的参数,来让函数帮我们完成不同的操作
-
但是目前我们在开发的过程中遇到了一个问题
-
我们在封装好一个函数之后,如果明确的写了这个函数的参数类型和返回值类型都是number的话,那么这个函数就不能用来处理string或者其他类型的数据了
- 如果使用联合类型,会导致类型过长
-
虽然也可以定义为any类型,但是这样就没什么意义了,我们已经丢失类型信息了
-
-
我们想要做到的是:
- 比如我们传入的是一个number,那么我们希望返回的可不是any类型,而是number类型
- 所以,我们需要在函数中可以获取到参数的类型是number,并且同时可以使用它来作为返回值的类型
-
这个时候,我们就可以使用泛型来完成这一需求:
-
泛型其实说白了就是类型参数化,它可以使我们在使用TS时,类型变得非常灵活
-
泛型的使用方式就是,在定义函数的时候,给函数名后面加一个
<>,里面写一个类型的参数,后续这个函数中,如果也想要返回和参数相同类型的数据的时候,就可以使用这个类型的参数function foo<T>(param: T): T { return param } -
那么在调用的时候,就需要我们将这个类型的参数传给函数,有两种方式可以给这个函数传递类型的参数
-
方式一:通过函数名<类型>() 的方式传递
-
方式二:通过类型推导,自动推导出来我们传给函数的参数类型
- 如果我们是通过const这个关键字定义的变量接收返回值的话,那推导出来的数据类型就直接是一个字面量类型
- 但是如果是通过let定义的变量接收返回值,那么推导出来的数据类型毫无疑问就是一个普通数据类型了(number)
-
不要太依赖于TS自动推断,如果发现它推导的不正确的话,还是要我们自己指定的
function foo<T>(param: T): T { return param } const res1 = foo<string>('Judy') const res2 = foo(123) let res3 = foo(456) -
-
-
使用泛型实现useState方法
function useState<T>(state: T): [T, (currentState: T) => void] { let newState = state function setState(currentState: T) { newState = currentState } return [ newState, setState ] } const [ count, setCount ] = useState(100)
2. 泛型的额外补充
-
我们在使用泛型的时候,也可以传入多个类型的参数
function foo<T, O>(name: T, info: O): {name: T, info: O} { return { name, info } } const { name, info } = foo('Judy', { age: 18, height: 1.88 }) -
平时开发中我们经常看到的一些参数类型是什么T、O、K、V之类的
-
其实它们都是缩写:
- K、V:key和value的缩写,键值对
- T:Type的缩写,类型
- O:Object的缩写,对象
- E:Element的缩写,元素
- R:ReturnType的缩写,返回值的类型
-
3. 泛型类、泛型接口
3.1. 泛型类
-
泛型也是可以应用到类上面的
-
如果我们在定义一个类时,想要这个类的构造函数接收的参数类型,也由调用者决定的话,就可以使用泛型
class Direction<T> { constructor(public x: T, public y: T) {} } const d1 = new Direction(111, 222) const d2 = new Direction('aaa', 'bbb')
3.2. 泛型接口
-
同时,泛型也是支持在接口上使用的,我们在定义接口的时候,也可以使用泛型:但是需要注意的是,在和接口搭配使用泛型的时候,TS是不会帮我们自动进行类型推导的
-
所以为了方便起见,泛型也是可以有默认类型的
interface IInfo<T = string> { name: T, age: number, slogan: T } const info1: IInfo = { name: 'Judy', age: 18, slogan: 'Hello' } const info2: IInfo<number> = { name: 123, age: 18, slogan: 456 }
4. 泛型约束
注意!!!当我们对一个类型参数使用extends这个关键字的时候,就可以理解为,这是要对这个类型参数做约束了
-
有时候我们希望在封装一个工具函数的时候,函数的参数类型可以由调用者决定,这时就可以使用泛型
-
但是同时,我们又希望对这个传入的参数加上一些约束,不能让调用者乱传。并且需要将这个传入的参数类型保留下来,最后返回值的类型就是传入的参数类型的话,这时就可以使用泛型约束
-
因为传入的类型参数其实就相当于一个变量,在给它传了具体的类型之后,它就可以将这个类型保存下来,在整个函数的生命周期中都可以任意使用
-
比如我们现在有个需求,我封装了一个工具函数,这个工具函数接收的参数,必须得有length,最终我会将这个参数又返回出去。但是我在获取到函数返回值之后,要求我传入的是什么类型,那么返回值的类型也不能丢失
- 有一种比较容易犯的错误就是使用对象参数来实现这个需求
- 虽然它成功限制了,没有length属性的对象无法作为参数传入
- 但是我们会发现最终拿到的res1和res2的类型都是
{ length: number } - 这丢失了它原本的类型,所以这是一种错误的做法
interface ILength { length: number } function getInfoHasLength(arg: { length: number }) { return arg } let res1 = getInfoHasLength('Judy') let res2 = getInfoHasLength({ length: 1 }) let res3 = getInfoHasLength(123)- 那么正确的做法是:我可以定义一个类型的参数,并且同时,让它继承一个拥有length属性类型的接口
- 那么在传入
'Judy'时,它的string类型就会被保存在T中,最终返回的时候,返回值的类型就是正确的了 - 最终我们在拿到res1和res2的时候,类型就不会丢失,分别是string和{ length: number }
interface ILength { length: number } function getInfoHasLength<T extends ILength>(arg: T): T { return arg } let res1 = getInfoHasLength('Judy') let res2 = getInfoHasLength({ length: 1 }) let res3 = getInfoHasLength(123) // 没有length属性,所以报错 -
泛型约束练习:
-
现在我们有一个需求,就是封装一个工具函数,这个函数接收两个参数,第一个参数是一个对象,第二个参数是一个字符串
-
但是我们要保证,第二个传入的字符串,必须是传入的对象的keys中的某一个
-
如果在调用的时候乱传,那么就直接报错
-
要实现这个,首先要了解一个关键字
keyof- keyof的作用就是获取到某个对象中的所有的key,并且将它们转换成一个联合类型
-
那么我们就可以实现它了
- 首先这是一个工具函数,别人可以传任意对象。并且key也是由函数调用者决定的,所以对象和key的类型都使用泛型
- 其次,要保证key是对象中的某个属性名,所以就可以使用extends给其添加约束。并且这个约束的条件就是当前对象类型的其中的某个key
function operateObj<O, K extends keyof O>(obj: O, key: K) { return obj[key] } const obj = { name: 'Judy', age: 18, height: 1.88 } const res1 = operateObj(obj, 'height') -
-
5. 映射类型
5.1. 基本使用
-
有的时候,我们想要创建一个基于另一个类型的新类型,但是又不想拷贝一份新的,也不能使用继承,这个时候就可以考虑使用映射类型
-
大部分的内置工具和类型体操的题目都是通过映射类型来完成的
-
不能使用继承的原因主要就是:
- 原类型中的某些属性可能是readonly或者可选的。但是我新类型中的属性不需要这些
- 如果使用继承的话,父类型是什么,子类型就是什么。无法修改是否readonly或者可选
- 所以这就是某些时候不能使用继承的主要原因
-
-
注意:映射类型只能通过type关键字定义,不能使用interface定义。并且不能对原有的类型做拓展
-
映射类型是建立在索引签名的基础上的:
-
其实很简单,就是定义一个新的type映射类型,通过泛型将想要映射的类型传入,然后在这个映射类型中写索引签名
-
映射类型通过泛型接收一个类型
-
拿到类型之后,新类型的key就是通过遍历这个接收到的类型的keys得到的
-
而新类型key对应的value,就是
接收到的类型[key]-
有点类似于这个:
const obj = { ... } const newObj = {} Object.keys(obj).forEach(key => { newObj[key] = obj[key] })
-
-
-
最后将映射类型调用一下,映射类型就会返回一个新类型,再将其返回的类型赋值给新类型就完事了
interface IPerson { name: string, age: number, height: number } // 定义映射类型 type PersonMap<T> = { [property in keyof T]: T[property] } // 调用映射类型对IPerson映射,获取到新类型 type NewPersonType = PersonMap<IPerson> // 此时鼠标放到p上,就可以发现这是一个和IPerson一模一样的类型 const p: NewPersonType = { }
-
5.2. 映射修饰符(内置工具)
-
刚才我们说到,不能使用extends继承是因为要使用一些修饰符,这和对象的修饰符是是一样的:readonly和 ?
-
并且我们可以通过给这两个修饰符前面加上
-和+,来控制映射出来的新类型是否需要这两个修饰符-
如果原类型有修饰符,新类型不想要,那就用减号
-
如果原类型没有修饰符,新类型想要,那就用加号了
interface IPerson { readonly name: string; age: number; height: number; } type PersonMap<T> = { -readonly [property in keyof T]?: T[property]; }; // 新类型上没有readonly,并且每个属性都是可选的 type NewPersonType = PersonMap<IPerson>; const p: NewPersonType = {};
-
6. 条件类型
-
在开发中,我们需要基于输入的值来决定输出的值,同样的道理,我们也可以基于输入的值的类型决定输出的值的类型
-
条件类型就可以帮我们来做这件事
-
它类似于三元运算符
-
type ResType = OriginType extends OtherType ? TrueType : FalseType -
如果OriginType符合OtherType的条件的话,那么就返回TrueType,否则返回FalseType
-
我们来对以前的那个重载签名做个优化
function sum<T extends number | string>(arg1: T, arg2: T): T extends string ? string : number function sum(arg1: any, arg2: any) { return arg1 + arg2 } sum(10, 20)
-
6.1. 在条件类型中推断
-
这个知识点的意思就是:条件类型提供了infer关键字,可以从正在使用条件比较的类型中,推断出来类型,然后在True分支中将推断出来的类型返回
- 推断出来函数的返回值类型
type FooFnType = (num1: number, num2: number) => number // TS提供的工具类型:ReturnType type FooReturnType = ReturnType<FooFnType>; // 自己实现:MyReturnType type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never type MyFooReturnType = MyReturnType<FooFnType>- 推断出来函数的参数类型
type FooFnType = (num1: number, num2: number) => number; // TS提供的工具类型:Parameters type FooReturnType = Parameters<FooFnType>; // 自己实现:MyParameters type MyParameters<T extends (...args: any[]) => any> = T extends ( ...args: infer P ) => any ? P : never; type MyFooParametersType = MyParameters<FooFnType>;
6.2. 分发条件类型
-
在泛型中使用条件类型的时候,如果传入了一个联合类型,那就会变成分发的
-
分发是什么意思呢?
- 比如下面这个例子,当我们给ToArrayType传入一个联合类型的时候,它就会自动遍历这个联合类型中的每一个类型
- 相当于
number extends any ? number[] : never和string extends any ? string[] : never - 所以最后的结果就是:
string[] | number[]
type ToArrayType<T> = T extends any ? T[] : never // 得到的结果是:string[] | number[] type NewType = ToArrayType<number | string> -