理解 TypeScript 泛型

什么是泛型(Generics)

定义:泛型容许咱们延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候。编程

解释定义

请看以下代码:数组

function identity<T>(arg: T) {
  return arg;
}

function main() {
  const result = identity<number>(1);
}

复制代码

咱们定义了一个泛型方法identity,定义了一个main方法,调用identity方法。bash

定义identity的时候,咱们并不知道参数arg是个什么类型,T就是一个占位符,告诉咱们arg是T类型的。编辑器

在调用identity的时候,才告诉它我要给你传递一个number类型的,而后传递真正的arg参数的值,是1。ide

这就是泛型定义中所说的,延迟编写方法(==identity==)中的编程元素(==arg参数==)的类型(==不写number,写T==),直到实际在程序中(==main方法==)使用它的时候。函数

类型参数化

上例中,将类型(number)变成了函数identity的一个参数,在调用的时候才传给函数identity,就叫作类型参数化。学习

泛型有什么好处

看完上面的解释,你多半会疑惑,这么写有什么好处啊,之前那种不也挺好吗,以下:ui

function identity(arg: number) {
    return arg;
}

function main() {
  const result = identity(1);
}
复制代码

这不也实现了相同的逻辑吗?何须延迟到调用的时候,才肯定参数的类型呢?this

答案是 ==泛型可让不一样类型的数据,复用一样的方法或类。== 若是方法或类在定义的时候,就固定参数的类型,那么此方法或类只能处理这一种类型的数据。但等调用的时候才肯定参数类型,就可让多种参数类型的数据,复用同一个方法或类。spa

这里咱们分析一个稍微复杂的例子,内置对象Array的声明:

interface Array<T> {
    length: number;
    
    toString(): string;
    
    pop(): T | undefined;
    reverse(): T[];
    sort(compareFn?: (a: T, b: T) => number): this;
    
    ...
}
复制代码

这里只是列出小部分Array的声明。我仍是按泛型的定义解释一边,在咱们编写Arry的时候,延迟定义编程元素(这里指在Array类内能够使用的T)的数据类型。

使用:

function main() {
  const nums = new Array<number>(1, 2, 3);
  nums.push(4);

  const popNum = nums.pop();
}

复制代码

直到实际在main函数中使用它的时候。咱们才告诉Array,咱们要一个数字类型的数组。

咱们还能够

function main() {
  const dates = new Array<Date>();
  dates.push(new Date('2019-7-8'));
  dates.push(new Date('2019-7-9'));
  dates.push(new Date('2019-7-10'));

  const reverseDates = dates.reverse();
}
复制代码

注意的问题的关键没,关键在于pop,reverse这样的方法的内在逻辑,是不关心被操纵数据的类型的,因此能够实现对于不一样类型的的复用。

因此,Array类就实现类对于不一样类型的数据列表的相关操做的复用。

约束依旧存在

若是您是由Javascript为基础,学习的TypeScript,或许会以为上面说的有些莫名其妙,TypeScript搞这个泛型干啥,JS对此彻底没有限制啊,没有类型,能够随意传参啊,以下:

function mainAboutNumber() {
    var nums = new Array(1, 2, 3);
    nums.push(4);
    var popNum = nums.pop();
}

function mainAboutDate() {
    var dates = new Array();
    dates.push(new Date('2019-7-8'));
    dates.push(new Date('2019-7-9'));
    dates.push(new Date('2019-7-10'));
    var reverseDates = dates.reverse();
}
复制代码

确实如此,若是没有类型,就没有泛型这一说了,泛型是对类型的一种放宽政策,但约束仍旧存在。若是是用JS,这样的代码在编写时不会出错:

function mainAboutDate() {
    var dates = new Array();
    dates.push(new Date('2019-7-8'));
    dates.push('我不是Date类型');
    dates.push(new Date('2019-7-10'));
    var reverseDates = dates.reverse();
}
复制代码

但若是你的业务中,dates是表明一个日期列表,显然仍是TS更好:

image

会在咱们编程期间及时报错,泛型的约束力依旧存在,当给定参数的类型后,咱们就有了一个日期类型的数组了。

泛型的其余约束力

除了复用这条优势,泛型还能够帮助咱们实现入参和返回值类型一致的校验,以下:

function identity<T>(arg: T): T {
    return arg;
}
复制代码

如方法后面的冒号+T,就是限定返回值的类型也是T,要和参数arg的T保持一致,当咱们使用它的时候:

image

将鼠标浮动到num变量上,发现它是个number类型,这是由于咱们将T设置成了numnber。经过类型推断,num就成了number类型,后面就能够经过编辑器的智能提示,看到number类型的全部方法了,方便咱们的编程。

泛型变量

在定义方法或类的时候,咱们不光能够使用T,还能够为T增长更多修饰,使其变成一种有更多信息的泛型变量。

以下是Object.freeze方法的签名。

Object.freeze() 方法能够冻结一个对象。一个被冻结的对象不再能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。 --- MDN定义

interface ObjectConstructor {
    freeze<T>(a: T[]): ReadonlyArray<T>;
    
    ...
}
复制代码

咱们来看freeze(a: T[]): ReadonlyArray签名。 这个签名的意思是,若是你传递的是个数组(a:T[]),返回值会是一个ReadonlyArray。以下是使用时的代码:

function main() {
  const nums = new Array<number>(1, 2, 3); 
  const freezeNums = Object.freeze(nums);
}
复制代码

咱们就获得了一个readonly的数字数组。这里的T[]和ReadonlyArray。利用T进一步描述freeze方法的参数和返回值。若是单单用T,显然信息量不够多。

另外假设让咱们来实现freeze方法。那内部咱们就能够调用a.length这样的代码,由于签名已经明确告诉咱们,入参a是某种数组类型。

泛型约束

泛型约束又是什么?咱们在看freeze方法的另外一个重载:

interface ObjectConstructor {
    freeze<T>(a: T[]): ReadonlyArray<T>;
    freeze<T extends Function>(f: T): T;
     
    ...
}
复制代码

不一样于泛型变量,这回咱们约束尖括号<>里的T,freeze(f: T): T;的意思是,若是传递给freeze的是个方法,那返回值仍是个方法。

再看一个例子:

interface SpecialType {
  specialProperty: string;
}

function identity<T extends SpecialType>(arg: T): T {
  console.log(arg.specialProperty);
  return arg;
}
复制代码

这个例子中,咱们让T继承SpecialType,这样咱们在identity方法内部,就能够使用SpecialType规定的属性了。

泛型约束能够说增长了泛型的复用能力,可让某些含有相同属性的数据,复用一样须要这些属性的方法。

再多分析一个包含泛型的签名

interface Array<T> {
    ...
    
    map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
    
    
    ...
}
复制代码

咱们来分析一下Array类中的map函数,也是咱们常常使用的函数。

map函数签名

第一个参数是个回调函数,回调函数的第一个参数value是T类型,第二个参数index是数字类型,第三个参数是T类型的列表。回调函数的返回值是一个U类型的数据。map函数的返回值是U类型的列表,是个全新类型的列表。

结束语

学习泛型,可让咱们很好的理解第三方库的TS声明。也可让咱们本身写出优秀的TypeScript代码。

相关文章
相关标签/搜索