变量
类型系统中声明?
一个全新的(类型)世界!
到目前为止,你学到的所有类型语法都是用于已知完整类型的情况下。然而,有时代码可能是需要根据调用方式与不同类型配合工作。
以这个 JavaScript 中的 identity
函数为例,它接收任何类型的输入,并将该输入作为输出返回。你会如何描述它的参数类型和返回类型?
function identity(input) {
return input;
}
identity("abc");
identity(123);
identity({ quote: "I think your self emerges more clearly over time." });
我们可以将 input
声明为 any
,但函数的返回类型也将为 any
:
function identity(input: any) {
return input;
}
let value = identity(42); // 值得类型:any
考虑到 input
参数可以是任何类型,我们需要一种方法来说明 input
类型和函数返回的类型之间存在关系。TypeScript 使用泛型(generics)来捕获类型之间的关系。
在 TypeScript 中,诸如函数之类的构造可以声明任意数量的泛型类型形参(type parameters):这些类型参数根据泛型构造的每个使用而确定的类型。这些类型参数在构造中被用作类型,表示在构造的每个实例中可以是不同的类型。针对构造的每个实例,可以为类型参数提供不同的类型,称为类型形参(type arguments),但在该实例内部保持一致。
类型参数通常具有单字母名称(如 T
和 U
)或 Pascal 式名称(如 Key
和 Value
)。在本章介绍的所有构造中,泛型可以使用 <
和 >
括号进行声明,如 someFunction<T>
或 SomeInterface<T>
。
泛型函数
通过在参数括号之前使用尖括号封装类型参数的别名,可以使函数成为泛型。该类型参数将可用于函数体内的参数类型注解、返回类型注解和类型注解。
以下版本的 identity
为其 input
参数声明了一个名为 T
的类型参数,这使得 TypeScript 能够推断函数的返回类型为 T
。然后,TypeScript 可以在每次调用 identity
时为 T
推断出不同的类型:
function identity<T>(input: T) {
return input;
}
const numeric = identity("me"); // 类型:"me"
const stringy = identity(123); // 类型:123
箭头函数也可以是泛型的。它们的泛型声明也是也放在参数列表的 (
前面的尖括号内。
下面的头函数与之前的声明在功能上相同:
const identity = <T>(input: T) => input;
identity(123); // 类型:123
警告:泛型箭头函数的语法在 .tsx 文件中有一些限制,因为它与 JSX 语法冲突。请参阅第 13 章 “配置选项”,了解解决方法以及配置 JSX 和 React 支持。
以这种方式向函数添加类型参数允许它们与不同的输入重用,同时仍保持类型安全性并避免使用 any
类型。
显式泛型调用类型
大多数情况下,在调用泛型函数时,TypeScript 将能够根据函数的调用方式推断类型参数。例如,在前面的示例中的 identity
函数,TypeScript 的类型检查器使用提供给 identity
的参数来推断相应函数参数的类型参数。
不幸的是,与类成员和变量类型一样,有时函数的调用不足以告诉 TypeScript 它的类型参数应该解析为什么。如果泛型构造提供了另一个类型参数未知的泛型构造,这种情况通常会发生。
TypeScript 对于无法推断类型参数的任何类型参数,它将默认为 unknown
类型。
例如,以下 logWrapper
函数接受一个回调,其参数类型设置为 logWrapper
的类型参数 Input
。如果 logWrapper
用一个显式声明其参数类型的回调来调用,TypeScript 就可以推断出类型参数。然而,如果参数类型是隐式的,则 TypeScript 无法知道 Input
应该是什么:
function logWrapper<Input>(callback: (input: Input) => void) {
return (input: Input) => {
console.log("Input:", input);
callback(input);
};
}
// 类型:(input: string) => void
logWrapper((input: string) => {
console.log(input.length);
});
// 类型:(input: unknown) => void
logWrapper((input) => {
console.log(input.length);
// ~~~~~~
// 错误:“input”的类型为“未知”。
});
为了避免默认为 unknown
,函数可以使用显式泛型类型参数调用,该泛型类型参数明确地告诉 TypeScript 该类型参数应该是什么。TypeScript 将对泛型调用执行类型检查,以确保所请求的参数与作为类型参数提供的参数匹配。
在这里,前面看到的 logWrapper
为其 Input
泛型提供了显式的 string
。然后,然后 TypeScript 可以推断泛型 input
回调函数的 input
参数的类型为 string
:
// 类型:(input: string) => void
logWrapper<string>((input) => {
console.log(input.length);
});
logWrapper<string>((input: boolean) => {
// ~~~~~~~~~~~~~~~~~~~~~~~
// 类型“(input: boolean) => void”的参数不能赋给类型“(input: string) => void”的参数。
// 参数“input”和“input” 的类型不兼容。
// 不能将类型“string”分配给类型“boolean”。
});
与变量上的显式类型注解非常相似,显式类型参数可能始终在泛型函数上指定,但通常不是必需的。许多 TypeScript 开发操作通常只在需要时指定它们。
以下 logWrapper
用法显式指定 string
作为类型参数和函数参数类型。可以删除任一选项:
// 类型:(input: string) => void
logWrapper<string>((input: string) => { /* ... */ });
用于指定类型参数的 Name<Type>
语法对于本章中的其他泛型构造是相同的。
多个函数类型参数
函数可以定义任意数量的类型参数,用逗号分隔。泛型函数的每次调用都可以为每个类型参数解析其自己的一组值。
在这个例子中,makeTuple
声明了两个类型参数,并返回一个类型为只读元组的值,其中一个 boolean
,另一个 string
:
function makeTuple<First, Second>(first: First, second: Second) {
return [first, second] as const;
}
let tuple = makeTuple(true, "abc"); // Type of value: readonly [boolean, string]
请注意,如果函数声明多个类型参数,则对该函数的调用必须显式声明任何泛型类型或所有泛型类型。TypeScript 尚不支持仅推断泛型调用的某些类型。
在这里,makePair
还包含两个类型参数,因此必须显式指定它们中的任何一个或两个:
function makePair<Key, Value>(key: Key, value: Value) {
return { key, value };
}
// Ok: 所有类型参数都不提供
makePair("abc", 123); // 类型:{ key: string; value: number }
// Ok: 提供所有类型参数
makePair<string, number>("abc", 123); // 类型:{ key: string; value: number }
makePair<"abc", 123>("abc", 123); // 类型:{ key: "abc"; value: 123 }
makePair<string>("abc", 123);
// ~~~~~~
// 错误:有 2 个类型参数,但获得 1 个。
尽量不要在任何泛型构造中使用一个或两个以上的类型参数。与运行时函数参数一样,使用得越多,阅读和理解代码就越困难。
泛型接口
接口也可以声明为泛型接口。它们遵循与函数类似的泛型规则:它们可以在其名称后的 <
和 >
之间声明任意数量的类型参数。该泛型类型稍后可以在其声明中的其他位置使用,例如在成员类型上。
以下 Box
声明具有属性的 T
类型参数。使用类型参数创建声明为 Box
的对象将强制 inside: T
属性与该类型参数匹配:
interface Box<T> {
inside: T;
}
let stringyBox: Box<string> = {
inside: "abc",
};
let numberBox: Box<number> = {
inside: 123,
}
let incorrectBox: Box<number> = {
inside: false,
// 错误:不能将类型“boolean”分配给类型“number”。
}
有趣的事实:内置的Array
方法在 TypeScript 中定义为泛型接口。Array
使用类型参数 T
来表示存储在数组中的数据类型。它的 pop
和 push
方法大致如下所示:
interface Array<T> {
// ...
/**
* 从数组中删除最后一个元素并返回它。
* 如果数组为空,则返回 undefined,数组不会被修改。
*/
pop(): T | undefined;
/**
* 将新元素附加到数组的末尾,
* 并返回数组的新长度。
* @param items 添加到数组中的新元素。
*/
push(...items: T[]): number;
// ...
}
推断的泛型接口类型
与泛型函数一样,泛型接口类型参数可以从用法中推断出来。TypeScript 将尽最大努力从接受泛型类型声明位置的值类型推断出类型参数。
这个 getLast
函数声明一个类型参数 Value
,然后用于其 node
参数。TypeScript 可以根据传递给该函数的参数值的类型来推断出 Value
的值。它甚至可以在推断的类型参数与值的类型不匹配时报告类型错误。提供一个不包含 next
的对象或者推断出的 Value
类型参数是与对象相同类型都是允许的。但是,如果提供的对象的 value
和 next.value
不匹配,则会抛出类型错误:
interface LinkedNode<Value> {
next?: LinkedNode<Value>;
value: Value;
}
function getLast<Value>(node: LinkedNode<Value>): Value {
return node.next ? getLast(node.next) : node.value;
}
// 推断 Value 类型参数: Date
let lastDate = getLast({
value: new Date("09-13-1993"),
});
// 推断 Value 类型参数: string
let lastFruit = getLast({
next: {
value: "banana",
},
value: "apple",
});
// 推断 Value 类型参数: number
let lastMismatch = getLast({
next: {
value: 123
},
value: false,
// ~~~~~
// 错误:不能将类型“boolean”分配给类型“number”。
});
请注意,如果接口声明类型参数,则引用该接口的任何类型注解都必须提供相应的类型参数。在这里,CrateLike
的使用不正确,因为不包含类型参数:
interface CrateLike<T> {
contents: T;
}
let missingGeneric: CrateLike = {
// ~~~~~
// 错误:泛型类型“CrateLike<T>”需要 1 个类型参数。
inside: "??"
};
在本章的后面部分,我将展示如何为类型参数提供默认值以满足此要求。
泛型类
类和接口一样,也可以声明任意数量的类型参数,以便稍后在成员上使用。类的每个实例可能具有不同的类型参数集合。
此 Secret
类声明 Key
和 Value
类型参数,然后将它们用于成员属性、构造函数参数类型以及方法的参数和返回类型:
class Secret<Key, Value> {
key: Key;
value: Value;
constructor(key: Key, value: Value) {
this.key = key;
this.value = value;
}
getValue(key: Key): Value | undefined {
return this.key === key
? this.value
: undefined;
}
}
const storage = new Secret(12345, "luggage"); // 类型:Secret<number, string>
storage.getValue(1987); // 类型:string | undefined
与泛型接口一样,使用类的类型注解必须向 TypeScript 表明该类上的任何泛型类型是什么。在本章的后面,我将展示如何为类型参数提供默认值,以满足类的这一要求。
显式泛型类类型
实例化泛型类遵循与调用泛型函数相同的类型参数推断规则。如果类构造函数的类型参数可以从参数类型中推断出来,比如前面的 new Secret(12345, "luggage")
,则 TypeScript 将使用推断的类型。否则,如果类型参数无法从传递给其构造函数的参数中推断出来,则类型参数将默认为 unknown
。
这个 CurriedCallback
类声明一个接受泛型函数的构造函数。如果泛型函数具有已知类型——例如来自显式类型参数类型注解——则类实例的 Input
类型参数可以由它推断出来。否则,类实例的 Input
类型参数将默认为 unknown
:
class CurriedCallback<Input> {
#callback: (input: Input) => void;
constructor(callback: (input: Input) => void) {
this.#callback = (input: Input) => {
console.log("Input:", input); callback(input);
};
}
call(input: Input) {
this.#callback(input);
}
}
// 类型:CurriedCallback<string>
new CurriedCallback((input: string) => {
console.log(input.length);
});
// 类型:CurriedCallback<unknown>
new CurriedCallback((input) => {
console.log(input.length);
// ~~~~~~
// 错误:“input”的类型为“未知”。
});
类实例也可以通过像其他泛型函数调用一样提供显式类型参数来避免默认为 unknown
。
这里,之前的 CurriedCallback
现在提供了一个显式的 string
作为它的 Input
类型参数,因此 TypeScript 可以推断出回调的 Input
类型参数解析为 string
:
// 类型:CurriedCallback<string>
new CurriedCallback<string>((input) => {
console.log(input.length);
});
new CurriedCallback<string>((input: boolean) => {
// ~~~~~~~~~~~~~~~~~~~~~~
// 类型“(input: boolean) => void”的参数不能赋给类型“(input: string) => void”的参数。
// 参数“input”和“input” 的类型不兼容。
// 不能将类型“string”分配给类型“boolean”。
});
扩展泛型类
泛型类可以使用 extends
关键字作为基类使用。TypeScript 不会尝试从使用情况推断基类的类型参数。任何没有默认值的类型参数都需要使用显式类型注解进行指定。
以下 SpokenQuote
类为其基类 Quote<T>
提供 string
作为 T
类型参数::
class Quote<T> {
lines: T;
constructor(lines: T) {
this.lines = lines;
}
}
class SpokenQuote extends Quote<string[]> {
speak() {
console.log(this.lines.join("\n"));
}
}
new Quote("The only real failure is the failure to try.").lines; // 类型:string
new Quote([4, 8, 15, 16, 23, 42]).lines; // 类型:number[]
new SpokenQuote([
"Greed is so destructive.", "It destroys everything",
]).lines; // 类型:string[]
new SpokenQuote([4, 8, 15, 16, 23, 42]);
// ~~~~~~~~~~~~~~~~~~~~~~
// 错误:不能将类型“number”分配给类型“string”。
泛型派生类可以将其类型参数依次传递给其基类。类型名称不必匹配;仅仅为了好玩,这个 AttributedQuote
将一个不同名称的 Value
类型参数传递给基类 Quote<T>
:
class AttributedQuote<Value> extends Quote<Value> {
speaker: string
constructor(value: Value, speaker: string) {
super(value); this.speaker = speaker;
}
}
// 类型:AttributedQuote<string>
// (扩展自 Quote<string>)
new AttributedQuote(
"The road to success is always under construction.", "Lily Tomlin",
);
实现泛型接口
泛型类也可以通过提供必要的类型参数来实现泛型接口。这类似于扩展一个泛型基类:任何在基础接口中的类型参数都必须由类声明。
在这个例子中,MoviePart
类将 ActingCredit
接口的 Role
类型参数指定为string
。IncorrectExtension
类会引发类型错误,因为它的 role
属性的类型是 boolean
,尽管它将 string
(译者注:原文是 string[]
) 作为 ActingCredit
的类型参数提供:
interface ActingCredit<Role> {
role: Role;
}
class MoviePart implements ActingCredit<string> {
role: string;
speaking: boolean;
constructor(role: string, speaking: boolean) {
this.role = role;
this.speaking = speaking;
}
}
const part = new MoviePart("Miranda Priestly", true);
part.role; // 类型:string
class IncorrectExtension implements ActingCredit<string> {
role: boolean;
// ~~~~~~~
// 错误:类型“IncorrectExtension”中的属性“role”
// 不可分配给基类型“ActingCredit<string>”中的同一属性。
// 不能将类型“boolean”分配给类型“string”。
}
方法泛型
类方法可以声明自己独立于类实例的泛型类型。每次调用泛型类方法时,每个类型参数可能有不同的类型。
这个泛型 CreatePairFactory
类声明 Key
类型,并包含一个 createPair
方法,该方法还声明单独的 Value
泛型类型。然后推断 create Pair
的返回类型为 { key: Key, value: Value }
:
class CreatePairFactory<Key> {
key: Key;
constructor(key: Key) {
this.key = key;
}
createPair<Value>(value: Value) {
return { key: this.key, value };
}
}
// 类型:CreatePairFactory<string>
const factory = new CreatePairFactory("role");
// 类型:{ key: string, value: number }
const numberPair = factory.createPair(10);
// 类型:{ key: string, value: string }
const stringPair = factory.createPair("Sophie");
静态类泛型
类的静态成员与实例成员是独立的,与类的任何特定实例都无关。他们无权访问任何类实例或特定于任何类实例的类型信息。因此,虽然静态类方法可以声明自己的类型参数,但它们无法访问类声明的任何类型参数。
这里有一个 BothLogger
类,它为其 instanceLog
方法声明一个 OnInstance
类型参数,为其静态 staticLog
方法声明一个单独的 OnStatic
类型参数。由于OnInstance
是为类实例声明的,因此静态方法无法访问实例 OnInstance
:
class BothLogger<OnInstance> {
instanceLog(value: OnInstance) {
console.log(value);
return value;
}
static staticLog<OnStatic>(value: OnStatic) {
let fromInstance: OnInstance;
// ~~~~~~~~~~
// 错误:静态成员不能引用类类型参数。
console.log(value);
return value;
}
}
const logger = new BothLogger<number[]>;
logger.instanceLog([1, 2, 3]); // 类型:number[]
// 推断 OnStatic 类型参数:boolean[]
BothLogger.staticLog([false, true]);
// 显式 OnStatic 类型参数:string
BothLogger.staticLog<string>("You can't change the music of your soul.");
泛型类型别名
TypeScript 中最后一个可以使用类型参数创建泛型的结构是类型别名。每个类型别名可以接收任意数量的类型参数,例如接收T
的Nullish
类型:
type Nullish<T> = T | null | undefined;
泛型类型别名通常与函数一起使用,以描述泛型函数的类型:
type CreatesValue<Input, Output> = (input: Input) => Output;
// 类型:(input: string) => number
let creator: CreatesValue<string, number>;
creator = text => text.length; // Ok
creator = text => text.toUpperCase();
// ~~~~~~~~~~~~~~~~~~
// 错误:不能将类型“string”分配给类型“number”。
泛型区分联合
我在第 4 章“对象”中提到,区分联合是我在所有 TypeScript 中最喜欢的功能,因为它将常见的优雅 JavaScript 模式与TypeScript 的类型缩小完美结合在一起。。我最喜欢的区分联合用途是添加一个类型参数来创建一个通用的“结果”类型,该类型表示具有数据的成功结果或带有错误的失败结果。
这个 Result
泛型类型具有 succeeded
判别因素,必须使用它来缩小结果是是成功还是失败。这意味着任何返回 Result
的操作都可以表示错误或数据结果,并确保使用者需要检查结果是否成功:
type Result<Data> = FailureResult | SuccessfulResult<Data>;
interface FailureResult {
error: Error;
succeeded: false;
}
interface SuccessfulResult<Data> {
data: Data;
succeeded: true;
}
function handleResult(result: Result<string>) {
if (result.succeeded) {
// Type of result: SuccessfulResult<string>
console.log(`We did it! ${result.data}`);
} else {
// Type of result: FailureResult
console.error(`Awww... ${result.error}`);
}
result.data;
// ~~~~
// 错误:类型“Result<string>”上不存在属性“data”。
// 类型“FailureResult”上不存在属性“data”。
}
总之,泛型类型和区分类型提供了一种对可重用类型(如 Result
)进行建模的绝佳方法。
泛型修饰符
TypeScript 包含允许你修改泛型类型参数行为的语法。
泛型默认值
到目前为止,我已经说过,如果在类型注解中使用泛型类型或作为类 extends
或 implements
的基础,它必须为每个类型参数提供一个类型参数。可以通过在类型参数的声明后放置一个 =
符号后跟默认类型来显式提供类型参数。默认值将用于未显式声明类型参数且无法推断的任何后续类型。
在这里,Quote
接口接受 T
类型参数,如果未提供,则默认为 string
。explicit
变量显式将 T
设置为 number
,而 implicit
和 mismatch
都解析为 string
:
interface Quote<T = string> {
value: T;
}
let explicit: Quote<number> = { value: 123 };
let implicit: Quote = { value: "Be yourself. The world worships the original." };
let mismatch: Quote = { value: 123 };
// ~~~
// 错误:不能将类型“number”分配给类型“string”。
类型参数也可以在同一声明中默认为先前的类型参数。由于每个类型参数都为声明引入了一个新类型,因此它们可用作该声明中后续类型参数的默认值。
这个 KeyValuePair
类型为其 Key
和 Value
泛型指定不同的类型,但默认保持它们相同——尽管由于 Key
没有默认值,它仍然需要推断或提供:
interface KeyValuePair<Key, Value = Key> {
key: Key;
value: Value;
}
// 类型:KeyValuePair<string, number>
let allExplicit: KeyValuePair<string, number> = {
key: "rating",
value: 10,
};
// 类型:KeyValuePair<string>
let oneDefaulting: KeyValuePair<string> = {
key: "rating",
value: "ten",
};
let firstMissing: KeyValuePair = {
// ~~~~~~~~~~~~
// 错误:泛型类型“KeyValuePair<Key, Value>”需要介于 1 和 2 类型参数之间。
key: "rating", value: 10,
};
记住,所有默认类型参数必须放在声明列表的最后,类似于默认函数参数。没有默认值的泛型类型不能跟在有默认值的泛型类型后面。
在这里,inTheEnd
是允许的,因为所有没有默认值的泛型类型都排在具有默认值的泛型类型之前。inTheMiddle
有问题,因为没有默认值的泛型类型跟在具有默认值的类型后面:
function inTheEnd<First, Second, Third = number, Fourth = string>() // Ok
function inTheMiddle<First, Second = boolean, Third = number, Fourth>()
// ~~~~~~
// 错误:所需的类型参数可能不遵循可选类型参数。
约束泛型类型
默认情况下,泛型类型可以分配世界上的任何类型:类、接口、基本类型、联合类型,你可以随意选择。但是,有些函数只能处理有限的类型集合。
TypeScript 允许类型参数声明自己需要扩展一个类型:这意味着它只允许别名可以分配给该类型的类型。约束类型参数的语法是在类型参数的名称后面放置 extends
关键字,后面跟一个类型来约束它。
例如,通过创建一个 WithLength
接口来描述任何具有 length: number
的东西,然后我们可以允许我们的泛型函数接受任何具有 length
作为其 T
泛型的类型。允许使用字符串、数组,甚至现在碰巧具有 length: number
的对象,而缺少数字 length
的类型形状(如 Date
)会导致类型错误:
interface WithLength {
length: number;
}
function logWithLength<T extends WithLength>(input: T) {
console.log(`Length: ${input.length}`);
return input;
}
logWithLength("No one can figure out your worth but you."); // 类型:string
logWithLength([false, true]); // 类型:boolean[]
logWithLength({ length: 123 }); // 类型:{ length: number }
logWithLength(new Date());
// ~~~~~~~~~~
// 错误:类型“Date”的参数不能赋给类型“WithLength”的参数。
// 类型 "Date" 中缺少属性 "length",但类型 "WithLength" 中需要该属性。
我将在第 15 章 “类型操作”中介绍可以使用泛型执行的更多类型操作。
keyof 和约束类型参数
第 9 章 “类型修饰符”中介绍的 keyof
运算符也可以很好地与约束类型参数一起使用。同时使用 extends
和 keyof
允许将类型参数约束为前一个类型参数的键。这也是指定泛型类型的键的唯一方法。
从流行库Lodash中拿出一个简化版本的 get
方法。它接受一个容器值,类型为 T
,和一个键名称,该名称是 T
的键之一,用于从 container
中检索。因为 Key
类型参数受限于 keyof T
,TypeScript知道该函数允许返回 T[Key]
:
function get<T, Key extends keyof T>(container: T, key: Key) {
return container[key];
}
const roles = {
favorite: "Fargo",
others: ["Almost Famous", "Burn After Reading", "Nomadland"],
};
const favorite = get(roles, "favorite"); // 类型:string
const others = get(roles, "others"); // 类型:string[]
const missing = get(roles, "extras");
// ~~~~~~~~
// 错误:类型“"extras"”的参数不能赋给类型“"favorite" | "others"”的参数。
如果没有keyof
,就没有正确类型化通用key
参数的方法。
请注意上一个示例中 Key
类型参数的重要性。如果仅提供 T
作为类型参数,并且允许 key
参数为任意 keyof T
,则返回类型将是 Container
中所有属性值的联合类型。这种不太具体的函数声明不会向 TypeScript 表明,每次调用都可以通过类型参数拥有特定的key
:
function get<T>(container: T, key: keyof T) {
return container[key];
}
const roles = {
favorite: "Fargo",
others: ["Almost Famous", "Burn After Reading", "Nomadland"],
};
const found = get(roles, "favorite"); // 类型:string | string[]
在编写泛型函数时,请确保知道参数的类型何时取决于前一个参数的类型。在这些情况下,通常需要对正确的参数类型使用约束类型参数。
Promise
现在你已经了解了泛型是如何工作的,终于到了谈论现代 JavaScript 中依赖它们概念的核心特性:Promise 的时候了!简要回顾一下,JavaScript 中的 Promise 代表可能仍处于挂起状态的内容,例如网络请求。每个 Promise 提供了注册回调的方法,以防待处理的动作“解决”(成功完成)或“拒绝”(抛出错误)。
Promise 能够代表任意值类型的类似操作,这很自然地适合 TypeScript 的泛型。Promise 在 TypeScript 类型系统中被表示为一个带有单个类型参数的 Promise
类,用于表示最终的解析值。
创建 Promise
Promise
构造函数在 TypeScript 中输入时接受一个参数。该参数的类型依赖于在泛型 Promise
类上声明的类型参数。简化形式大致如下:
class PromiseLike<Value> {
constructor(
executor: (
resolve: (value: Value) => void,
reject: (reason: unknown) => void,
) => void,
) { /* ... */ }
}
创建一个最终使用值解析的 Promise,通常需要显式声明 Promise 的类型参数。如果没有明确的泛型类型参数,TypeScript 将默认假定参数类型为 unknown
。
明确提供 Promise
构造函数的类型参数将允许 TypeScript 理解所得到的 Promise 实例的解析类型:
// 类型:Promise<unknown>
const resolvesUnknown = new Promise((resolve) => {
setTimeout(() => resolve("Done!"), 1000);
});
// 类型:Promise<string>
const resolvesString = new Promise<string>((resolve) => {
setTimeout(() => resolve("Done!"), 1000);
});
Promise 的通用 .then
方法引入了一个新的类型参数,表示它返回的 Promise 的解析值。
例如,以下代码创建一个 textEventually
Promise 对象,它在一秒后以 string
值解析,以及一个 lengthEventually
对象,它等待另外一秒来解析一个 number
:
// 类型:Promise<string>
const textEventually = new Promise<string>((resolve) => {
setTimeout(() => resolve("Done!"), 1000);
});
// 类型:Promise<number>
const lengthEventually = textEventually.then((text) => text.length)
异步函数
在 JavaScript 中,使用 async
关键字声明的任何函数都会返回一个 Promise
。如果 async
函数返回的值不是 Thenable(一个具有 .then()
方法的对象;实际上几乎总是一个 Promise),它将被包装在一个 Promise
中,就好像调用了 Promise.resolve
一样。TypeScript 识别到这一点,并且会推断 async
函数的返回类型始终为返回值的 Promise
。
在这里,lengthAfterSecond
直接返回一个 Promise<number>
,而 lengthImmediately
被推断为返回一个 Promise<number>
,因为它是 async
的,并且直接返回一个 number
:
// 类型:(text: string) => Promise<number>
async function lengthAfterSecond(text: string) {
await new Promise((resolve) => setTimeout(resolve, 1000))
return text.length;
}
// 类型:(text: string) => Promise<number>
async function lengthImmediately(text: string) {
return text.length;
}
因此,在 async
函数上手动声明的任何返回类型都必须始终是 Promise
类型,即使函数在其实现中没有明确提及 Promise。
// Ok
async function givesPromiseForString(): Promise<string> {
return "Done!";
}
async function givesString(): string {
// ~~~~~~
// 错误:异步函数或方法的返回类型必须为全局 Promise<T> 类型。
return "Done!";
}
正确使用泛型
正如本章前面介绍的 Promise<Value>
实现一样,虽然泛型可以让我们在代码中描述类型的灵活性,但它们很快就会变得相当复杂。 TypeScript 的新手程序员通常会经历一个过度使用泛型的阶段,导致代码难以阅读和过于复杂。 TypeScript 最佳实践通常是仅在必要时使用泛型,并在使用时清楚地说明其用途。
TypeScript 中编写的大多数代码都不应过度使用泛型,以至于令人困惑。然而,对于实用程序库(尤其是通用模块)的类型,有时可能需要大量使用它们。了解泛型特别有助于有效地处理这些实用程序类型。
泛型的黄金法则
一个可以帮助显示类型参数对于函数是否必需的快速测试是,它应该至少使用两次。泛型描述类型之间的关系,因此,如果泛型类型参数只出现在一个位置,则它不可能定义多个类型之间的关系。
每个函数类型参数应用于一个参数,然后也应用于至少一个其他参数和/或函数的返回类型。
例如,此 logInput
函数只使用一次它的 Input
类型参数,来声明它的 Input
参数:
function logInput<Input extends string>(input: Input) {
console.log("Hi!", input);
}
与本章前面的 identify
函数不同,logInput
不对其类型参数执行任何操作,例如返回或声明更多参数。因此,声明 Input
类型参数没有多大用处。我们可以在没有它的情况下重写 logInput
:
function logInput(input: string) {
console.log("Hi!", input);
}
Dan Vanderkam(O’Reilly,2019)的 Effective TypeScript 包含有关如何使用泛型的几个很好的技巧,其中有一节名为“泛型的黄金法则”。如果你发现自己在代码中花费大量时间来处理泛型,我强烈推荐阅读《Effective TypeScript》,尤其是那一节。
通用命名约定
许多语言(包括 TypeScript)中类型参数的标准命名约定是默认调用第一个类型参数“T”(表示“类型”或“模板”),如果存在后续类型参数,则调用它们“U”、“V”等。
如果已知有关如何使用类型参数的一些上下文信息,则约定有时会扩展到使用该用法的术语的第一个字母:例如,状态管理库可能将泛型状态称为“S”。“K”和“V”通常指数据结构中的键和值。
不幸的是,用一个字母命名类型参数可能与只用一个字符命名函数或变量一样令人困惑:
// L and V 到底是什么?!
function labelBox<L, V>(l: L, v: V) { /* ... / }
当泛型的意图不能从单个字母 T
中清晰地表达出来时,最好使用描述性的泛型类型名称来指明该类型的用途:
// 清楚多了。
function labelBox<Label, Value>(label: Label, value: Value) { / ... */ }
每当构造具有多个类型参数,或者单个类型参数的用途不是很清楚时,请考虑使用完整的名称来以提高可读性,而不是使用单字母缩写。
总结
在本章中,你通过允许类、函数、接口和类型别名使用类型参数,使它们成为“泛型”:
- 使用类型参数来表示构造函数不同使用之间的类型
- 调用泛型函数时提供显式或隐式类型参数
- 使用泛型接口表示泛型对象类型
- 向类添加类型参数以及它们如何影响类型
- 向类型别名添加类型参数,特别是有区分的类型联合
- 使用默认值(
=
)和约束(extends
)修改泛型类型参数 - Promise 和
async
函数如何使用泛型来表示异步数据流 - 泛型的最佳实践,包括其黄金法则和命名约定
本书的“功能”部分到此结束。恭喜:你现在了解了大多数项目的 TypeScript 类型系统中所有最重要的语法和类型检查功能!
下一部分用法将介绍如何配置 TypeScript 以在项目上运行、与外部依赖项交互以及调整其类型检查和生成的 JavaScript。这些都是在你自己的项目中使用 TypeScript 的重要功能。
TypeScript 语法中还有其他一些杂项类型操作可用。你不需要完全理解它们即可在大多数 TypeScript 项目中工作——但它们很有趣且很有用。我把它们放在了第四部分,算是第三部分“用法”之后的“额外学分”。如果你有时间就去领取这个有趣的小礼物。
现在你已经读完了这一章,你最好练习一下学到的东西 https://learningtypescript.com/generics。
为什么泛型会激怒开发人员?
他们总是在输入参数。
评论 (0)