本文作者为 360 奇舞团前端开发工程师
一、什么是泛型
泛型,我们光从字面上来推断,泛,宽泛,广泛,型,型号,类型。所以我们可以先认为,泛型就是给我们的代码增加一种相对宽泛的类型约束。在TypeScript中,我们定义一个变量,我们可以赋予其一种确定的类型。使得我们的代码具有更好的维护性,但是在增强代码的可维护性同时,我们又要考虑代码应该具有一定的灵活性。使得在未来,代码也能被复用。于是泛型就在这个背景下出现了。
二、泛型函数
const printFun = (value) => {
console.log(value);
return value;
};
console.log(printFun("hello world"));
我们在上面代码中创建了一个函数,作用很简单,就是打印我们传入的参数并返回该值。这时我们没有添加任何类型约束。我么可以传入一切类型,但是我们现在希望,该函数只能打印我们传入的类型。而且返回值的类型一定要和传入的参数类型一致,所以我们就要给代码增加一定约束,就像下面这样。
const printFun = (value: string): string => {
console.log(value);
return value;
};
printFun("hello world");
现在我们的函数就只能接收字符串类型的变量了,并将其返回。
在未来的某一天,我们需要打印一个数字,我们调用这个函数的时候,就会报错,并提示我们需要传入字符串类型,前面这段代码虽然有了一定的约束性,但是也丢失了灵活性,我们当然可以通过重新定义一个函数,来解决这个问题,但是,我们如果使用泛型来解决这个问题,就显得更加合适。
const printFun = <T>(value: T): T => {
console.log(value);
return value;
};
printFun(233);
我们在代码中引入了一对尖括号,并传入了一个T,字母T代表一种类型,一种没有确定的类型,我们可以把这个T看作和函数所接收的参数value一样,是一个占位符,我们可以用自己喜欢的字母,单词来替换掉T,在我们没有给函数传参之前,我们只通过这个函数,至少就能知道一点,函数的入参和出参,在类型上是一致的,这就对我们的代码形成了约束,但是这种约束和我们具体定义某一种类型不同,我们定义函数时,也并不知道,T具体会是什么类型。我们可以把这种写法和函数传参进行对比,声明函数时,我们也不知道value的值会是什么。但是在给函数传参的时候,我们value的值就确定了,此时T的类型也被确定了,推断出T是number类型。同理,我们如果传入一个字符串的话,T就会被确定为string类型,这样我们的代码在有了更好的灵活性的同时,也具有了一定的约束。
三、泛型接口
interface是我们在使用TS的时候,最常用的关键字。
interface Person {
name: string;
age: number;
}
const me: Person = {
name: "sunny",
age: 18,
};
我们在前面的代码中用声明接口限制了一个对象,我们也可以通过泛型对我们的接口进行改造。
![105ea741734d01c71d1ed7ab34bef5e9.png](https://i-blog.csdnimg.cn/blog_migrate/a7ac6ae11459a4ef1c7c9fd21955d571.png)
这时候我们通过定义J,K两个占位符,使得我们的接口具备了泛型,更加灵活。但是我们发现泛型接口和泛型函数不一样,我们像前面图片中这样使用,有一个地方报错了,提示我们需要传入类型参数,而不能依赖自动的类型推断,来确实对象属性的类型,其实这也是可以理解,因为我们对一个对象的限制,主要就是限制对象的属性类型,如果我们不确定类型,那我们传入的一切类型都是有效的了。就起不到类型的限制,而前面我们使用泛型函数,虽然没有显示声明类型,但是我们达到了对函数入参和出参进行了统一。
interface Person<J, K> {
name: J;
age: K;
}
const me: Person<string, number> = {
name: "sunny",
age: 18,
};
这次我们在使用泛型接口时,对其传入了两个确定的类型。J,K的类型也因此确定了。其实在学习泛型之前,大家或许就已经使用过泛型了,比如
const arr: Array<number> = [1, 2, 3];
这是TS中定义一个数组的一种写法。其实就是定义了一个数组对象,并限制了数组中每个元素的类型为number。
四、泛型约束
我们使用泛型也是为了对我们的代码进行类型的约束,但是泛型我们知道是一种没有确定的类型,我们通过下面的代码来感受一下。
![fc91ddeb5bde24d05c868484dd85d259.png](https://i-blog.csdnimg.cn/blog_migrate/64fddd4205f4f67c74d825663d96cbd2.png)
我们给函数传入了一个对象,在函数中想要读取其所具有的属性值,但是编辑器却提示我们类型T上没有age和name属性。因为泛型本身就不是某一种具体的类型,所以静态的类型检查,自然无法判断出,其是否具有相应的类型。我们可以对函数进行一些改造,像下面这样。
interface Person<J, K> {
name: J;
age: K;
}
const printFun = <T extends Person<string, number>, S>(
person: T,
msg: S
): S => {
console.log(person.age);
console.log(person.name);
return msg;
};
printFun({ name: "sunny", age: 18 }, "success");
这次我们使用了extends关键字,和接口泛型,对类型T进行了一些扩展,使得其具备了我们所要求的属性。
五、小结
通过前面几节的介绍,我们对泛型有了初步的了解,知道泛型出现的背景和意义,能将泛型初步的使用于我们的项目之中。
- END -
关于奇舞团
奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。