TypeScript学习笔记(七) 泛型

大家好,我是半虹,这篇文章来讲 TypeScript 中的泛型


1、概述

有些时候,定义函数或者类时,可能无法事先确定其中参数的类型

举个例子,假设现在有个函数,这个函数接收一个值再返回这个值

实际上这个值可以是任意类型,我们无法事先确定,因此只好设为 any

function identity(value:any):any {
  return value;
}

但是,这种写法无法反映参数和返回类型间的关系,理论上二者是相同的

为此,这个时候就要用到泛型!


所谓的泛型其实就是参数化的类型,具体有两特点:

  1. 定义的时候声明参数表示类型(定义时声明),具体是在 <> 内声明
  2. 使用的时候参数绑定实际类型(使用时绑定),同样是在 <> 内绑定
// 定义
// 声明泛型参数,这里参数声明为 T (参数名称可任意指定)
function identity<T>(value:T):T {
  return value;
}

// 使用
// 绑定实际类型,这里分别绑定为 number 和 string
identity<number>(123);  // 相当于:function identity(value:number):number { ... }
identity<string>('0');  // 相当于:function identity(value:string):string { ... }

定义泛型时就像声明一个占位类型,而在使用时会把占位类型替换成实际类型

使用泛型有助于提高代码的重用性,允许不确定类型时,依然可以有类型约束


2、基本语法

泛型可以应用于不同的场景,这些场景具体包括:函数、类、接口、类型别名

不同的场景中,都遵循一个规则:定义时声明泛型参数,使用时绑定泛型参数

定义时,在 <> 内声明泛型参数,如果存在有多个参数,那么需要用逗号隔开

使用时,在 <> 内绑定泛型参数,如果存在有多个参数,那么则要按顺序指定

但是不同的场景定义时声明参数的位置以及参数的作用范围也不同,具体如下:


(1)泛型函数

我们知道,定义函数有两种方式,一是函数声明,二是函数表达式

  1. 如果是函数声明,  泛型参数声明写在函数名称之后
  2. 如果是函数表达式,泛型参数声明写在类型签名之中,而类型签名有两种
    1. 如果是简写版类型签名,泛型参数声明写在整条声明前面
    2. 如果是完整版类型签名,泛型参数声明写在每条声明前面

声明的类型参数可以在函数之内的任意位置引用

无论哪一种定义方式,使用函数时泛型参数绑定都在函数名称之后

// 定义函数

// 函数声明
// 格式如下:
// function func_name<gene_name1, ...>(prop1:type1, ...):return_type { ... }
// 例子如下:
function reverseArray0<E>(items: E[]): E[] {
  return items.reverse();
}

// 函数表达式(简写版类型签名)
// 格式如下:
// <gene_name1, ...>(prop1:type1, ...) => return_type
// 例子如下:
let reverseArray1: <E>(items: E[]) => E[];
reverseArray1 = reverseArray0;

// 函数表达式(完整版类型签名)
// 格式如下:
// {
//   <gene_name1, ...>(prop1:type1, ...): return_type;
//   ...
// };
// 例子如下:
let reverseArray2: { <E>(items: E[]): E[]; }
reverseArray2 = reverseArray0;

// 使用函数

// 调用函数
reverseArray0<number>([123, 456, 789]); // 绑定泛型参数为:number
reverseArray0<string>(['1', '4', '7']); // 绑定泛型参数为:string
reverseArray1<number>([234, 567, 891]);
reverseArray1<string>(['2', '5', '8']);
reverseArray2<number>([345, 678, 912]);
reverseArray2<string>(['3', '6', '9']);

(2)泛型类

而对于类来说,声明和绑定泛型参数都写在类名之后

声明泛型参数后可以在整个类中引用,除了静态成员,因为泛型类是描述类的实例

绑定泛型参数则发生在使用类的时候,具体包括实例化对象以及类的继承等的操作

类静态成员在类加载时就已经存在,且不依赖类的实例化过程

而泛型参数在类使用时才具体确定,因此无法被静态成员引用

// 定义类

class Container<T> {
  // 普通属性
  value: T;

  // 构造函数
  constructor(value: T) {
    this.value = value;
  }

  // 普通方法
  setValue(value: T) {
    this.value = value;
  }
  getValue(): T {
    return this.value;
  }
  convert<U>(f:(input:T) => U): U { // 类中方法也可声明泛型参数,此处声明的参数只能在该方法内引用
    return f(this.value);
  }
}

// 使用类

// 实例化对象
let container1 = new Container<number>(123); // 绑定泛型参数为:number
let container2 = new Container<string>('0'); // 绑定泛型参数为:string
container1.setValue(456);
container2.setValue('1');
container1.convert<string>(num => num.toString()); // 调用方法时,绑定泛型参数为:string
container2.convert<string>(str => str.toString()); // 调用方法时,绑定泛型参数为:string

// 继承类
class Extended1 extends Container<number> {  // 绑定泛型参数为:number (实际类型)
  compare(input:number):boolean {
    return this.value === input;
  }
}
class Extended2<NT> extends Container<NT> {  // 绑定泛型参数为:NT     (泛型参数)
  compare(input:NT):boolean {
    return this.value === input;
  }
}
let container3 = new Extended1(123);         // 无需绑定
let container4 = new Extended2<string>('0'); // 绑定泛型参数为:string
container3.setValue(456);
container4.setValue('1');
container3.convert<string>(num => num.toString()); // 调用方法时,绑定泛型参数为:string
container4.convert<string>(str => str.toString()); // 调用方法时,绑定泛型参数为:string
container3.compare(1234);
container4.compare('00');

(3)泛型接口

接口可以用于描述对象的属性和方法,此时声明和绑定泛型参数都写在接口名后

使用接口通常包括以下的场景:接口作为类型、类实现接口、接口继承接口等等

// 定义接口

interface SimpleGuest<T> { // 访客接口
  id: T;
  name: string;
}

interface SimpleCache<K, V> { // 缓存接口
  set(key: K, val: V): void;
  get(key: K): V | undefined;
}

// 使用接口

// 作为类型
let guest1:SimpleGuest<number> = { id: 123, name: '1' };
let guest2:SimpleGuest<number> = { id: 456, name: '4' };
let guest3:SimpleGuest<number> = { id: 789, name: '7' };

// 实现接口
class GuestCache implements SimpleCache<number, string> {
  private keys: number[] = [];
  private vals: string[] = [];
  // 实现 set
  set(key: number, val: string): void {
    const index = this.keys.indexOf(key);
    if (index !== -1) {
      this.vals[index] = val;
    } else {
      this.keys.push(key);
      this.vals.push(val);
    }
  }
  // 实现 get
  get(key: number): string | undefined {
    const index = this.keys.indexOf(key);
    if (index !== -1) {
      return this.vals[index];
    } else {
      return undefined;
    }
  }
}
let cache0 = new GuestCache();
cache0.set(guest1.id, guest1.name);
cache0.set(guest2.id, guest2.name);
cache0.get(guest2.id); // 4
cache0.get(guest3.id); // undefined

// 继承接口
interface DetailCache<K, V> extends SimpleCache<K, V> {
  remove(key: K): void;
  clear(): void;
  size(): number;
}

(4)泛型类型别名

类型别名和接口十分相似,同样也是:声明和绑定泛型参数都写在类型别名之后

类型别名和接口不同的是,类型别名除了可以定义对象类型,还能定义任意类型

// 定义类型别名

type Result<T> = T | Error; // 非对象类型

type TreeNode<T> = { // 对象类型
  value: T;
  children: TreeNode<T>[];
}

// 使用类型别名

function handleResult<T>(result: Result<T>): void {
  if (result instanceof Error) {
    // ...
  } else {
    // ...
  }
}
handleResult<number>(123);
handleResult<string>(new Error('something went wrong'));

let node: TreeNode<number> = {
  value: 1,
  children: [{
    value: 2,
    children: [],
  }, {
    value: 3,
    children: [],
  }]
}

3、泛型推导

上面的例子中,我们在使用泛型参数时都是显式绑定实际类型

实际上对于函数和类来说,编译器也能根据传入参数自动推导

// 定义泛型函数
function identity<T>(value: T): T {
  return value;
}
function reverseArray<T>(items: T[]): T[] {
  return items.reverse();
}

// 定义泛型类
class Container<T> {
  public value: T;
  constructor(value: T) {
    this.value = value;
  }
}

// 使用泛型函数
identity(123);                       // 此时自动推导 T 为 123
identity('0');                       // 此时自动推导 T 为 '0'
reverseArray([123, 456, 789]);       // 此时自动推导 T 为 number
reverseArray(['1', '4', '7']);       // 此时自动推导 T 为 stirng

// 使用泛型类
let container1 = new Container(123); // 此时自动推导 T 为 number
let container2 = new Container('0'); // 此时自动推导 T 为 string

但是有些时候,编译器可能无法推导出正确的类型,这时就要显式绑定

function combineArray<T>(arr1: T[], arr2: T[]): T[] {
  return [...arr1, ...arr2];
}
combineArray([123, 456, 789], ['1', '4', '7']);                 // 隐式推导,编译错误
combineArray<number|string>([123, 456, 789], ['1', '4', '7']);  // 显式绑定,编译正常

另外需要注意,所有的泛型参数要么都有显式绑定,要么都不显式绑定

function combineArray<U, V>(arr1: U[], arr2: V[]): (U | V)[] {
  return [...arr1, ...arr2];
}
combineArray([123, 456, 789], ['1', '4', '7']);                 // 都不绑定,编译正常
combineArray<number, string>([123, 456, 789], ['1', '4', '7']); // 都有绑定,编译正常
combineArray<number>([123, 456, 789], ['1', '4', '7']);         // 部分绑定,编译错误

4、泛型约束

有些时候,仅声明泛型参数可能还不够,我们还希望表达这个泛型参数满足某种约束

举个例子,有一个打印数组元素的函数,操作是遍历数组然后调用元素  print 方法

// 遍历数组,调用元素中的 print 方法进行打印

function printArray<T>(values:T[]): void {
  values.forEach(item => item.print()); // 编译错误
}

但是上述写法编译时会报错,因为无法保证数组元素具有  print 方法

这个时候就要用到泛型约束,确保泛型参数满足某种约束

具体可在泛型参数后加  extends 关键字,并在其后写明具体约束条件

例如 T extends U 可理解为 TU 的子类型,T 至少应该为 U

// 通过 T extends { print() : void } 确保 T 至少应该具有 print 方法

function printArray<T extends { print() : void }>(values:T[]): void {
  values.forEach(item => item.print()); // 编译正常
}

如果存在多个泛型参数,那么一个参数可以引用其它参数作为约束条件

但是这里需要注意一点,就是这些参数之间不能循环引用,否则会报错

// 编译正常,引用其它参数
function getProperty<O, K extends keyof O>(obj: O, key: K): O[K] { // 获取对象属性的常用泛型函数
  return obj[key];
}

// 编译错误,存在循环引用
function test1<U extends U>() {}
function test2<U extends V, V extends U>() {}

5、默认类型

就像函数参数指定默认值一样,声明泛型参数的时候也能指定默认类型

带有默认类型的泛型参数必须在没有默认类型的泛型参数之后

function makePair1<U, V = string>(first: U, second: V): [U, V] { // 编译正常
  return [first, second];
}

function makePair2<U = number, V>(first: U, second: V): [U, V] { // 编译错误
  return [first, second];
}

带有默认类型的泛型参数使用时遵循以下规则:

  1. 如有显式指定的参数,此时:
    1. 已被指定的参数,就要用指定的类型覆盖默认的类型
    2. 没被指定的参数,就要用默认的类型
  2. 如无显式指定的参数,此时:
    1. 如果能隐式推导,则会用推导的类型覆盖默认的类型
    2. 如不能隐式推导,则会用默认的类型
function makePair0<U = number, V = string>(first: U, second: V): [U, V] {
  return [first, second];
}

// 如有显式指定的参数:

// 第一个参数已被指定,使用指定的类型覆盖默认的类型,即 string
// 第二个参数已被指定,使用指定的类型覆盖默认的类型,即 number
makePair0<string, number>('0', 123);

// 第一个参数已被指定,使用指定的类型覆盖默认的类型,即 string
// 第二个参数没被指定,使用默认的类型,即 string【注意】
makePair0<string>('0', '0');

// 如无显式指定的参数:

// 此时可以做隐式推导,使用推导的类型覆盖默认的类型
makePair0('0', 123);  // 第一个参数为 number,第二个参数为 string
makePair0('0', '0');  // 第一个参数为 string,第二个参数为 string


好啦,本文到此结束,感谢您的阅读!

如果你觉得这篇文章有需要修改完善的地方,欢迎在评论区留下你宝贵的意见或者建议

如果你觉得这篇文章还不错的话,欢迎点赞、收藏、关注,你的支持是对我最大的鼓励 (/ω\)

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值