TypeScript 06 - 泛型

  1. 基本示例
  2. 使用泛型变量
  3. 泛型类型
  4. 泛型类
  5. 泛型约束

1. 基本示例

考虑到组件的可重用性,引入了泛型的概念,可以使得函数的返回值类型总是和传入值类型保持一致,而不管你传入的是什么具体的类型,使用了泛型的函数叫做泛型函数。

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

调用该函数时,通常依赖编译器自身的类型推断来确定 T 的类型:

let output = identity("myString");  // type of output will be 'string'

但复杂情况下,或许还是需要手动使用 <>尖括号来明确传入类型:

let output = identity<string>("myString");  // type of output will be 'string'

2. 使用泛型变量

需要时可以把泛型变量 T 当做类型的一部分使用,就像其他的类型变量一样,number[]string[]

// T[] 表示元素类型是 T 的数组
function identity<T>(arg: T[]): T[] {
  console.log(arg.length); // 通过声明泛型 T[],进一步缩小范围,获取 length 属性时才不会报错 
  return arg;
}

使用过其它语言的话,也可以这样来声明泛型函数:

function identity<T>(arg: Array<T>): Array<T> {
  console.log(arg.length);
  return arg;
}

3. 泛型类型

刚才已经创建了一个泛型函数,它可以灵活地根据具体传入的类型,而返回相同的类型。现在继续研究一下函数本身是什么类型,之前接口那一篇中提到了函数这种类型的接口,用来把函数的类型抽象出来、给出定义,其实就是在接口中定义了函数的签名:

interface SearchFunc {
  (source: string, subString: string): boolean;
}

下面定义了 myIdentity 变量,并且指明了它的类型,是一种泛型函数,接下来用它存储已定义好的泛型函数,这没问题:

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

let myIdentity: <T>(arg: T) => T = identity;

还可以使用带有调用签名的对象字面量来定义泛型函数:

let myIdentity: {<T>(arg: T): T} = identity;

这种花括号包裹着函数签名的对象字面量写法,其实就可以单独抽出来,定义为一个泛型接口了,以便多次复用。所谓的泛型接口,也就是当函数接口加入了泛型时的称呼,然后改写刚才的 myIdentity 变量声明,用泛型接口代替了直接写死:

interface GenericIdentityFn {
  <T>(arg: T): T
}

let myIdentity: GenericIdentityFn = identity;

到目前为止,其实和最开始的泛型例子区别不大,只是抽出了个所谓的泛型接口,泛型函数内部的 T 究竟是什么具体的类型,还是要等到真正调用函数的时候才能确定。可是如果我们想早点确定下来呢?换句话说,既然已经抽离出来了泛型接口,我们想把 T 的指定权从函数调用处,转移到泛型接口使用处。这样还有个好处是接口中的其他成员也能利用 T 了。

很简单,类似于给一个函数定义形参一样,下面把这个 T 也当做接口的“形参”,刚才篇幅可能有点长,完整写法及用法如下:

interface GenericIdentityFn<T> {
  (arg: T): T;
}

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

let myIdentity: GenericIdentityFn<number> = identity;
myIdentity(200);

可以看到,指定 T 具体是什么类型的时机发生在使用接口的时候而非等到调用函数的时候。何时把参数放在调用签名里和何时放在接口上,取决于具体实践中,想把哪部分类型归属到泛型部分。

4. 泛型类

除了泛型接口,还可以创建泛型类,和泛型接口差不多。与接口一样,直接把泛型类型放在类后面:

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) { return x + y; };

let result1 = myGenericNumber.add(myGenericNumber.zeroValue, 200);
console.log(result); // 200

既然是泛型,所以并不限制只能使用 number 类型,也可以用 string 或更复杂的类型:

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

let result2 = stringNumeric.add(stringNumeric.zeroValue, 'test');
console.log(test); // 'test'

5. 泛型约束

5.1 基本用法

在本篇最初的例子中说了为什么需要泛型,以及更“精确”一点的泛型 T[],为了能正确推断出 T 具有 length 属性。再看一下本篇最初的例子:

function identity<T>(arg: T[]): T[] {
  console.log(arg.length); // 通过声明泛型 T[],进一步缩小范围,获取 length 属性时才不会报错 
  return arg;
}

但如果只是为了满足参数具有 length 属性这一个要求,却不得不传入定义为 T[],显然这样丧失了一些泛型的灵活性和精髓。同样类似的需求,在定义泛型时能否既拥有必要的约束,又不至于太死板?这就引入了泛型约束的概念:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length); // 现在我们知道 arg 拥有 length 属性
    return arg;
}

调用该泛型函数时,只要传入的参数类型符合 Lengthwise 接口即可:

loggingIdentity(3);  // 报错,数字 3 没有 length 属性
loggingIdentity({ length: 10, value: 5 }); // ok,传入的对象有 length 属性
loggingIdentity([1, 2, 3]); // 也 ok

5.2 在泛型约束中使用类型参数

可以声明一个类型参数,它被另一个类型参数所约束。比如用属性名获取对象属性时,想要保证该属性是存在于对象上的:

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, 'a');
getProperty(x, 'c');
getProperty(x, 'e'); // 报错

5.3 在泛型里使用类类型

之前在接口那一节讲类类型时,也演示了工厂函数相关的操作。 现在结合泛型在创建工厂函数时,并不写死传入的构造函数具体类型,而是引用构造函数的类类型,满足构造函数类型的类,就可以在调用工厂函数时传入其中:

class Person {
  name: string = 'Tom';
  print() {
    console.log(this.name);
  }
}

function create<T>(c: { new(): T; }): T {
  return new c();
}

let p = create(Person);
p.print(); // 'Tom'

一个更高级的例子,刚才的工厂函数可以进一步使用原型属性推断并约束构造函数与类实例的关系:

class BeeKeeper {
  hasMask: boolean;
}

class ZooKeeper {
  nametag: string;
}

class Animal {
  numLegs: number;
}

class Bee extends Animal {
  keeper: BeeKeeper;
}

class Lion extends Animal {
  keeper: ZooKeeper;
}

function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}

createInstance(Lion).keeper.nametag;  // 可以成功推断
createInstance(Bee).keeper.hasMask;   // 可以成功推断
createInstance(Animal).numLegs; // 可以成功推断

点到为止,实际写项目时可能才有更深入的理解和摸索出最佳实践。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,104评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,816评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,697评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,836评论 1 298
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,851评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,441评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,992评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,899评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,457评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,529评论 3 341
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,664评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,346评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,025评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,511评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,611评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,081评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,675评论 2 359

推荐阅读更多精彩内容

  • C# 泛型(Generics) 泛型概述 泛型是C#编程语言的一部分,它与程序集中的IL(Intermediate...
    OctOcean阅读 2,250评论 0 4
  • 软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能...
    2o壹9阅读 609评论 0 48
  • object 变量可指向任何类的实例,这让你能够创建可对任何数据类型进程处理的类。然而,这种方法存在几个严重的问题...
    CarlDonitz阅读 917评论 0 5
  • 介绍 软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。组件不仅能够支持当前的数据类型,同...
    24KBING阅读 362评论 0 1
  • 泛型 软件工程中,我们不仅要创建定义良好且一致的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,...
    罗彬727阅读 620评论 0 0