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
我们都知道当访问这个对象的’c’属性时,这个属性是没有的,但是在开发时是不会提醒报错的。在 typescript 中,我们可以实现对这个问题的检查,要使用到一个 keyof 关键字:
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"”的参数
这里我们使用让 K 来继承索引类型 keyof T,可以理解 keyof T 相当于一个由泛型变量 T 的属性名构成的联合类型,这里的 K 就被约束为了只能是 'a' 或 'b',所以当我们传入字符串 'c' 想要获取对应属性时就会报错。
posted @ 2020-05-12 15:18  黑色瓶子  阅读(4542)  评论(0编辑  收藏  举报