单个类型参数
假设我们用一个函数,它可接受一个 number
参数并返回一个 number
参数。
function returnItem (param: number): number {
return param
}
我们按以上的写法貌似是没问题的,那么如果我们要接受一个 string
并返回同样一个 string
呢?逻辑是一样的,但是仅仅是类型发生了变化,难道需要再写一遍?
function returnItem (param: string): string {
return param
}
这明显是重复性的代码,我们应该如何才能避免上述情况呢?
难道我们只能用 any
表示了?
function returnItem (param: any): any {
return param
}
我们现在的情况是,我们在静态编写的时候并不确定传入的参数到底是什么类型,只有当在运行时传入参数后我们才能确定。
那么我们需要变量,这个变量代表了传入的类型,然后再返回这个变量,它是一种特殊的变量,只用于表示类型而不是值。
这个类型变量在 TypeScript
中就叫做「泛型」。
function returnItem<T>(param: T): T {
return param
}
我们在函数名称后面声明泛型变量 <T>
,它用于捕获开发者传入的参数类型(比如说string),然后我们就可以使用T
(也就是string)做参数类型和返回值类型了。
console.log(returnItem<string>("randy"));
console.log(returnItem<number>(1));
多个类型参数
定义泛型的时候,可以一次定义多个类型参数,多个类型参数用逗号分开。
比如我们可以同时定义泛型 T
和 泛型 U
:
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
console.log(swap<number, string>([27, "randy"])); // ['randy', 27]
注意:通常使用单个字母来命名泛型类型。这不是语法规则,我们也可以像 TypeScript 中的任何其他类型一样命名泛型,但这种约定有助于向阅读代码的人传达泛型类型不需要特定类型。
默认类型
定义泛型方法后,如果是简单方法,我们可以不用显示传递泛型类型TS
的类型推断会帮我们推算出来。但是复杂情况下每次都要我们显示传递泛型类型。
type Gen1 = {
name: string
}
async function genfun1() {
async function fetchApi<T>(path: string): Promise<T> {
const response = await fetch(path);
return response.json();
}
const result = await fetchApi<Gen1>('/test')
console.log(result.name) // OK
}
如果很多方法的返回值都是一样的,我们不想每次使用方法的时候都去传递一遍泛型类型该怎么办呢?
这就可以用到默认类型啦。使用形式跟默认参数一样,使用=
即可。
type Gen1 = {
name: string
}
async function genfun1() {
async function fetchApi<T = Gen1>(path: string): Promise<T> {
const response = await fetch(path);
return response.json();
}
const result = await fetchApi('/test')
console.log(result.name) // OK
}
泛型约束
现在有一个问题,我们的泛型现在似乎可以是任何类型,但是我们明明知道我们的传入的泛型属于哪一类,比如属于 number
或者 string
其中之一,那么应该如何约束泛型呢?
class Stack<T> {
private arr: T[] = []
public push(item: T) {
this.arr.push(item)
}
public pop() {
this.arr.pop()
}
}
我们可以用 <T extends xx>
的方式约束泛型,比如下图显示我们约束泛型为 number
或者 string
之一,当传入 boolean
类型的时候,就会报错。
type Union1 = string | number
class Stack2<T extends Union1> {
private arr: T[] = []
public push(item: T) {
this.arr.push(item)
}
public pop() {
this.arr.pop()
}
}
const stack2 = new Stack2<string>()
const stack3 = new Stack2<number>()
const stack4 = new Stack2<boolean>() // Error
泛型接口和泛型类
泛型接口
interface Inter1<T> {
param: T
}
const param1: Inter4<string> = {param: 123} // Error 不能将类型“number”分配给类型“string”。
const param2: Inter4<string> = {param: 'randy'} // OK
我们知道接口也是可以定义函数的,以上面的函数为例,如果我们将其转化为接口的形式。
interface ReturnItemFn<T> {
(param: T): T
}
那么当我们想传入一个number
作为参数的时候,就可以这样声明函数:
const returnItem: ReturnItemFn<number> = param => param
那么当我们想传入一个string
作为参数的时候,就可以这样声明函数:
const returnItem: ReturnItemFn<string> = param => param
其他类型都可以传递,是不是灵活性大大提升啦。
泛型类
泛型除了可以在函数中使用,还可以在类中使用,它既可以作用于类本身,也可以作用与类的成员函数。
我们假设要写一个栈
数据结构,它的简化版是这样的:
class Stack {
private arr: number[] = []
public push(item: number) {
this.arr.push(item)
}
public pop() {
this.arr.pop()
}
}
同样的问题,如果只是传入 number
类型就算了,可是需要不同的类型的时候,还得靠泛型的帮助。
class Stack<T> {
private arr: T[] = []
public push(item: T) {
this.arr.push(item)
}
public pop() {
this.arr.pop()
}
}
我们在实例化类的时候就可以传递各种类型啦。
const stack1 = new Stack<number>()
stack1.push(1)
const stack2 = new Stack<string>()
stack2.push('randy')
泛型类看上去与泛型接口差不多, 泛型类使用 <>
括起泛型类型,跟在类名后面。
索引类型
我们先看一个场景,现在我们需要一个 pick 函数,这个函数可以从对象上取出指定的属性,是的,就是类似于 lodash.pick
的方法。
在 JavaScript 中这个函数应该是这样的:
function pick(o, names) {
return names.map(n => o[n]);
}
如果我们从一个 user
对象中取出 id ,那么应该这样:
const user = {
username: 'randy',
id: 2300002033333333,
token: '230000201922222',
avatar: 'http://randy.jpg',
role: 'admin'
}
const res = pick(user, ['id'])
console.log(res) // [ '4600002033333333' ]
那么好了,我们应该如何在 TypeScript
中实现上述函数?结合我们之前学到的知识,你会怎么做?
如何描述 pick 函数的第一个参数 o
呢?你可能会想到之前提到过的可索引类型
,这个对象的 key
都是 string
而对应的值可能是任意类型,那么可以这样表示:
interface Obj {
[key: string]: any
}
而第二个参数 names
很明显是个字符串数组,这个函数其实很容易就用 TypeScript
写出来了:
function pick(o: Obj, names: string[]) {
return names.map(n => o[n]);
}
这样似乎没什么问题,但是如果你够细心的话,还是会发现我们的类型定义不够严谨:
- 参数
names
的成员应该是参数o
的属性,因此不应该是 string 这种宽泛的定义,应该更加准确 - 我们 pick 函数的返回值类型为
any[]
,其实可以更加精准,pick 的返回值类型应该是所取的属性值类型的联合类型
我们应该如何更精准的定义类型呢?
这里我们必须了解两个类型操作符:索引类型查询操作符和索引访问操作符。
索引类型查询操作符
索引类型查询操作符使用keyof
关键字。我们可以用 keyof
作用于泛型 T
上来获取泛型 T
上的所有 public
属性名构成联合类型。
class User6 {
public name: string;
public age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
// 对象键属性
type objKeys = keyof User6; // name | age
keyof
正是赋予了开发者查询索引类型的能力。
索引访问操作符
我们可以通过 keyof
查询索引类型的属性名,那么如何获取属性名对应的属性值类型呢?因为在上面提到的 pick 函数中,我们确实有一个需求时获取属性名对应的属性值类型的需求。
这就需要索引访问符出场了,与 JavaScript 种访问属性值的操作类似,访问类型的操作符也是通过 []
来访问的,即 T[K]
。
class User6 {
public name: string;
public age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
// 对象键属性
type objKeys = keyof User6; // name | age
// 对象值类型
type objValues = User6[objKeys] // string | number
当我们了解了这两个访问符之后,上面的问题就迎刃而解了。
首先我们需要一个泛型 T
它来代表传入的参数 o
的类型,因为我们在编写代码时无法确定参数 o
的类型到底是什么,所以在这种情况下要获取 o
的类型必须用面向未来的类型--泛型。
那么传入的第二个参数 names
,它的特点就是数组的成员必须由参数 o
的属性名称构成,这个时候我们很容易想到刚学习的操作符keyof
, keyof T
代表参数o类型的属性名的联合类型,我们的参数names的成员类型K
则只需要约束到keyof T
即可。
我们的返回值就更简单了,我们通过类型访问符T[K]
便可以取得对应属性值的类型,他们的数组T[K][]
正是返回值的类型。
function pick<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n]);
}
const res = pick(user, ['token', 'id', ])
我们用索引类型结合类型操作符完成了 TypeScript
版的 pick 函数,它不仅仅有更严谨的类型约束能力,也提供了更强大的代码提示能力:
映射类型
在了解映射类型之前,我们不妨看一个例子.
我们有一个User接口,现在有一个需求是把User
接口中的成员全部变成可选的,我们应该怎么做?难道要重新一个个:
前面加上?
,有没有更便捷的方法?
interface User {
username: string
id: number
token: string
avatar: string
role: string
}
这个时候映射类型就派上用场了,映射类型的语法是[K in Keys]
:
- K:类型变量,依次绑定到每个属性上,对应每个属性名的类型
- Keys:字符串字面量构成的联合类型,表示一组属性名(的类型)
那么我们应该如何操作呢?
首先,我们得找到Keys
,即字符串字面量构成的联合类型,这就得使用上一节我们提到的keyof
操作符,假设我们传入的类型是泛型T
,得到keyof T
,即传入类型T
的属性名的联合类型。
然后我们需要将keyof T
的属性名称一一映射出来[K in keyof T]
,如果我们要把所有的属性成员变为可选类型,那么需要T[K]
取出相应的属性值,最后我们重新生成一个可选的新类型{ [K in keyof T]?: T[K] }
。
用类型别名表示就是:
type partial<T> = { [K in keyof T]?: T[K] }
我们做下测试
type partialUser = partial<User>
果然所有的属性都变成了可选类型:
系列文章
TypeScript入门之类型推断、类型断言、双重断言、非空断言、确定赋值断言、类型守卫、类型别名
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!