typescript中使用泛型
介绍
这里引入官网一段介绍,了解个大概:
软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像C#和Java这样的语言中,可以使用
泛型
来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
认识泛型的作用
很多时候我们无法准确定义一个类型,它可以是多种类型,这种情况下我们习惯用 any 来指定它的类型,代表它可以是任意类型。any 虽好用,但是它并不是那么安全的,这时候应该更多考虑泛型。
为了理解泛型的作用,举个例子说明。我们来创建下面这样的一个函数,传入什么参数就返回什么参数,这个函数可以看成是一个 echo 命令:
function echoValue(arg: any): any { return arg }
为了不限制传入的参数类型,所以使用 any 类型。此函数咋一看是没问题的,但是缺丢失了一些信息,即传入的类型与返回的类型应该是相同的,使用 any 不能保证这一点。使用 any 不是一个安全的方案,比如我们来改变一下这个函数,返回传入值的 length :
function echoValue(arg: any): any { return arg.length }
这样写不会报任何错误,因为 arg 可以是任意值,所以不管做什么操作都是可以的。但如果函数传入的参数是 number 类型的,显然它是没有 length 属性的,那么执行时程序就会报错了。例子虽然很牵强,但也能说明问题,any 的不确定性,注定会带来各种问题,如果动不动就使用 any,那么也失去了使用 typescript 的意义。
现在我们使用泛型的方法来改写上面例子:
function echoValue<T>(arg: T): T { return arg }
T 是类型变量,它是一种特殊的变量,只用于表示类型而不是值,使用 <> 定义。定义了类型变量之后,你在函数中任何需要指定类型的地方使用 T 都代表这一种类型,这样也能保证返回值的类型与传入参数的类型是相同的了。
我们将这个版本的 echoValue 函数称作“泛型”,因为它适用于多种类型。定义了泛型函数后,有两种方法调用它,第一种明确指定 T 的类型:
echoValue<string>('hello world')
第二种方法就是直接调用,更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定 T 的类型:
echoValue('hello world')
当定义泛型时,不符合的操作都会报错,比如返回传入值的 length 时:
function echoValue<T>(arg: T): T { return arg.length // error,类型“T”上不存在属性“length” }
使用泛型变量
需要认识到泛型变量 T 可以是整个类型,也可以是某个类型的一部分,比如:
function echoValue<T>(arg: T[]): T[] { console.log(arg.length) return arg }
定义泛型变量 T,函数参数是各元素为 T 类型的数组类型,返回值是各元素为 T 类型的数组元素。
T 并不是固定的,你可以写成 A、B或者其他名字,而且可以在一个函数中定义多个泛型变量,如下面这个例子:
function getArray<T,U>(arg1: T, arg2: U): [T,U]{ return [arg1, arg2] }
我们定义了 T 和 U 两个泛型变量,第一个参数指定 T 类型,第二个参数指定 U 类型,函数返回一个元组包含类型 T 和 U。
泛型类型
我们可以定义一个泛型函数类型,泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面。
直接定义:
let echoValue: <T>(arg: T) => T = function<T>(arg: T): T { return arg }
使用类型别名定义:
type EchoValue = <T>(arg: T) => T let echoValue: EchoValue = function<T>(arg: T): T { return arg }
使用接口定义:
interface EchoValue{ <T>(arg: T): T } let echoValue: EchoValue = function<T>(arg: T): T { return arg } // 可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以 let echoValue2: EchoValue = function<U>(arg: U): U { return arg }
对于接口而言,我们可以把泛型参数当作整个接口的一个参数,这样我们就能清楚的知道使用的具体是哪个泛型类型。如下:
// 泛型变量作为接口的变量 interface EchoValue<T>{ (arg: T): T } let echoValue: EchoValue<string> = function<T>(arg: T): T { return arg } echoValue(123) // error,类型“123”的参数不能赋给类型“string”的参数 let echoValue2: EchoValue<number> = function<U>(arg: U): U { return arg } echoValue2(123)
泛型类
泛型类看上去与泛型接口差不多。 泛型类使用( <>
)括起泛型类型,跟在类名后面。
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T; } // T 为 number 类型 let myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function(x, y) { return x + y; }; // T 为 string 类型 let stringNumeric = new GenericNumber<string>(); stringNumeric.zeroValue = ""; stringNumeric.add = function(x, y) { return x + y; };
类有两部分:静态部分和实例部分, 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
泛型约束
我们有时在操作某值的属性时,是事先知道它具有此属性的,但是编译器不知道,就如上面有个例子,我们访问 arg.length 是行不通的:
function echoValue<T>(arg: T): T { console.log(arg.length) // 类型“T”上不存在属性“length” return arg }
现在我们可以通过泛型约束来对泛型变量进行约束,让它至少包含 length 这一属性,具体实现如下:
// 定义接口,接口规定必须有 length 这一属性 interface Lengthwise{ length: number } // 使用接口和 extends 关键字实现约束,此时 T 类型就必须包含 length 这一属性 function echoValue<T extends Lengthwise>(arg: T): T { console.log(arg.length) // 通过,因为被约束的 T 类型是包含 length 属性的 return arg }
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
echoValue(3) // 类型“3”的参数不能赋给类型“Lengthwise”的参数 echoValue({value: 3, length:10}) // right echoValue([1, 2, 3]) // right
泛型约束中使用类型参数
当我们定义一个对象,想对它做一个要求,即只能访问对象上存在的属性,该怎么做?来看看这个需求的样子:
const getProps = (obj, propName) => { return obj[propName] } const o = {a: 'aa', b: 'bb'} getProps(o, 'c') // undefined
const getProps = <T, K extends keyof T>(obj: T, propName: K) => { return obj[propName] } const o = {a: 'aa', b: 'bb'} getProps(o, 'c') // error,类型“"c"”的参数不能赋给类型“"a" | "b"”的参数