TypeScript5-泛型

泛型是 TS 中一个重要的概念,它可以创建可复用的组件,同时保持对类型信息的一致性。

泛型提供了一种方式使得类型可以被参数化,这样就可以创建可以适用于各种数据类型的函数或类,而不仅仅限于一个数据类型。

一、泛型

先来看一个需求:

我有一个 compiler 函数,我希望这个函数可以接收一个字符串或一个字符串数组,返回值也是一个字符串或一个字符串数组,有什么办法?

结合多种编程语言,我们很容易想到几种解决方法:

1. 使用函数重载

在传统的面向对象编程语言中,例如 Java 或 C++,函数重载允许我们使用相同的函数名定义多个函数,只要它们的参数列表(参数的数量或类型)不同即可。在编译过程中,编译器会通过函数调用时的参数列表判断调用哪个重载函数。

在 JavaScript 中,如果尝试使用相同的函数名定义多个函数,后定义的函数将覆盖先定义的函数。

在 TS 中可以函数重载,如我们可以这样做:

function compliler(template: string): string;
function compliler(template: string[]): string[];
function compliler(template: any) {
    return template;
}
console.log(compliler('abc')); // "abc" 
console.log(compliler(['abc', 'efg'])); // ["abc", "efg"] 
// console.log(compliler(123)); // 报错

在这个例子中,compiler 函数有两个重载签名和一个实现函数。第一个重载签名接受一个字符串类型的参数,返回一个 字符串。第二个重载签名接受一个字符串数组类型的参数,返回一个字符串数组。

实现函数接受任意类型的参数,返回参数本身。这个实现函数必须能兼容所有的重载签名,所有对 compiler 函数的调用都会执行这个实现函数。

需要注意的是,TS 在解析函数重载时,会按照重载签名从上到下的顺序进行匹配,所以在编写重载签名时,应该把最精确的定义放在最前面。

使用这个方法可以解决这个需求,但是这么一个简单的需求,写出的代码看起来很麻烦,让人很不舒服,不够简洁,因此此方案作废。

2. 使用联合类型

使用联合类型也可以解决这个需求。

function compiler(template: string | string[]): string | string[] {
    return template;
}

我们定义了一个 compiler 函数,有一个字符串类型或字符串数组类型的参数,返回值可以是字符串,也可以是字符串数组。

这个方法和使用函数重载的方法相比,代码简洁了许多。但如果参数类型可以是很多种,比如 template 参数同时也可以是数字类型、布尔类型、数字数组等。虽然可以在参数的类型注解后面继续加上 | number | bool | number[],但这样代码的观感也不是很好。

在这个例子中,传入 compiler 函数的参数类型可以和返回值类型不一致。如果想让返回值的类型和参数的类型一致(如函数重载案例的效果),就可以使用类型变量。

3. 使用类型变量

类型变量是 TS 中的一种特殊的变量,只用于表示类型而不是值。通过使用类型变量,我们可以创建出在多种类型之间都可以共享的函数或组件。

// 类型变量
function compiler <T> (template: T): T {
    // ...
    return template;
}

尖括号中的 T 就是类型变量,它可以捕获用户传入的一些类型,允许我们跟踪函数中使用的这些类型的信息。这里我们定义了类型变量 T,就可以在函数定义中使用它。我们定义了参数 template 的类型为 T,返回值类型也为 T,这样就保证了参数类型和返回值类型一致。

在这个案例中,T 可以是任意类型,函数运行时也并不知道 T 是什么类型。、

为了解决这个问题,我们可以在调用函数时指定参数的类型。如下所示:

compiler<string>('123');

这里我们指定了传入的参数类型为字符串,后面传入的 '123' 就是一个字符串。

也可以直接传入 '123',TS 有类型推导机制,在编译时编译器会推导出传入的参数为字符串类型,T 这时也就是字符串类型,返回值的类型也自然就是字符串类型。

// 类型推导
compiler('123');

现在返回去看需求,传入的参数需要是字符串类型或字符串数组类型,我们就可以结合联合类型这样做:

compiler<string | string[]>('123');

这个案例中的 compiler 函数就是一个泛型函数。

二、泛型约束

来看下面的案例:

function compiler <T> (template: T): T {
    console.log(template.length);
    // ...
    return template;
}

compiler<string>('123');

编辑器可以通过我们传入的参数 '123' 来推断出 T 为字符串类型,但是编译时仍然会报错:Property 'length' does not exist on type 'T'.,表示类型 T 中不存在 length 这个属性。

我们可以使用继承接口的方法来实现泛型约束,通过接口来指定一个泛型必须具有某些属性或方法。

interface Len {
    length: number;
}

function compiler <T extends Len> (template: T): T {
    console.log(template.length);
    // ...
    return template;
}

compiler<string>('123');

这里我们先定义一个接口 Len,接口中有一个属性 length,再让类型变量 T 继承这个接口,编译器知道了类型 T 下有 length 这个变量,即可编译成功。

这里需要注意,使用这种方法,我们传入的这个参数必须要有 length 这个属性,否则会报错。如:

compiler([]); // 0
compiler(false); // 报错

第一行我们传入一个数组,我们都知道,数组是有 length 属性的,因此可以正确运行,输出 0。

第二行我们传入一个 false,为布尔类型,布尔类型没有 length 属性,所以会报错:Argument of type 'boolean' is not assignable to parameter of type 'Len'.,表示布尔类型和接口 Len 不匹配。

如果我们修改 Len 接口,在接口中增加一个 split 属性,类型为 函数:

interface Len {
    length: number;
    split: Function;
}

修改 Len 接口后,传入 compiler 的参数必须同时具有 length 属性和 split 函数。因此 compiler([]) 会报错,因为数组不具备 split 方法。

三、总结

使用泛型的优势:

1. 函数和类可以轻松的支持多种类型,增强程序的扩展性。

2. 不必写多条函数重载、联合类型声明,增强代码的可读性。

3. 灵活控制类型之间的约束。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晴雪月乔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值