- Published on
TypeScript - 泛型
- Authors
- Name
- Deng Hua
目录
- Introducing Generics
- Another Example of A Built-In Generic
- Writing Our Generic
- Writing Another Generic function
- Inferred Generic Type Parameters
- Generics, Arrow Functions, & TSX Files
- Generics With Multiple types
- Adding Type Constraints
- Default Type Parameters
- Writing Generic Classes
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
函数接收一个number
或string
类型,返回的也是这两者类型的联合类型。
假设,我想升级一下这个函数,它接收一个任意类型的参数,并将这个任意类型的参数返回,我该如何定义呢?
function doThing(thing: /* 任意类型 */) : /* 任意类型 */ {
// ...
}
我们无法穷举所有类型,因为TS允许我们使用Type alias
和Interface
定义一个新的类型或新的接口,无法再未知一个类型的前提下,将此类型在函数内注释。
这时,我们需要对类型进行”编程“了,类似的,在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
将会返回element
或null
,element
是最基础的DOM元素,也是我们可以获取的最基本的对象类型。
但是,作为coder的我们,更能进一步的知道,选择器是什么元素,一个div
?一个input
?还是一个button
。
所以这里我们要进步缩小范围:
const inputEl = document.querySelector<HTMLInputElement>('#username')
现在我们再查看inputEl
的类型:const inputEl: HTMLInputElement | null
但是我们也不能直接获取inputEl
的value
属性,注意到,它是一个联合类型,还有可能为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' })