以为 TypeScript 泛型有点难,想系统学习 TypeScript 泛型相关知识的小伙伴们看过来,本文从八个方面入手,全方位带你一步步学习 TypeScript 中泛型,详细的内容大纲请看下图:javascript
动静(图)结合,在泛型学习之路助你一臂之力,还在犹豫什么,赶忙开启 TypeScript 泛型的学习之旅吧!前端
软件工程中,咱们不只要建立一致的定义良好的 API,同时也要考虑可重用性。 组件不只可以支持当前的数据类型,同时也能支持将来的数据类型,这在建立大型系统时为你提供了十分灵活的功能。java
在像 C# 和 Java 这样的语言中,可使用泛型来建立可重用的组件,一个组件能够支持多种类型的数据。 这样用户就能够以本身的数据类型来使用组件。node
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员能够是:类的实例成员、类的方法、函数参数和函数返回值。typescript
为了便于你们更好地理解上述的内容,咱们来举个例子,在这个例子中,咱们将一步步揭示泛型的做用。首先咱们来定义一个通用的 identity
函数,该函数接收一个参数并直接返回它:shell
function identity (value) {
return value;
}
console.log(identity(1)) // 1
复制代码
如今,咱们将 identity
函数作适当的调整,以支持 TypeScript 的 Number 类型的参数:数组
function identity (value: Number) : Number {
return value;
}
console.log(identity(1)) // 1
复制代码
这里 identity
的问题是咱们将 Number
类型分配给参数和返回类型,使该函数仅可用于该原始类型。但该函数并非可扩展或通用的,很明显这并非咱们所但愿的。安全
咱们确实能够把 Number
换成 any
,咱们失去了定义应该返回哪一种类型的能力,而且在这个过程当中使编译器失去了类型保护的做用。咱们的目标是让 identity
函数能够适用于任何特定的类型,为了实现这个目标,咱们可使用泛型来解决这个问题,具体实现方式以下:bash
function identity <T>(value: T) : T {
return value;
}
console.log(identity<Number>(1)) // 1
复制代码
对于刚接触 TypeScript 泛型的读者来讲,首次看到 <T>
语法会感到陌生。但这没什么可担忧的,就像传递参数同样,咱们传递了咱们想要用于特定函数调用的类型。微信
参考上面的图片,当咱们调用 identity<Number>(1)
,Number
类型就像参数 1
同样,它将在出现 T
的任何位置填充该类型。图中 <T>
内部的 T
被称为类型变量,它是咱们但愿传递给 identity 函数的类型占位符,同时它被分配给 value
参数用来代替它的类型:此时 T
充当的是类型,而不是特定的 Number 类型。
其中 T
表明 Type,在定义泛型时一般用做第一个类型变量名称。但实际上 T
能够用任何有效名称代替。除了 T
以外,如下是常见泛型变量表明的意思:
其实并非只能定义一个类型变量,咱们能够引入但愿定义的任何数量的类型变量。好比咱们引入一个新的类型变量 U
,用于扩展咱们定义的 identity
函数:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(68, "Semlinker"));
复制代码
除了为类型变量显式设定值以外,一种更常见的作法是使编译器自动选择这些类型,从而使代码更简洁。咱们能够彻底省略尖括号,好比:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity(68, "Semlinker"));
复制代码
对于上述代码,编译器足够聪明,可以知道咱们的参数类型,并将它们赋值给 T 和 U,而不须要开发人员显式指定它们。下面咱们来看张动图,直观地感觉一下类型传递的过程:
(图片来源:medium.com/better-prog…
感谢 @仑(前端搬砖党)指出,该动图有bug。
动态图最后一句错了吗?console.log(identity([1,2,3]))这里注入类型应该是number[]吧?
如你所见,该函数接收你传递给它的任何类型,使得咱们能够为不一样类型建立可重用的组件。如今咱们再来看一下 identity
函数:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
复制代码
相比以前定义的 identity
函数,新的 identity
函数增长了一个类型变量 U
,但该函数的返回类型咱们仍然使用 T
。若是咱们想要返回两种类型的对象该怎么办呢?针对这个问题,咱们有多种方案,其中一种就是使用元组,即为元组设置通用的类型:
function identity <T, U>(value: T, message: U) : [T, U] {
return [value, message];
}
复制代码
虽然使用元组解决了上述的问题,但有没有其它更好的方案呢?答案是有的,你可使用泛型接口。
为了解决上面提到的问题,首先让咱们建立一个用于的 identity
函数通用 Identities
接口:
interface Identities<V, M> {
value: V,
message: M
}
复制代码
在上述的 Identities
接口中,咱们引入了类型变量 V
和 M
,来进一步说明有效的字母均可以用于表示类型变量,以后咱们就能够将 Identities
接口做为 identity
函数的返回类型:
function identity<T, U> (value: T, message: U): Identities<T, U> {
console.log(value + ": " + typeof (value));
console.log(message + ": " + typeof (message));
let identities: Identities<T, U> = {
value,
message
};
return identities;
}
console.log(identity(68, "Semlinker"));
复制代码
以上代码成功运行后,在控制台会输出如下结果:
68: number
Semlinker: string
{value: 68, message: "Semlinker"}
复制代码
泛型除了能够应用在函数和接口以外,它也能够应用在类中,下面咱们就来看一下在类中如何使用泛型。
在类中使用泛型也很简单,咱们只须要在类名后面,使用 <T, ...>
的语法定义任意多个类型变量,具体示例以下:
interface GenericInterface<U> {
value: U
getIdentity: () => U
}
class IdentityClass<T> implements GenericInterface<T> {
value: T
constructor(value: T) {
this.value = value
}
getIdentity(): T {
return this.value
}
}
const myNumberClass = new IdentityClass<Number>(68);
console.log(myNumberClass.getIdentity()); // 68
const myStringClass = new IdentityClass<string>("Semlinker!");
console.log(myStringClass.getIdentity()); // Semlinker!
复制代码
接下来咱们以实例化 myNumberClass
为例,来分析一下其调用过程:
IdentityClass
对象时,咱们传入 Number
类型和构造函数参数值 68
;IdentityClass
类中,类型变量 T
的值变成 Number
类型;IdentityClass
类实现了 GenericInterface<T>
,而此时 T
表示 Number
类型,所以等价于该类实现了 GenericInterface<Number>
接口;GenericInterface<U>
接口来讲,类型变量 U
也变成了 Number
。这里我有意使用不一样的变量名,以代表类型值沿链向上传播,且与变量名无关。泛型类可确保在整个类中一致地使用指定的数据类型。好比,你可能已经注意到在使用 Typescript 的 React 项目中使用了如下约定:
type Props = {
className?: string
...
};
type State = {
submitted?: bool
...
};
class MyComponent extends React.Component<Props, State> {
...
}
复制代码
在以上代码中,咱们将泛型与 React 组件一块儿使用,以确保组件的 props 和 state 是类型安全的。
相信看到这里一些读者会有疑问,咱们在何时须要使用泛型呢?一般在决定是否使用泛型时,咱们有如下两个参考标准:
颇有可能你没有办法保证在项目早期就使用泛型的组件,可是随着项目的发展,组件的功能一般会被扩展。这种增长的可扩展性最终极可能会知足上述两个条件,在这种状况下,引入泛型将比复制组件来知足一系列数据类型更干净。
咱们将在本文的后面探讨更多知足这两个条件的用例。不过在这样作以前,让咱们先介绍一下 Typescript 泛型提供的其余功能。
有时咱们可能但愿限制每一个类型变量接受的类型数量,这就是泛型约束的做用。下面咱们来举几个例子,介绍一下如何使用泛型约束。
有时候,咱们但愿类型变量对应的类型上存在某些属性。这时,除非咱们显式地将特定属性定义为类型变量,不然编译器不会知道它们的存在。
一个很好的例子是在处理字符串或数组时,咱们会假设 length
属性是可用的。让咱们再次使用 identity
函数并尝试输出参数的长度:
function identity<T>(arg: T): T {
console.log(arg.length); // Error
return arg;
}
复制代码
在这种状况下,编译器将不会知道 T
确实含有 length
属性,尤为是在能够将任何类型赋给类型变量 T
的状况下。咱们须要作的就是让类型变量 extends
一个含有咱们所需属性的接口,好比这样:
interface Length {
length: number;
}
function identity<T extends Length>(arg: T): T {
console.log(arg.length); // 能够获取length属性
return arg;
}
复制代码
T extends Length
用于告诉编译器,咱们支持已经实现 Length
接口的任何类型。以后,当咱们使用不含有 length
属性的对象做为参数调用 identity
函数时,TypeScript 会提示相关的错误信息:
identity(68); // Error
// Argument of type '68' is not assignable to parameter of type 'Length'.(2345)
复制代码
此外,咱们还可使用 ,
号来分隔多种约束类型,好比:<T extends Length, Type2, Type3>
。而对于上述的 length
属性问题来讲,若是咱们显式地将变量设置为数组类型,也能够解决该问题,具体方式以下:
function identity<T>(arg: T[]): T[] {
console.log(arg.length);
return arg;
}
// or
function identity<T>(arg: Array<T>): Array<T> {
console.log(arg.length);
return arg;
}
复制代码
泛型约束的另外一个常见的使用场景就是检查对象上的键是否存在。不过在看具体示例以前,咱们得来了解一下 keyof
操做符,keyof
操做符是在 TypeScript 2.1 版本引入的,该操做符能够用于获取某种类型的全部键,其返回类型是联合类型。 "耳听为虚,眼见为实",咱们来举个 keyof
的使用示例:
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
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];
}
复制代码
在以上的 getProperty
函数中,咱们经过 K extends keyof T
确保参数 key 必定是对象中含有的键,这样就不会发生运行时错误。这是一个类型安全的解决方案,与简单调用 let value = obj[key];
不一样。
下面咱们来看一下如何使用 getProperty
函数:
enum Difficulty {
Easy,
Intermediate,
Hard
}
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
let tsInfo = {
name: "Typescript",
supersetOf: "Javascript",
difficulty: Difficulty.Intermediate
}
let difficulty: Difficulty =
getProperty(tsInfo, 'difficulty'); // OK
let supersetOf: string =
getProperty(tsInfo, 'superset_of'); // Error
复制代码
在以上示例中,对于 getProperty(tsInfo, 'superset_of')
这个表达式,TypeScript 编译器会提示如下错误信息:
Argument of type '"superset_of"' is not assignable to parameter of type
'"difficulty" | "name" | "supersetOf"'.(2345)
复制代码
很明显经过使用泛型约束,在编译阶段咱们就能够提早发现错误,大大提升了程序的健壮性和稳定性。接下来,咱们来介绍一下泛型参数默认类型。
在 TypeScript 2.3 之后,咱们能够为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也没法推断出类型时,这个默认类型就会起做用。
泛型参数默认类型与普通函数默认值相似,对应的语法很简单,即 <T=Default Type>
,对应的使用示例以下:
interface A<T=string> {
name: T;
}
const strA: A = { name: "Semlinker" };
const numB: A<number> = { name: 101 };
复制代码
泛型参数的默认类型遵循如下规则:
在 TypeScript 2.8 中引入了条件类型,使得咱们能够根据某些条件获得不一样的类型,这里所说的条件是类型兼容性约束。尽管以上代码中使用了 extends
关键字,也不必定要强制知足继承关系,而是检查是否知足结构兼容性。
条件类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:
T extends U ? X : Y
复制代码
以上表达式的意思是:若 T
可以赋值给 U
,那么类型是 X
,不然为 Y
。在条件类型表达式中,咱们一般还会结合 infer
关键字,实现类型抽取:
interface Dictionary<T = any> {
[key: string]: T;
}
type StrDict = Dictionary<string>
type DictMember<T> = T extends Dictionary<infer V> ? V : never
type StrDictMember = DictMember<StrDict> // string
复制代码
在上面示例中,当类型 T 知足 T extends Dictionary
约束时,咱们会使用 infer
关键字声明了一个类型变量 V,并返回该类型,不然返回 never
类型。
在 TypeScript 中,
never
类型表示的是那些永不存在的值的类型。 例如,never
类型是那些老是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。另外,须要注意的是,没有类型是
never
的子类型或能够赋值给never
类型(除了never
自己以外)。 即便any
也不能够赋值给never
。
除了上述的应用外,利用条件类型和 infer
关键字,咱们还能够方便地实现获取 Promise 对象的返回值类型,好比:
async function stringPromise() {
return "Hello, Semlinker!";
}
interface Person {
name: string;
age: number;
}
async function personPromise() {
return { name: "Semlinker", age: 30 } as Person;
}
type PromiseType<T> = (args: any[]) => Promise<T>;
type UnPromisify<T> = T extends PromiseType<infer U> ? U : never;
type extractStringPromise = UnPromisify<typeof stringPromise>; // string
type extractPersonPromise = UnPromisify<typeof personPromise>; // Person
复制代码
为了方便开发者 TypeScript 内置了一些经常使用的工具类型,好比 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考虑,这里咱们只简单介绍其中几个经常使用的工具类型。
Partial<T>
的做用就是将某个类型里的属性所有变为可选项 ?
。
定义:
/** * node_modules/typescript/lib/lib.es5.d.ts * Make all properties in T optional */
type Partial<T> = {
[P in keyof T]?: T[P];
};
复制代码
在以上代码中,首先经过 keyof T
拿到 T
的全部属性名,而后使用 in
进行遍历,将值赋给 P
,最后经过 T[P]
取得相应的属性值。中间的 ?
号,用于将全部属性变为可选。
示例:
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
title: "organize desk",
description: "clear clutter"
};
const todo2 = updateTodo(todo1, {
description: "throw out trash"
});
复制代码
在上面的 updateTodo
方法中,咱们利用 Partial<T>
工具类型,定义 fieldsToUpdate
的类型为 Partial<Todo>
,即:
{
title?: string | undefined;
description?: string | undefined;
}
复制代码
Record<K extends keyof any, T>
的做用是将 K
中全部的属性的值转化为 T
类型。
定义:
/** * node_modules/typescript/lib/lib.es5.d.ts * Construct a type with a set of properties K of type 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" }
};
复制代码
Pick<T, K extends keyof T>
的做用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。
定义:
// node_modules/typescript/lib/lib.es5.d.ts
/** * From T, pick a set of properties whose keys are in the union K */
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
};
复制代码
Exclude<T, U>
的做用是将某个类型中属于另外一个的类型移除掉。
定义:
// node_modules/typescript/lib/lib.es5.d.ts
/** * Exclude from T those types that are assignable to 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 复制代码
ReturnType<T>
的做用是用于获取函数 T
的返回类型。
定义:
// node_modules/typescript/lib/lib.es5.d.ts
/** * Obtain the return type of a function type */
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
复制代码
简单介绍了泛型工具类型,最后咱们来介绍如何使用泛型来建立对象。
有时,泛型类可能须要基于传入的泛型 T 来建立其类型相关的对象。好比:
class FirstClass {
id: number | undefined;
}
class SecondClass {
name: string | undefined;
}
class GenericCreator<T> {
create(): T {
return new T();
}
}
const creator1 = new GenericCreator<FirstClass>();
const firstClass: FirstClass = creator1.create();
const creator2 = new GenericCreator<SecondClass>();
const secondClass: SecondClass = creator2.create();
复制代码
在以上代码中,咱们定义了两个普通类和一个泛型类 GenericCreator<T>
。在通用的 GenericCreator
泛型类中,咱们定义了一个名为 create
的成员方法,该方法会使用 new 关键字来调用传入的实际类型的构造函数,来建立对应的对象。但惋惜的是,以上代码并不能正常运行,对于以上代码,在 TypeScript v3.9.2 编译器下会提示如下错误:
'T' only refers to a type, but is being used as a value here.
复制代码
这个错误的意思是:T
类型仅指类型,但此处被用做值。那么如何解决这个问题呢?根据 TypeScript 文档,为了使通用类可以建立 T 类型的对象,咱们须要经过其构造函数来引用 T 类型。对于上述问题,在介绍具体的解决方案前,咱们先来介绍一下构造签名。
在 TypeScript 接口中,你可使用 new
关键字来描述一个构造函数:
interface Point {
new (x: number, y: number): Point;
}
复制代码
以上接口中的 new (x: number, y: number)
咱们称之为构造签名,其语法以下:
ConstructSignature:
new
TypeParametersopt(
ParameterListopt)
TypeAnnotationopt
在上述的构造签名中,TypeParametersopt
、ParameterListopt
和 TypeAnnotationopt
分别表示:可选的类型参数、可选的参数列表和可选的类型注解。与该语法相对应的几种常见的使用形式以下:
new C
new C ( ... )
new C < ... > ( ... )
复制代码
介绍完构造签名,咱们再来介绍一个与之相关的概念,即构造函数类型。
在 TypeScript 语言规范中这样定义构造函数类型:
An object type containing one or more construct signatures is said to be a constructor type. Constructor types may be written using constructor type literals or by including construct signatures in object type literals.
经过规范中的描述信息,咱们能够得出如下结论:
那么什么是构造函数类型字面量呢?构造函数类型字面量是包含单个构造函数签名的对象类型的简写。具体来讲,构造函数类型字面量的形式以下:
new < T1, T2, ... > ( p1, p2, ... ) => R
复制代码
该形式与如下对象类型字面量是等价的:
{ new < T1, T2, ... > ( p1, p2, ... ) : R }
复制代码
下面咱们来举个实际的示例:
// 构造函数类型字面量
new (x: number, y: number) => Point
复制代码
等价于如下对象类型字面量:
{
new (x: number, y: number): Point;
}
复制代码
在介绍构造函数类型的应用前,咱们先来看个例子:
interface Point {
new (x: number, y: number): Point;
x: number;
y: number;
}
class Point2D implements Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const point: Point = new Point2D(1, 2);
复制代码
对于以上的代码,TypeScript 编译器会提示如下错误信息:
Class 'Point2D' incorrectly implements interface 'Point'.
Type 'Point2D' provides no match for the signature 'new (x: number, y: number): Point'.
复制代码
相信不少刚接触 TypeScript 不久的小伙伴都会遇到上述的问题。要解决这个问题,咱们就须要把对前面定义的 Point
接口进行分离,即把接口的属性和构造函数类型进行分离:
interface Point {
x: number;
y: number;
}
interface PointConstructor {
new (x: number, y: number): Point;
}
复制代码
完成接口拆分以后,除了前面已经定义的 Point2D
类以外,咱们又定义了一个 newPoint
工厂函数,该函数用于根据传入的 PointConstructor 类型的构造函数,来建立对应的 Point 对象。
class Point2D implements Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
function newPoint( pointConstructor: PointConstructor, x: number, y: number ): Point {
return new pointConstructor(x, y);
}
const point: Point = newPoint(Point2D, 1, 2);
复制代码
了解完构造签名和构造函数类型以后,下面咱们来开始解决上面遇到的问题,首先咱们须要重构一下 create
方法,具体以下所示:
class GenericCreator<T> {
create<T>(c: { new (): T }): T {
return new c();
}
}
复制代码
在以上代码中,咱们从新定义了 create
成员方法,根据该方法的签名,咱们能够知道该方法接收一个参数,其类型是构造函数类型,且该构造函数不包含任何参数,调用该构造函数后,会返回类型 T 的实例。
若是构造函数含有参数的话,好比包含一个 number
类型的参数时,咱们能够这样定义 create 方法:
create<T>(c: { new(a: number): T; }, num: number): T {
return new c(num);
}
复制代码
更新完 GenericCreator
泛型类,咱们就可使用下面的方式来建立 FirstClass
和 SecondClass
类的实例:
const creator1 = new GenericCreator<FirstClass>();
const firstClass: FirstClass = creator1.create(FirstClass);
const creator2 = new GenericCreator<SecondClass>();
const secondClass: SecondClass = creator2.create(SecondClass);
复制代码
建立了一个 “重学TypeScript” 的微信群,想加群的小伙伴,加我微信 "semlinker",备注重学TS。目前已有 TS 系列文章 41 篇。