learn-typescript

泛型

泛型的定义

泛型(Generics),即 类型参数化。如果对某个参数的类型不明确,就可以使用泛型代替。

泛型入门

现在想要定义一个简单的 identity 方法,接收 number 类型参数, 返回参数本身:

function identity(arg: number): number {
  return arg
}

假设现在不明确参数 arg 的类型,可以使用 any 类型来定义:

function identity(arg: any): any {
  return arg
}

这样写存在问题,返回可以是 any 类型,不一定是参数 arg 的类型,同时也丢失了 arg 类型信息。
此时可以使用泛型来重写该方法:

function identity<T>(arg: T): T {
  return arg
}

函数名后加上 <T> 来表明该方法是 泛型方法T 是一个 类型参数(这里就是将类型定义成变量 T 了,不需要写死类型为 number等明确类型),类型参数可以定义为任意字符,常用的还有 KVE等,方法中可以使用定义过的类型参数作为类型使用。
identity 添加类型参数 T 之后, T 帮助我们捕获用户传入的类型(如:number),之后便可以使用这个类型。

使用泛型函数

使用泛型变量

  1. 使用泛型变量时,必须将其当作通用类型来看待
    例如 identity 方法中想要打印 arg 参数的长度,可能会这么做:
     function identity<T>(arg: T): T {
       console.log(arg.length)
       return arg
     } 
    

    这样是错误的,T 要当作通用类型看待,而如果传入的是 number类型,是没有 length属性的。

  2. 如果想要传入的参数是数组,可以这么定义:
    function identity<T>(arg: T[]): T[] {
      console.log(arg.length)
      return arg
    }
    

    或者

    function identity<T>(arg: Array<T>): Array<T> {
      console.log(arg.length)
      return arg
    }
    

泛型函数类型

泛型函数类型和普通函数类型类似,只需要在前面加上 类型参数

function identity<T>(arg: T): T {}

let foo: <U>(arg: U) => U  // 类型参数只需要对应即可,不必须为T
foo = identity

还可以使用 带有调用签名的对象字面量 来定义泛型函数:

let foo: { <T>(arg: T): T } 
foo = identity

泛型接口

把泛型类型的对象字面量形式拿出来作为一个接口即可:

interface IdentityInterface {
  <T>(arg: T): T
}

let foo: IdentityInterface = identity

泛型的类型参数还可以定义在接口名后面,作为整个接口的参数,来锁定整个接口中使用的类型:

interface GenericIdentityInterface<T> {
  (arg: T): T
}

function identity<T>(arg: T): T {}

let foo: GenericIdentityInterface<number>
foo = identity

除了 泛型接口,还可以创建 泛型类。但是没有泛型枚举和泛型命名空间。

泛型类

泛型类与泛型接口差不多:

class GenericNumber<T> {
  zeroValue?: T
  add?: (x: T, y: T) => T
}

let myGenericNumber = new GenericNumber<number>()
myGenericNumber.zeroValue = 0
myGenericNumber.add = function(x, y) {
  return x + y
}
console.log(myGenericNumber.add(myGenericNumber.zeroValue, 3)) // 3, 0 + 3

let stringNumeric = new GenericNumber<string>()
stringNumeric.zeroValue = ''
stringNumeric.add = function(x, y) {
  return x + y
}
console.log(stringNumeric.add(stringNumeric.zeroValue, 'abc')) // 'abc', '' + 'abc'

与泛型接口一样,将类型参数放在泛型类的类名后面,可以帮助确认类中所使用的类型。

注意: 类有静态部分(包括静态属性,静态方法,类的构造方法)和实例部分。而泛型只能用于类的实例部分

泛型约束

即对泛型的类型参数进行约束。例如之前的例子:

function identity<T>(arg: T): T {
  return arg
}

如果想要打印 arg.length 会报错,因为泛型 T 不一定具有 length 属性,此时可以让 T extends 某个接口,来强制约束 T 具有该属性:

interface Lengthwise {
  length: number
}

function identity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length)
  return arg
}

现在这个泛型函数被定义了约束,传入的参数必须具有 length 属性:

identity(3)  // Error, number doesn't have a .length property

identity({ value: 3, length: 1})  // OK

泛型约束中使用类型参数

可以声明一个类型参数,而它被另一个类型参数约束。
比如,想要用属性名从一个对象上获取该属性的值,并且想要确保属性名一定存在于该对象上,此时就需要在两个类型参数间使用约束:

function getProperty<T, K extends keyof T>(obj: T, key: K): any {
  return obj[key]
}

let obj = {
  a: 1,
  b: 'string',
  c: true,
  d: null
}

getProperty(obj, 'b')
// getProperty(obj, 'm')  // Error

泛型中使用类类型(即在泛型函数中使用类型参数对应的类名)

例如,想要定义一个 createInstance 函数创建特定的类的实例:

function createInstance<T>(c: new () => T): T {
  return new c()
}

更进一步,综合使用 泛型约束类类型,来限定创建的实例的类型:

interface Animal {
  name: string
}

class Cat implements Animal {
  name: string = 'cat'
}

class Mouse implements Animal {
  name: string = 'mouse'
}

function createInstance<T extends Animal>(c: new () => T): T {
  return new c()
}

createInstance(Cat).name // 'cat'
createInstance(Mouse).name // 'mouse'