Published on

TypeScript - 泛型

Authors
  • avatar
    Name
    Deng Hua
    Twitter

目录

Introducing Generics

Generics allow us to define reusable functions and classes that work with multiple types rather than a single type.

在typescript中,泛型是一个特殊的功能,一种特殊的语法,它允许我们定义可重用的函数或可重要的类,这些函数或类可以与多种类型一起工作。

function doThing(thing: number | string): number | string {
  return '1'
}

doThing函数接收一个numberstring类型,返回的也是这两者类型的联合类型。

假设,我想升级一下这个函数,它接收一个任意类型的参数,并将这个任意类型的参数返回,我该如何定义呢?

function doThing(thing: /* 任意类型 */) : /* 任意类型 */ {
  // ...
}

我们无法穷举所有类型,因为TS允许我们使用Type aliasInterface定义一个新的类型或新的接口,无法再未知一个类型的前提下,将此类型在函数内注释。

这时,我们需要对类型进行”编程“了,类似的,在TS中这种为类型编程的也叫泛型

其实在TS的数组类型系统中,已经包含泛型系统了。

const nums: number[] = []

我们声明数组的时候,需要注明数组的成员类型。

当然,还有一种替代语法也能做到。

const nums: Array<number> = []

我们把鼠标移入到类型上,会显示:interface Array<T>

表示,这是一个interface,接口的定义为数组,类型为T(代表某种类型)

在这里,T就代表了number类型。

当然,我们也可以进行仿写,使用其他的类型。

const strArr: Array<string> = ['str']

这时,T就表示了string类型。

Another Example of A Built-In Generic

来看另一个内置的泛型系统

假设DOM中存在一个input元素,我们使用DOM API获取其,并写入对应的value属性。

const inputEl = document.querySelector('#username')

inputEl.value = 'Hacked!' // ❌ 类型“Element”上不存在属性“value”。ts(2339)

在TS中,这段代码会报错,将鼠标移入inputEl,TS提示它的类型为const inputEl: Element | null

TS只知道querySelector将会返回elementnullelement是最基础的DOM元素,也是我们可以获取的最基本的对象类型。

但是,作为coder的我们,更能进一步的知道,选择器是什么元素,一个div?一个input?还是一个button

所以这里我们要进步缩小范围:

const inputEl = document.querySelector<HTMLInputElement>('#username')

现在我们再查看inputEl的类型:const inputEl: HTMLInputElement | null

但是我们也不能直接获取inputElvalue属性,注意到,它是一个联合类型,还有可能为null。TS无法知道DOM中 是否真的有有一个ID为username的元素,如果你想更明确,可以使用Type Assertion

const inputEl = document.querySelector<HTMLInputElement>('#username')!

inputEl.value = 'Hacked!'

对于其他元素也是类似的

const buttonEl = document.querySelector<HTMLButtonElement>('.btn')

Writing Our Generic

我们来写一个自己的泛型

需求: 一个函数,接受一个任意类型的参数,并返回此参数。

function numberIdentity(item: number): number {
  return item
}
function stringIdentity(item: string): string {
  return item
}
function booleanIdentity(item: boolean): boolean {
  return item
}
// ...

难道我们要像这样写吗?当然不

这样?

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

当然也不行,这样就失去了使用TS的意义。这将跳过所有的类型检查。


在函数名后添加<Type>,其中 Type 用来指代任意输入的类型,参数定义item: Type和返回类型Type

接着在调用的时候,可以指定它具体的类型。

function identity<Type>(item: Type): Type {
  return item
}

identity<string>('str') // function identity<string>(item: string): string

identity<boolean>(true) // function identity<boolean>(item: boolean): boolean

identity<number>(1) // function identity<number>(item: number): number

也可以使用非内置类型

interface Cat {
  name: string
}

identity<Cat>({ name: 'tom' }) // function identity<Cat>(item: Cat): Cat

Writing Another Generic function

function getRandomElement<T>(list: T[]): T {
  // ...
}

getRandomElement函数接收一个数组,数组的类型为任意类型,并返回数组内的任意一个元素。

function getRandomElement<T>(list: T[]): T {
  const randomIdx = Math.floor(Math.random() * list.length)

  return list[randomIdx]
}

getRandomElement<string>(['a', 'b', 'c']) // function getRandomElement<string>(list: string[]): string

getRandomElement<number>([1, 2, 3]) // function getRandomElement<number>(list: number[]): number

interface Cat {
  name: string
}

getRandomElement<Cat>([{ name: 'tom' }, { name: 'tom2' }]) // function getRandomElement<Cat>(list: Cat[]): Cat

Inferred Generic Type Parameters

在许多情况下,TS实际上可以推断类型

let x = 123 // let x: number

同理,在泛型中,也会进行类型推断。

function getRandomElement<T>(list: T[]): T {
  const randomIdx = Math.floor(Math.random() * list.length)

  return list[randomIdx]
}

getRandomElement([1, 2, 3])
// function getRandomElement<number>(list: number[]): number

Generics, Arrow Functions, & TSX Files

如果在TSX文件中,上述定义的getRandomElement函数会引发错误

const getRandomElement = <T>(list: T[]): T => { // ❌ JSX 元素“T”没有相应的结束标记。
  const randomIdx = Math.floor(Math.random() * list.length)

  return list[randomIdx]
}

<T>加上一个,

const getRandomElement = <T,>(list: T[]): T => {
  const randomIdx = Math.floor(Math.random() * list.length)

  return list[randomIdx]
}

谨记: 如果在TSX文件中要定义一个泛型的箭头函数,需要注意这点。

Generics With Multiple types

多个类型参数的泛型函数

就像使用T表示第一个类型参数,我们也可以使用任意字母表示第二个类型参数,通常的使用U

function merge<T, U>(obj1: T, obj2: U) {
  return {
    ...obj1,
    ...obj2,
  }
}
const comboObj = merge({ name: 'colt' }, { pets: ['blue', 'elton'] })

查看comboObj的类型为const comboObj: { name: string;} & {pets: string[];},TS已经自动进行推断了。

Adding Type Constraints

对泛型进行类型参数约束

function merge<T, U>(obj1: T, obj2: U) {
  return {
    ...obj1,
    ...obj2,
  }
}

const comboObj = merge({ name: 'colt' }, 9)
// const comboObj: { name: string;} & 9

还是刚才的例子,假设merge函数需要接收两个对象类型,合并后并返回一个新的对象。但是并没有约束参数的类型。 可以对函数传任意类型的参数。

有什么方法能对泛型的类型参数进行约束呢?

可以使用extends关键字。

function merge<T extends object, U extends object>(obj1: T, obj2: U) {
  return {
    ...obj1,
    ...obj2,
  }
}

const comboObj = merge({ name: 'colt' }, 9) // ❌ 类型“number”的参数不能赋给类型“object”的参数。

它表明了,类型T不能是任意类型,它必须扩展自对象类型,类型U也必须扩展自对象类型。

这时候,不符合条件的参数传入TS就会报错了。

继承自object类型也可以使用自定义的interface定义。

interface Fruit {
  name: string
  color: string
}

function merge<T extends object, U extends Fruit>(obj1: T, obj2: U) {
  return {
    ...obj1,
    ...obj2,
  }
}

const comboObj = merge({ name: 'colt' }, { name: 'watermelon', color: 'green' })

使用接口扩展的另一个例子

interface Lengthy {
  length: number
}

function printDoubleLength<T>(thing: T): number {
  return thing.length // ❌ 类型“T”上不存在属性“length”
}

在这个例子中,类型参数T可能是任何类型,并不能从任意类型中推断出length的存在。

我们可以使用extends进一步扩展

interface Lengthy {
  length: number
}

function printDoubleLength<T extends Lengthy>(thing: T): number {
  return thing.length
}

现在无论什么类型,都要遵循Lengthy接口的规则。

当然对于这个例子,也有其他方式实现。

interface Lengthy {
  length: number
}

function otherPrintDoubleLength(thing: Lengthy): number {
  return thing.length
}

总之,当你需要用到泛型,并且需要对泛型进行约束时就可以这么做。

Default Type Parameters

为类型参数设置默认参数,默认类型。

function makeEmptyList<T>(): T[] {
  return []
}

这是一个类型为T的函数makeEmptyList,它返回一个T类型的数组。函数体返回一个空数组。但即便是返回一个空数组,TS也知道这是一个T类型的空数组,当然这么说可能会有一点奇怪。

我们查看makeEmptyList函数的类型,为function makeEmptyList<T>(): T[];

并且我们如果使用了别的类型,如:

const strings = makeEmptyList<string>()

此时移动鼠标到函数名称上查看makeEmptyList函数的类型: function makeEmptyList<string>(): string[]

如果我们什么类型都没传呢?

makeEmptyList() // function makeEmptyList<unknown>(): unknown[]

我们见得到一个unknown类型的数组。


如果想为类型T设置一个默认类型,可以在T后面加上=并表明需要设置的默认类型。

function makeEmptyList<T = number>(): T[] {
  return []
}

makeEmptyList() // function makeEmptyList<number>(): number[]

此时makeEmptyList函数参数T没有指定类型,将使用默认类型number。TS无法知道DOM中

Writing Generic Classes

关于泛型类

interface Song {
  title: string
  artist: string
}

interface Video {
  title: string
  creator: string
  resolution: string
}

class SongPlayList {
  public songs: Song[] = []
}
class VideoPlayList {
  public videos: Video[] = []
}

class PlayList<T> {
  public queue: T[] = []

  add(el: T) {
    this.queue.push(el)
  }
}

const songs = new PlayList<Song>()
songs.add({ title: 'imagine', artist: 'john lennon' })

const videos = new PlayList<Video>()
videos.add({ title: 'Oppenheimer', creator: 'Christopher Nolan', resolution: 'I-MAX' })