TypeScript泛型概述
借用Java中泛型的释义可知泛型指的是类型参数化,通俗理解就是将原先的某种具体类型进行参数化。同理定义函数的参数,我们可以给泛型定义若干类型参数,并在调用时给泛型传入明确的类型参数。
更重要的是,设计泛型可有效约束类型成员之间的关系,比如函数参数和返回值、类或者接口成员和方法之间的关系。
(通过学习实践进行体会,如下)
首先定义一个通用 identity 函数,函数接收一个参数并直接返回该参数
function identity (value) {
return value;
}
console.log(identity(1))
对函数进行设置使其支持 Number 类型的参数
function identity (value: Number) : Number {
return value;
}
console.log(identity(1))
出现问题:我们将 Number 类型分配给参数和返回类型,使该函数只可用于该原始类型,该函数并不是可拓展的,这不是我们追求的。如果考虑将类型设为 any ,就会失去定义返回类型的能力,而且编译器还失去了类型保护的作用。目标应该是 identity 函数可以适用于任何特定的类型,为了实现此效果可以使用泛型,如下
function identity <T>(value: T) : T {
return value;
}
console.log(identity<Number>(1))
就像传递参数一样,我们传递了我们想要用于特定函数调用的类型
参考上图,当我们调用 identity<Number>(1) ,Number 类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中 <T> 内部的 T 被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给 value 参数用来代替它的类型:此时 T 充当的是类型,而不是特定的 Number 类型
T 代表 Type,在定义泛型时通常用作第一个类型变量名称,它也可以用任何有效名称代替。另外还有如 K(Key)表示对象中的键类型、V(Value)表示对象中的值类型、E(Element)表示元素类型。
继续引入一个新的类型变量 U ,拓展函数
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(123, "Hello"));
(而且除了为类型变量显示设定值外,更常用的方法还有使编译器自动选择类型,即可以省略掉
<Number, string>,编译器能够知道我们的参数类型,并将它们赋值给 T 和 U )
接着思考一个问题:新的函数增加了一个类型变量 U ,但该函数的返回类型我们仍然使用 T ,若想要返回两种类型的对象如何做?一种方式是采用元组,为元组设置通用的类型
function identity <T, U>(value: T, message: U) : [T, U] {
return [value, message];
}
但还有更合适的方式,即考虑采用泛型接口
泛型接口
针对前面想到的问题,考虑采用泛型接口,首先创建一个用于 identity 函数的通用 Identities 接口,之后就可以将该接口作为 identity 函数的返回类型
interface Identities<V, M> {
value: V,
message: M
}
function identity<T, U> (value: T, message: U): Identities<T, U> {
console.log(value + ": " + typeof (value));
console.log(message + ": " + typeof (message));
let identities: Identities<T, U> = {
value,
message
};
return identities;
}
console.log(identity(123, "Hello"));
(运行结果)
泛型类
想在类中使用泛型只需在类名后面使用 <T, ...> 的语法定义任意多个类型变量(实践示例如下)
interface GenericInterface<U> {
value: U
getIdentity: () => U
}
class IdentityClass<T> implements GenericInterface<T> {
value: T
constructor(value: T) {
this.value = value
}
getIdentity(): T {
return this.value
}
}
const myNumberClass = new IdentityClass<Number>(123);
console.log(myNumberClass.getIdentity());
const myStringClass = new IdentityClass<string>("Hello");
console.log(myStringClass.getIdentity());
在实例化 IdentityClass 对象时,我们传入 Number 类型和构造函数参数值 123 ,之后在 IdentityClass 类中,类型变量 T 的值变成 Number 类型。IdentityClass 类实现了
GenericInterface<T>, T 表示 Number 类型,因此等价于该类实现了GenericInterface<Number>接口。而对于GenericInterface<U>接口来说,类型变量 U 也变成了 Number 。
(积累学习)
泛型类可确保在整个类中一致地使用指定的数据类型。例如在使用 Typescript 的 React 项目中使用了以下约定
上面代码中,将泛型与 React 组件一起使用,以确保组件的 props 和 state 是类型安全的。
这时就会思考一个问题:何时需要使用泛型?参考标准有什么?
1、当函数、接口或类将处理多种数据类型时;2、当函数、接口或类在多个地方使用该数据类型时;
由于没有办法保证在项目早期就使用泛型的组件,但随着项目的开发,组件的功能通常会被扩展。这种增加的可扩展性最终很可能会满足上述两个条件,在这种情况下,引入泛型将比复制组件来满足一系列数据类型更清晰干净
类型约束
关键问题:如何使用类型约束来增加代码的灵活性和安全性?
泛型不仅可以提供灵活性,还可以增加代码的安全性,通过使用类型约束可以限制泛型参数的类型范围
(1)确保属性的存在
有时我们希望类型变量对应的类型上存在某些属性。但除非我们显式地将特定属性定义为类型变量,否则编译器不会知道它们的存在。举个例子:在处理字符串或数组时,我们假设 length 属性是可用的,再次使用 identity 函数并尝试输出参数的长度
function identity<T>(arg: T): T {
console.log(arg.length); // Error
return arg;
}
这种情况下,编译器不会知道 T 确实含有 length 属性,特别是在可以将任何类型赋给类型变量 T 的情况下。我们应该要让类型变量 extends 含有我们所需属性的接口
interface Length {
length: number;
}
function identity<T extends Length>(arg: T): T {
console.log(arg.length); // 可以获取length属性
return arg;
}
T extends Length 用于告诉编译器,我们支持已经实现 Length 接口的任何类型。以后如果使用不含有 length 属性的对象作为参数调用 identity 函数时,TypeScript 就会提示相关的错误信息。
另外还可以使用逗号来分隔多种约束类型,比如:<T extends Length, Type2, Type3>。对于上述 length 属性问题来说,如果显式地将变量设置为数组类型也可以达到目的。
(2)检查对象上的键是否存在
在实践操作前,先了解学习了 keyof 操作符, keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。
通过 keyof 操作符,就可以获取指定类型的所有键,接着就可以结合前面介绍的 extends 约束,即限制输入的属性名包含在 keyof 返回的联合类型中。
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
在 getProperty 函数中,通过 K extends keyof T 确保参数 key 一定是对象中含有的键,这样就不会发生运行时错误。这是一个类型安全的解决方案,与简单地调用 let value = obj[key]; 不同。
(使用 getProperty 函数)
通过实践可知,使用类型约束,我们在编译阶段就可以提前发现错误,提高了程序的稳定性和安全性。
总结
泛型是 TypeScript 的一强大特性,我们可以在函数、接口和类中使用泛型,使用类型约束来限制泛型参数的类型范围,增加代码的灵活性和安全性。在自己学习和实践 TypeScript 泛型的过程中,也发现有关泛型的知识点还有很多,需要继续积累和完善知识点,并通过代码的实践来加深理解。