觉得 TypeScript 泛型有点难,想体系学习 TypeScript 泛型相关知识的小伙伴们看过来,本文从八个方面下手,全方位带你一步步学习 TypeScript 中泛型,详细的内容纲要请看下图:

一文读懂 TypeScript 泛型及应用( 7.8K字)

动态(图)结合,在泛型学习之路助你一臂之力,还在犹疑什么,赶忙开启 TypeScript 泛型的学习之旅吧!

想入门 TypeScript 的小伙伴看过来,阿宝哥特意为你们预备的 —— 1.2W字 | 了不起的 TypeScript 入门教程(645+ 个)教程。

一、泛型是什么

软件工程中,咱们不只需创立一致的界说杰出的 API,一起也要考虑可重用性。 组件不只能够支撑当时的数据类型,一起也能支撑未来的数据, L 4 @ n H Z G类型,这在创立大型体系时为你供给K K 0了非常灵活的& 1 D M x功用。

在像 C# 和 Java 这样的语言中,能够运用泛型来创立可重用的组件,一个组件能够支撑多种类型的数据。 这样用户就能够以} o R H 0 h t 3 l自己的数据类型来运用组件。

设计泛型的要害目的是在成员之间供给有意义的束缚,这些成员能够是:类的实例成员、类的办法、函数参数和函数回来值。

为了便于咱们更U r v好地了解上述的t 5 I M ^ 8内容,咱们来举个比方,在这个比方中,咱们将一步步揭示泛, k o型的作用。首要咱们来界说一个通用的 identity 函数,该函数接纳一个参数并直接回来它:

function identic 8 U M Qty (value) {
return value;
}
console.log(identity(1)) // 1

现在,咱们将 identity 函数做恰当的调整F L v ~ m =,以支撑 TypeScript 的 Numberg h O K g ! 2 – 类型的参数:

function identity (value: Number) : Number {
return value;
}
console.log(identity(1))A v ~ = S S // 1

这儿 identity 的问题z F p , e O I R o是咱们将 Number 类型分配给参数和回来类型,使该函数仅可用于该原始类型。但该函数并不是可扩展或通用的,很明显这并不是咱们所希望的。

咱们的确能够把 Number 换成 any,咱们失去了界说应该回来哪种类型的能力,并且在* } P Q |这个进程中使编译器失去了类型保护的作用。咱们的方针是让 identity 函数能够适用于任何特定的类型,为了完成这个方针,咱们能够运用泛型来处理这个问题,~ q j J . R 6详细完成D ! v ^ (办法如: k i e下:

function identity <T>(value: Tq 4 ,) : T {
return value;
}
console.log(identity<Number>(1)) // 1

关于刚触摸 TypeScript 泛型的读者来说,首次看到 <T> 语法会感到陌生。但这没什么可忧虑的,就像传递参数相同,咱们传递了咱们想要用于特定函数调用的类型。

一文读懂 TypeScript 泛型及应用( 7.8K字)

参阅上面的图片,当咱们调用 ident= G g (ity<Number&A [ Q 1 S 3 Lgt;(1)Number 类型就像参数 1 相同,它将在出现 T 的任何方位填充该类型。图中 <T> 内部的 T 被称为类型变量– E L J * : f f,它是咱们希望传递给 identity 函数的类型占位符,一起它被分配给 value 参数用来替代它的类型:此刻 T( V 4 ! r ; [ w当的是类型,而不是特定的 Number 类型。

其间 T 代表 Type,在界说泛型时一般用作第一个类型变量称号。但实践上 T 能够用任何有用称号替代。除了 T 之外,以下是常见泛型变量代表的意思:

  • K(Key):表明目标中的键类型b { C E ! 2 a
  • V(Value):表明目标中的值类型;
  • E(ElementW I ` & _ @ * A):表明元素类型。

其实并不是只能界说一个类型变量,咱们能够引进希望 ( X界说的K W & ~任何数量的类型变量p _ V E B _。比方咱们引进一个新的类型变量 UN 8 _ i i f,用于扩展咱们界说的 identity 函数:

function identity <T, U>(vald P r e + ] U eue: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(68, "Semlinker"));
一文读懂 TypeScript 泛型及应用( 7.8K字)

除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动挑选这些类型,从而使代码更简练。咱们能够彻底省掉尖z M 9 r r v ;括号,比方:

function identity <T, U>(value: T, messagd } m t Y { R j ee: U) : T {
console.log(messag`  F g = i + je);
retup [ # & $rn value;
}
console.log(identity(68,B P = ] W G J R z "Semlinker"));

关于上述代码,编译器足够聪明,能够知道咱h * _ ` a n们的参数类型,并将它们赋值给 T 和 U,而不需求开发人员显式指定它们。下面咱们来看张动图,直观地感受一下类型传递的进程:

一文读懂 TypeScript 泛型及应用( 7.8K字)

(图片来源:medium.l 1 ) qcom/better-prog…

感谢 @仑(前端搬砖党)指出,该动图有bug。

动态图最终一句错了吗?console.log(identity([1( ^ j R t,2,3]))这儿注| ~ – J入类型应该是number[]吧?

如你所见,该函数接纳你传递给它的任何F , Y M y N L 1类型,使得咱们能够为不同类型创立可重用的组件。现在咱们再来看一下 identity 函数:

function identity <T, U>(value:} B s Y k k ` L T, message: U) : T {
console.log(message);
return value;
}

相比之前界说W ? k . 6 pidentiu J ~ + i K Z b (ty 函数,新的 identity 函数添加了一个类型变量 U,但该函数的回来类型咱们依然运用 T。假如咱们想要回来两种类型的目标该怎么办呢?针对这个问题,咱们有多种计划,其间一种便是运用元组,即为元组设置通用的类型:

function identity <T,u U q Z # D ; U>(value: T, message: U) : [T, U] {
return [value, message];
}

尽管运用元组处理了上述的问题,但有没有其它更好的计划呢?答案是有的,你能够运用Z l v P ) Q # Y泛型接口。

二、泛型接口

为了处理上面提到的问题,首要让咱们创立一个用于的 identity 函数通用 Identities 接口:

interface Identities<V, M> {
valO 6 D ? ? 5 N P fue: V,
message: M
}

在上述的 Identities 接口中,咱们引进了类型变量 VM,来进一步说明有用的字母都能够用于表明类型变量,之后咱们就能够将 Identities 接口作为 ip ~ y 5dentity 函数的回来类型:

functiof s 4n iden? c # G KtityD { W U : O 6 ; $<T, U> (value: T, message: U): Identities<x - | D F p z d;T,x Z ! U> {
console.log(value + ": " + typeof (valu: Z A y P ;e));
console.log(message + ": " + typeof (message));
let identities: Identities<T, U> = {
value,
message
}? 7 E K j;
returH T - E : G R 9 in identities;
}
console.log(identity(68, "Semlinker"));

以上代码成功运转后,在控制台会输出以下成果:

68: number
Semlinker: string
{value: 68, messaT ) I C ] * Jge:% ~ A 7 A ~ 2 ) "SemlinkeJ c $ Wr"}

泛型除了能够运用在函数和接口之外,它也能够运用在类中,下面咱们就来看一下在类中怎么运用泛型。

三、泛型类

在类中运用泛型也很简略,咱们只需求在类名后面,运用 <T, ...> 的语法界说任意多个类型变量,详细示例如下:

interface GenericInterface<U>` Z } *; {
value: U
getIdentity: () => U
}
class IdentityClass<: a %T> imps ? b d U V N hlements GenericIntb h 3erfb $ 6 5ace<T> {
value: T
constructor(value: T)4 i _ | 3 a ] {
this.value = value
}
getIdentity(): T^ x 1 5 ( _ X ` {
return this.valy r 7 K 1 l . cue
}
}
const myNumberCs F lass = new IdentityClass<Number>(68);
console.log(myNumberClass.ge5 U b E ZtIdentity()); // 68
const myStringClass = new IdentityClass<string>("Semlinker!");
cop o * rnsole.log(myStringClass.getIdentity()); //0 N _ z l * G Semlinker!

接下来咱们以实例化 myNumberClass 为例,来剖析一下其调用进程:

  • 在实例化 IdentityClass 目标时,咱们传入 Number 类型和结构函数参数值 68
  • 之后在 IdentityClass 类中,类型变量 T 的值变成 Number 类型;
  • IdentityClass 类完成了 GenericInterface<T>,而此刻 T 表明 NI y 0umber 类型,因h w C @ { Mg 7 # E L等价于该类完成了 GenericInterfae C e G F F ] Fce<Number> 接口;
  • 而关9 ] a 7 2 [GenericInterface<U> 接口来说,类型变量 U 也变成了 Number。这儿我有意运用不同的变量名K K | b & m 9 ;,以表明类型值沿链向上传达,且与变量名无关。

泛型类可保证在整个类中一致地. k * 4 u运用指定的数据类型。比方,你或许现已留意到在运用 Typ. h ! ^ k L ( – :escript 的 React 项目中运用了以下约好:

type Props = {
className?: string
...
}t s !;
typer + : } ? v State = {
submitted?: bool
...
};
class MyComponent extends React.Component<Props, State> {
..J K ] * g m K Q 9.
}

在以上代码中,咱们将泛型与 React 组件一起运用,以保证组件的 props 和 state 是类型安全的{ ) S 7 8

相信看到这儿一些读者会有疑问,咱们在什么时候需求运用泛型呢?一般在决定是否运用B P 5 / e O泛型时,咱们, s ] –有以下两个参阅标准:

  • 当你的函数、接口或R ! * x ! X D |类将处理多种数据类型时;
  • 当函数、接口或类在多个地方运用该d n e l数据类型时。

很有或许你没有办法保证在项目前期就运用泛型的组件,可是随着项目的开展,组件的功用一般会被扩展。这种添加的可扩展性终究很或许4 6 O L f S会满意上述两个条件,在这种情况下,引进泛型t e T K U p rh f l G ? Y 3 M N比复制组件来满意一系列数据类型更洁净。

咱们将在o R h H w f本文的后面探讨更多满意这两个条件的用例。不过在这样做之前,让咱l z g 0 L们先介绍一下 Typescript 泛型供给的其他功用。

四、泛型q t s v k L Y束缚

有时咱们或许希望限j ( ` | –制每个类型变量接受的类型数量,这便是泛型束缚的作用。下面咱们来举几个比方,介绍一下怎么运用泛型束缚。

4.1 保证特点存在

有时候,咱们希望类型变量对应的类型上存在某些特点。这时,除非咱们显式地将特定特点界说为类型变量,否则编译器不会知道它们的B K c } ? 8 E H存在。

一个很好的比方是在处理字符串或数组时,咱2 + _ # @ H L们会假设 length 特点是可用的。让咱们再次运用 identity 函数并测验输出参数的2 S d ~长度:

function identity<T&gJ i N -t;(arg: T): T {
console.log(arg.length); // Error
return arg;
}

在这种情况下,编译器将不会知道 T 的确含有 length 特点,尤其是在能够将任何类型赋给类型变量 T 的情O ; x ) +况下。咱们需求做的便是让类型变量 extends 一个含有咱们所需特点的接口,比方这样:

interface Lengt: z / oh {
length: numberE u U | : ` l };
}
f& i p  e Vunction identity<T extends Length>(arg: T): T {
consb p 3 #ole.log(arg.length); // 能够获取length特点
return arg;
}

T extends Length 用于告诉编译器,咱们支撑现已完成 Length 接口的任何类型。之后` } {,当咱们运用不含有 lenV q lgth 特点的目标作为参数调用 identity 函数时,Type] 8 1 S ] c ( )Script 会提示相关的过错信息:

identity(68); // Error
// Argument of type '68' is^ K & l Q j h ^ & not assignable t m 4 !o parametei `  f qr of type 'Length'.(25 6 | n345)

此外,咱们还能够运用 , 号来分隔多种束缚类型,比方:<T ex/ i @ .tends Length, Type2, Type3>。而关于上述的 length 特点问题来说,假如咱们显式地将变量设置为数组类型,也能I b y K够处理该问题,详细办法如下} [ V # s { $

functi3 r ? g T U M hon identitya I S<T>(arg: T[]): T[] {
console.d @ _ Y V B u Z tlog(arg.length);
retF 2 b . O W } 0 &urn9 ~ W j arg;
}
// or
function identity<T>(arg: Array<T>): Array<T> {
console.logr P _ % U d(arg.length): A I;
retur/ j Y ) | `n arg;
}

4.2 检查目标上的键是否存在

泛型束缚的另一个常见的运用场景便是检查目标上的键是否存在。不过在看详细示例之前,咱们得来了解一下 keyof 操作符,keyof 操作符是在 TypeScript 2.1 版本引进的,该操作符能够用于获取某种类型的一切键,其回来类型是联合类型。 “耳听为虚,眼见为实”,咱们来举个 keyof 的运用示例:

interface Person {
name: str, r [ u xing;
age: number;
location: string;
}
type K1 = keyoa y ff Person; // "name" | "agZ 7 X ? % W Ie" | "location"
type K2 = keyof Person[];  // number | "length" | "push"h b )  | "concat" |r u ( ...
type9 & N v B ) 9 K3 = keyof {S @ k n N F [x: string]: Person };  // string | number

经过 keyof 操作符,咱们就能够获取指定类型的一切键,之后咱们就能够结合前面介绍的 extends 束缚,即限制输入的特点名包括在 keyof 回来的联合类型中。详细的运用办法如下:

function getProperty<T, K extends keyof T>(obj: T, ky r ley: K): T[K] {
return obj[key];
}

在以上的 getProperty 函数中,咱} # 5 X们经过 K extends keyof T% W R @ } V O 保证参数 key 一定是目标中含有的键,这样就不会发生运转时过错。这是一个类型安全的处理计划,与简略调用 let value = obj[key]; 不同。

下面咱们来看一下怎么运用 getProperty 函数:

e} C p znum 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",
difficd 4 t ; *ulty: Difficultyi O K O P.Intermedia0 f } 3 2 % Ate
}
let difficulty: Diff# b wiculty =
getPropF ! 6 R X l F f +erty(tsIf C [ Y E & vnfo, 'difficulty'); // OK
let super[ 2 r hsetOf: string =
getProperty(tsInfo, 'superset_of'); // Error

在以上示例中,关于 getProperty(tsInfo, 'superset_of') 这个表达式,TypeScript 编译器会提示以下过错信息:

Argument of type '"supersem ` 9 N 8 t_of"' is not assC p ) D r I u $ dignable to paramK ` _ # $ ( , 1 Beter of type
'"difficulty" | "name" | "suM 6 0 2 { q bperi = ?setOf"'.(2345)

很明显经过运用泛型束缚,在编译阶段咱们就能够提早发现过错,大大提高了程序的q H o健壮性和稳定性。接下来,咱们来介绍一下泛型参数默许类型6 U Z U X h W

五、泛型参数默许类型

在 TypeScript 2.3 今后,咱们能h d [ 8 X | t .够为泛型中的类型参数指定默许类型。当运用泛型时没有在代码中直接指定类型参数,从实践值参数中也无法揣度出类型时,这个默许类型就会起作用。

泛型参@ x _ ? $ a数默许类型与一般函数默许值类似,对应的语法很简略,即 <T=Default Type>,对g e 2 /应的运用示例如下:

interface A<T=string> {
name: T;
}
const strA: A = { name: "Semlinker" };
const numB: A<number> = { name: 101 };

泛型参数的默许类型遵循以下规矩:

  • 有默许类型的类型参数被认为是可选的。
  • 必选的类型参数不能在可选的类型参数后。
  • 假如类型参数有束缚,类型参数的默许类型有必要满意这个束缚。
  • 当指定类型实参时,你只需求指定必选类型参数的类型实参。 未指定的类型参数会被解析为它们的默许类型。
  • 假如指定了默许类型,且类型揣度无法挑选一q ^ r M个候选类型,那么将运用默许类型f N l s作为揣度成果。
  • 一个被现有类或接口合并的类或许接口的声明能够为现有类型参数引进默许类型。
  • 一个被现有类或接口合并的类或许接口的声明能够引进新的类型参数,只需它指定了默许类型。

# _ 0 % T E p、泛型条件类型

在 TypeScript 2.8 中引进了条件类型,使得咱们能够依据某些条件得到不同的类型,这儿所说的条件 ) g { A ? d U {是类型兼容性束缚。尽管以上代码中运用了 extends 要害字,也纷歧定要强制满意承继联系,而是检查是否满意结构兼容性。

条件类型会以一个条件表达式进行类型联系检测a r B o,从而在两种类型中挑选其一:

T extends U ? X : Y

以上表达式的意思是:若 T 能够赋值给 U_ ) e – B r那么类型是 X,否则为 Y。在条件类型表达式D g ~ 1 7 R U #中,咱们一般还会结合 i( d ) | A 6 ~ ]nfer 要害字,完成类型抽取:

interface Dictionaw k V l I p r [ 1ry<T = any> {
[key: string]: T;
}
type StrDict = Dictionary&c + s @lt;string>
type DictMember<[ % * l ; C `;T&F p g  O ` [ |gt; = T ex- l : 7 ltends Dictionary<infer V> ? V : never
type StrDictMember = DictMember<StrDict> // string

在上面示例中,当类型 T 满意 T extends Dictionary 束缚时,咱们会运用 infer 要害字声明晰一个类型变量 V,并回来该类型,否则回来 never 类型。

在 TypK ] EeScript 中,never 类型表明的是Q { 3 E ( u 9 3那些永不存在的值的类型。 例如, never 类型是那些总是会抛出反常或底子就不会有回来值的函数表达式或箭头函数表达式的回来值类型。

另外,需求留意的是,没有类型是 never 的子类型或能够赋值给 never 类型w V *(除了 never 自身之外)。 即便 any 也不能够赋值给 never

@ R 5了上述的运用外,运用条件类型和 infer 要害字,咱们还能够方便地完成获取 Promise 目标的回来值类型– * z y { J ` $,比方:

async function st( 5 L R k % m ,ringPromi. L ( + W Hse() {
return "Hello, Semlinker!";
}
interface Person {
name: string;
age: number;
}
async function personPromise() {
return { name: "SA J 8 Xemlinker", age: 30 } as Person;
}
type PromiseType<T> = (args: any[]) => Promise<T>;W ` u q 6 -
type UnPromisify<T&gt W 5 ^ jt; = T ext[ Z 1ends PromiseType<infer U> ? U : never;
type extrac3 M @ q H KtStringPromise = UnProG 1 S 1 | o z Z +misify<typeof stringPromise>; // string
type extractPersonProm; S f E tise = UnPromisify<typeof personPromise>; // Person

七、泛型东西类型

为了方便开发者 TypeScri/ + 7 c jpt 内置了一些常用的东西类型,比方 Partial、Required、Readonly、Record 和 ReturnType 等。出于@ V @ R } ] O `篇幅考虑,这儿咱们只简略介绍其间几个常用的东西类型} 2 H % } B E 8

7.1 Partial

Partial<T> 的作用便是将某个类型s K w i A J u = d里的特点全部变为可选项 ?

) A + R c M 7 S $说:

/**
* node_modules/5 e . 8tyk 2 B k x B . : 5pescript/lib/libf , ^ E K.es5.d.ts
* Make all properties in T optional
*/1 P i L + c X
typ1 N me Partial<T> = {
[P in keyof T]?: T[P];
};

在以上代码中,首要经过 keyof T 拿到 T 的一切特点名,然后运用 in 进行遍历,将值赋给 P,最终经@ ? ) W } q $ T ^T[P] 获得相应o v W的特点值。中间的 ? 号,用于将一切特点变为可选。

示例:

interface Todo {
title: s% H y ] k F :tring;
description: string;
}
function update_ ( S 0 Z # +Todo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
title: "o+ # ` rganize desk",
description: "clear clutter"
};
const todo2 = updateTodo(todo1, {
description: "throw out; * / n G O trash"
});

在上面的 updateJ i Y HTodo 办法中,咱们运用 Partial<T> 东西类型,界说 fieldsToUpdate 的类型为 Partial<Todo>,即:

{
title?: string | undefined;
description?: stE ; Z / K 3 R @ring | undefineh h c 5 O f M e md;
}

7.2 Record

Record<K extends keyof any, T> 的作用是将 K 中一切的特点的值转化为 T 类型。

界说:

/**
* node_modules/typescript/lib/lib.es5.d.ts
* C* * = l [onstruct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};

示例:

interfH 8 c b ; H Vace PageInfo {
title: string;
}
type Page = "home" | "about" | "contact";
const x: Record<Page, PageInf: i r do> = {
about: { title: "about" },
contact: { title: # x x A S "C # & ^ = i %contat p M % . k N `ct" },
home: { title: "home" }
};

7.3 Pick

Pick<T, K eq ~ A 7xtends keyof T> 的作用是将某个类型中5 @ L c d _ J t K的子特点挑出来,变成包括这个类型部分特点的子类型。

界说:

// node_modules/typescriptM m w A 8 G 2 d/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;
comC j # y ) T ipletV s j % u G Ged: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
constg ? + todo: TodoPreview = {
title: "Clean room",
completed:k ~ I s g false
};

7J + 5 0 l Q @.4 Exclude

Exclude<T, U? K $ . h f O> 的作用是将某个类型中归于另一p v % 7个的类型移除掉。

界说:

// node_modules/typescript/lib/lib.es5.d.ts
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T exB [ L o # L S B Ctends U ? never : T;

假如 T 能赋值给 U 类型的话,那么就会回来 never 类型,否则回来 T 类型。终究完成的效果便是将 T 中某些归于 U 的类型移 – h除掉。

示例:

type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclud{ | . 5 je<"a" | "b" |, : H k { "c", r 0 ! I j ^ f ) B"a" | "b">; // "c"
type T2 = Exclude<string | numbeR * [ Z fr | (() =&gm C j 0 l N Wt; void), FP g  ]unction>; /O C R E  U g Q 7/ string | numberf & a ? @

7.5 ReturnType

ReturnType<T> 的作用是用于获取函数 T 的回来类i ^ % + ? = V ~ Y型。

界说:

// node_modules/typescript/lib/lib.es5.d.ts
/**
* Obtain the return type of a function type
*/_ J B 2 X  r h @
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

示例:

type! 0 L P h t y T0 = Re/ R + V uturnType<() => string>; // string
type T1 = ReturnType&lO - 9 jt;(s: string) => voi) 5 m a gd>; // void
type T2 = ReturnType<<T>() => T>; // {}
typem / H f d t T3 = ReturnType<<T extends U, U extends numbx { 2 Q A 6 Mer[]>() => T>; // number[]
type T4 =W o 8 { ( j b u C ReturnTZ F Z X H s i r Uype<any>; // any
type Te q {5 = Returnr u t a G z T NType&) x n . } o 7lt;never>; // any
type+ K P J r m s , G T6 = ReturnType<string>; // Error
type T7 = ReturnType<Function>; // Err1 B G K 5 +or

简略介绍了泛型东西类型,最终咱们来介绍怎么运用泛型来创立目标。

八、运用泛型创立目标c S : – W

8.1 结构签名

有时,泛型类或许需求依据传入的泛型 T 来创立其类型相关的目标。比方:

class FirstClass {
id: number | undefined;
}
class SecondClass {
name: string | undefin0 ( I ?ed;
}
class GenericCreator<T> {
create(): TY C 0 5 ~ b  1 {
return new T();
}
}
const c, m o Rreator1 = new GenericCreator<FirstClassH r O w X ) z J i>();
const firstClass: FirstClass = creator1.create();
coX C G U F @ Enst creator2 = new GenericCreator<SecondClass>();
const secondClassJ P Q H % ( 4 (: SecondClass, T o  = crI G m M Q d @eator2.create();

在以上代码中,咱们界说了两个一般类N L m和一个泛型类 GenericCrea4 3 l w $ Rtor<T>。在通用的 GenericCreator 泛型类中,咱们界说了一个名为 crea! Z ] @ Wte 的成员办法,该办法7 T _会运用 new 要害字来调用传入的实践类型的结构函数,来创立对应的目标。但惋惜的是,以上代码并不能正常运转,关于以上代码,在 TypeScr3 E W e *ipt v3.9.2 编译器下会提示以下过错:

'T' only refK ) [ e : n X 7 6ers to a type, but is being used as a value here.

这个过错的意思是:T 类型仅指类型,但此处被用作值。那么怎么处理这个问题呢?依据 TypeScript 文档,为了使通用类能够创立 T 类型的目标,咱们需求经过其结构函数来引用 T 类型。关于上述问题,在介绍详细的处理计划前,咱们先来介绍一下结构签名。

在 TypeScript 接口中,你能够运用 new 要害字来描述一个结构函r 7 v +数:

inb q n L 3 2 h bterface Point {
new[ p D V a n s R b (x: number, y: number): Point;
}

以上接口中的 new (x: number, y: number) 咱们称之为结构签名,其语法如下:

ConstructSignature:
newTypeParametersopt(ParameterListopt)TypeAnnotationopt

在上述的结构签名中,TypeParametersoptParameterListoptTypeAnnotationopt 分别表明:可选的类型参数、可选的参数列z b R u n i R 7 $表和可选的类型注解。与该语法相对应的几种常见的运用方法如下:

new C
new C ( ... )
new C < ... > ( ... )

介绍完结构签名,咱们再来介绍一个a j { ( f Y | 5与之相关的概念,即结构函数类型。

8.2 结构函数类型

在 TypeScript 语言标准中这样界说结构函数类型:

An object type containing one or mF h | o U f dore construct sigr u e ~ % L *natures is said to be a constructor type. ConstrV = puctor typeN [ l 7 t n ;s may be written using constructor type literals or by including construct sigz 5 q 0 f ( jnatures in object type literals.

经过标准中的描述信息,咱们能够得出以Q | S ` q u X Z下定论:

  • 包括一个或多个结构签名的目标类型被称为结构函数类型;
  • 结构函数类型w W M K J R G : c能够运用结构函数类型字面量或包括结构签名的目标类型字面量来编写。

那么什么是结构函数类型字面量呢?结构函数类型字面量是包括单个结构函数签名的目标类型的简写。详细来说,结构函数类型字面量的方法如下:

news v O O l [ < T1, T2, ... > ( p1, p2, ... ) => R

该方法与以下目标类型字面量是等价的:

{ new &s 2 E d hlt; T1, T2, ... > ( p1, p2, ... ) : R }

下面咱们来举个实践的示例:

// 结构函数类型字面量
new (x: number, y: number) =&s i 7 # i / / !gt; Poin^ d  + /t

等价于以下目标类型字面量:

{
new (x: number, y: number): Point;
}

8.3 结构函数类型的运用

在介绍结构函数类型的运用前,咱们先来看个比方:

interface Point {
new (x: number, y: number): Point;
x: number;
y: number;
}
cF D z _ 2 M +lass Point2D implements Point {
rh O E z U c teadonly 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 'Poij ^ Y 9 u  Z N rnt( A E 7 O g m Y w'.
Type 'Point2D' provides no match for the signature 'new (x: number, y: numberN C W ) L z #): Point'.

相信许N o ? 7 B多刚触摸 TypeS} F ; g = dcript 不久的小伙伴都会遇到上述的问题。要处理这个问题,咱们就需求把对前面界说的 Point 接口进行分离,即把接口的特点和结构函数类型进行分离:

interface Poil k A t s = Znt {
x: number;
y: number;
}
interface PointConstructor {
new (x: number, y: number): Point;
}

完成接口拆分之后,除了前面现已界说的 Point2D 类之外,咱们又界说了一个 newPoint 工厂函数,t Y ( a M W该函数用于依据传入的 PointConstructor 类型的结构函数,来创立对应的 P@ = [ / _ }oint 目标。

class Poi~ [ W G O v R W Bnt2D implements Point {
readonly x: number;
readog | Y 3 9 hnly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
function newPoI } 5int(
pointConstructor: PoB _ $ 0 m ointConstru; r 4 ] I j J z hctor,
x: number,
y: number
): Point {
return new poz ] = 8 y %intConstructor(x, y);
}
const p: . C 3 c koint: Point = newPoint(Point2D, 1, 2)5 p O;

8.4 运用泛型创立目标

了解完结构签名和结构函数类型之后,下面咱们来开始处理上面遇U ; k I K到的问题,首要咱们需求重构一下 create 办法,详细如下所示:

class GenericCreator<T> {
create<T>(c: { new (): T }): T {
return new c();
}
}

在以上k ^ # H 6 F . A代码中,咱们从头C 0 N v W R Z界说了 create 成员办法,依据该办法的签名,咱们能够知道该办法接纳一个参数,其类型是结构函数类型I ! T,且该结构函数不包括任何参数,调用该结构函数后,会回来类型 T 的实例。

假如结构函数含有参数的话,比方包括一个 number 类型的参数时,咱们能够这样界说 create 办法:

create<T&= Y z x $gt;(c: { new(a: number): T; }, num: number): T {
return new c(num);
}

更新完 GenericCrea/ l q Stor 泛型类,咱们就能够运用下面的办法来创立 FirstClassSecondClass 类的实例:

const creator1 = new GenericCreator<FirstClassK M J 7 W>();
const firstClass: FirstClass = crel ) x / U m , ~ jator1.create(FirstClass);
con  7 K _nst creator2 = new GenericCre! g Rator<SecondClass>();
const secondClass: SecondClass = crg x n 4 meator2.create(SecondClass);

. ~ n 4 0、参阅资源

  • typescript-gen9 I A . r $ R merc Y _ a p : 0 H gics
  • typescript-generics-explained
  • typescript-tip-of-the-week-generics

创立了一个 “重学TypeScri9 . } u k C s f }pt” 的微信群,想加群的小伙伴,加我微信 “se{ U 9 1 @ Nmlinker”,补白重学TS。阿里、京东、腾讯的m 9 .大佬都在群里等你哟。

semlinker/awesome-typescript 1.6K

一文读懂 TypeScript 泛型及应用( 7.8K字)