泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。
简单来说,泛型就是解决 类、接口、方法的复用性,以及对不特定数据类型的支持。
一、泛型函数
泛型最常应用的应该就是泛型函数,泛型函数可以接收传递给它的任何类型,可以为不同类型创建可重用的组件。
function identity<T>(value: T): T {
return value;
}
console.log(identity<Number>(1)); // 1
console.log(identity<String>("1")); // "1"
console.log(identity<Boolean>(true)); // true
其中 T
代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T
可以用任何有效名称代替。除了 T
之外,以下是常见泛型变量代表的意思:
- K(Key):表示对象中的键类型;
- V(Value):表示对象中的值类型;
- E(Element):表示元素类型。
二、泛型接口
如果我们想要在泛型中返回两种类型的对象,可以使用泛型接口。
interface GenericIdentityFn<T> {
(arg: T): T;
}
interface ConfigFn{
(value1:string,value2:string):string;
}
let setData:ConfigFn=function(v1:string,v2:string):string{
return v1+v2;
}
setData('name','张三');
三、泛型类
泛型类可确保在整个类中一致地使用指定的数据类型。
class MinClass<T>{
public list: T[] = [];
add(value: T): void {
this.list.push(value);
}
min(): T {
let minNum = this.list[0];
for (let e of this.list) {
if (minNum > e) {
minNum = e
}
}
return minNum;
}
}
// number
let m = new MinClass<number>(); //实例化类,并且制定了类的代表类型是number
m.add(5);
m.min();
// string
let m1 = new MinClass<string>();
m1.add('d');
m1.min();
TypeScript 在React 中的属性规定。
type Props = {
className?: string
...
};
type State = {
submitted?: bool
...
};
class MyComponent extends React.Component<Props, State> {
...
}
在以上代码中,我们将泛型与 React 组件一起使用,以确保组件的 props 和 state 是类型安全的。
四、泛型约束
有时我们可能希望限制每个类型变量接受的类型数量,这就是泛型约束的作用。
1. 确保属性存在
有时对某个类型的泛型要调用该泛型内的方法,例如:
function echoWithArr<T>(arg: T): T {
console.log(arg.length)
return arg
}
如果此时传入的是number类型的参数,就没有length属性。
这里可以使用泛型约束了。
function echoWithArr<T>(arg: T[]): T[] {
console.log(arg.length)
return arg
}
const arr = echoWithArr([1, 2, 3])
上面代码中约束了传入的参数需要是任意类型的数组,但是,Object,String类型都是有length属性的,这时候就不满足这种场景了。这时候需要对泛型进行约束,允许这个函数传入包含length属性的变量(约束泛型):
interface IWithLength {
length: number
}
function echoWithLength<T extends IWithLength>(arg: T): T {
console.log(arg.length)
return arg
}
const len01 = echoWithLength('abc') // 3
const len02 = echoWithLength({ length: 12 }) // 12
const len03 = echoWithLength([1, 2, 3]) // 3
上面代码中,定义了一个接口IWithLength,在函数echoWithLength的泛型中使用extends关键字继承自这个接口,表示这个泛型必须要有length这个属性。
2. 检查对象上的键是否存在
泛型约束的另一个常见的使用场景就是检查对象上的键是否存在。这需要用到 keyof 操作符,keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。
interface Person {
name: string;
age: number;
gender: string;
}
type K1 = keyof Person; // "name" | "age" | "gender"
type K2 = keyof Person[]; // number | "length" | "push" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string | number
通过 keyof 操作符,我们就可以获取指定类型的所有键,之后我们就可以结合前面介绍的 extends 约束,即限制输入的属性名包含在 keyof 返回的联合类型中。具体的使用方式如下:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
通过使用泛型约束,在编译阶段我们就可以提前发现错误,大大提高了程序的健壮性和稳定性。
五、泛型参数默认类型
在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推断出类型时,这个默认类型就会起作用。
泛型参数默认类型与普通函数默认值类似,对应的语法很简单,即 <T=Default Type>
,对应的使用示例如下:
interface A<T=string> {
name: T;
}
const strA: A = { name: "Semlinker" };
const numB: A<number> = { name: 101 };
泛型参数的默认类型遵循以下规则:
- 有默认类型的类型参数被认为是可选的。
- 必选的类型参数不能在可选的类型参数后。
- 如果类型参数有约束,类型参数的默认类型必须满足这个约束。
- 当指定类型实参时,你只需要指定必选类型参数的类型实参。 未指定的类型参数会被解析为它们的默认类型。
- 如果指定了默认类型,且类型推断无法选择一个候选类型,那么将使用默认类型作为推断结果。
- 一个被现有类或接口合并的类或者接口的声明可以为现有类型参数引入默认类型。
- 一个被现有类或接口合并的类或者接口的声明可以引入新的类型参数,只要它指定了默认类型。
六、泛型工具类型
为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。这里我们只简单介绍其中几个常用的工具类型。
1. Partial
Partial<T>
的作用就是将某个类型里的属性全部变为可选项 ?
。
定义
type Partial<T> = {
[P in keyof T]?: T[P];
};
在以上代码中,首先通过 keyof T
拿到 T
的所有属性名,然后使用 in
进行遍历,将值赋给 P
,最后通过 T[P]
取得相应的属性值。中间的 ?
号,用于将所有属性变为可选。
interface Todo {
title: string;
description: string;
}
const a: Partial<Todo> = {
title: "1",
};
// a = { title?: string | undefined; description?: string | undefined; }
const c: Todo = {
description: "1",
};
// Error: 类型 "{ description: string; }" 中缺少属性 "title",但类型 "Todo" 中需要该属性
2. Record
Record<K extends keyof any, T>
的作用是将 K
中所有的属性的值转化为T
类型。
type Record<K extends keyof any, T> = {
[P in K]: T;
};
interface PageInfo {
title: string;
}
type Page = "home" | "about" | "contact";
const x: Record<Page, PageInfo> = {
about: { title: "about" },
contact: { title: "contact" },
home: { title: "home" }
};
3. Pick
Pick<T, K extends keyof T>
的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false
};
4. Exclude
Exclude<T, U>
的作用是将某个类型中属于另一个的类型移除掉。
type Exclude<T, U> = T extends U ? never : T;
如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T 类型。最终实现的效果就是将 T 中某些属于 U 的类型移除掉。
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number
5. ReturnType
ReturnType<T>
的作用是用于获取函数T
的返回类型。
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>; // {}
type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
type T4 = ReturnType<any>; // any
type T5 = ReturnType<never>; // any
type T6 = ReturnType<string>; // Error
type T7 = ReturnType<Function>; // Error
参考链接
TS之泛型:概述、泛型约束一文读懂 TypeScript 泛型及应用