原文地址:Performance,本文只摘录《编写易于编译的代码》一部分。
有简单的方法可以配置 TypeScript,以确保更快的编译和编辑体验。越早采用这些实践,效果越好。除了最佳实践之外,还有一些常见的技巧用于调查缓慢的编译/编辑体验,一些常见的修复方法,以及一些常见的方法,帮助 TypeScript 团队作为最后手段调查问题。
编写易于编译的代码
请注意,以下并非一套完美的规则。根据您的代码库,每条规则都可能存在例外情况。
优先使用 interface 而非交叉类型
很多时候,一个简单的对象类型别名与 interface 表现得非常相似。
interface Foo { prop: string }
type Bar = { prop: string }
然而,一旦您需要组合两个或多个类型,您可以选择通过 interface 扩展这些类型,或者在类型别名中使它们相交,这时差异就开始变得重要了。
interface 创建单一的扁平对象类型,用于检测属性冲突,而解决这些冲突通常至关重要!另一方面,交集只是递归地合并属性,在某些情况下甚至不会产生 never 结果。interface 的显示效果也始终更佳,而指向交集的类型别名无法在其他交集的部分内容中显示。此外,interface 之间的类型关系会被缓存,而交集类型本身则会被缓存。最后一个值得注意的区别是,在检查目标交集类型时,会先检查每个组成部分,然后再检查“有效”/“扁平化”类型。
因此,建议使用 interface s/ extends 扩展类型,而不是创建交叉类型。
type Foo = Bar & Baz & { // [!code --]
someProp: string // [!code --]
} // [!code --]
interface Foo extends Bar, Baz { // [!code ++]
someProp: string // [!code ++]
} // [!code ++]
使用类型注解
添加类型注解,尤其是返回类型注解,可以为编译器节省大量工作。部分原因是命名类型通常比匿名类型(编译器可能会推断)更简洁,从而减少了读取和写入声明文件的时间(例如,在增量构建中)。类型推断非常方便,因此没有必要普遍启用类型注解——但是,如果您发现代码中存在性能瓶颈,尝试使用类型注解可能很有用。
import { otherFunc } from "other"; // [!code --]
import { otherFunc, OtherType } from "other"; // [!code ++]
export function func() { // [!code --]
export function func(): OtherType { // [!code ++]
return otherFunc();
}
如果你的 --declaration 输出包含类似 import("./some/path").SomeType 的类型,或者包含源代码中未定义的超大型类型,则可能值得尝试一下。尝试显式地编写一些类型,必要时可以创建一个命名类型。
对于非常大的计算类型,打印/读取这种类型开销大的原因显而易见;但是,为什么 import() 代码生成开销也大呢?这为什么会是个问题?
在某些情况下, --declaration emit` 需要引用来自另一个模块的类型。例如,以下文件的声明需要发出……
// foo.ts
export interface Result {
headers: any;
body: string;
}
export async function makeRequest(): Promise<Result> {
throw new Error("unimplemented");
}
// bar.ts
import { makeRequest } from "./foo";
export function doStuff() {
return makeRequest();
}
将生成以下 .d.ts 文件:
// foo.d.ts
export interface Result {
headers: any;
body: string;
}
export declare function makeRequest(): Promise<Result>;
// bar.d.ts
export declare function doStuff(): Promise<import("./foo").Result>;
注意导入语句 import("./foo").Result 需要生成代码,以便在 bar.ts 的声明输出中引用 foo.ts 中名为 Result 类型。这涉及:
- 确定该类型是否可以通过本地名称访问。
- 查找类型 type 是否可通过
import(...)访问。 - 计算导入该文件的最合理路径。
- 生成新节点以表示该类型引用。
- 打印这些类型引用节点。
对于一个非常大的项目,每个模块中可能会反复出现这种情况。
优先使用基本类型而非联合类型
联合类型非常棒——它们可以让你表达一个类型的可能值范围。
interface WeekdaySchedule {
day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday";
wake: Time;
startWork: Time;
endWork: Time;
sleep: Time;
}
interface WeekendSchedule {
day: "Saturday" | "Sunday";
wake: Time;
familyMeal: Time;
sleep: Time;
}
declare function printSchedule(schedule: WeekdaySchedule | WeekendSchedule);
然而,它们也会带来一些代价。每次向 printSchedule 传递参数时,都必须将其与联合体中的每个元素进行比较。对于包含两个元素的联合体来说,这很简单也很省事。但是,如果联合体包含十几个元素,就会严重影响编译速度。例如,要从联合体中消除冗余成员,必须对元素进行两两比较,这相当于二次方复杂度。当对大型联合体进行交集运算时,可能会出现这种检查,因为对每个联合体成员进行交集运算会导致生成非常庞大的类型,而这些类型随后需要进行缩减。避免这种情况的一种方法是使用子类型,而不是联合体。
interface Schedule {
day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday";
wake: Time;
sleep: Time;
}
interface WeekdaySchedule extends Schedule {
day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday";
startWork: Time;
endWork: Time;
}
interface WeekendSchedule extends Schedule {
day: "Saturday" | "Sunday";
familyMeal: Time;
}
declare function printSchedule(schedule: Schedule);
更实际的例子可能出现在尝试对所有内置 DOM 元素类型进行建模时。在这种情况下,最好创建一个包含公共成员的基类 HtmlElement , DivElement 、 ImgElement 等元素都继承自该基类,而不是创建一个像 DivElement | /.../ | ImgElement | /.../
给复杂类型命名
复杂类型可以写在任何允许类型注解的地方。
interface SomeType<T> {
foo<U>(x: U):
U extends TypeA<T> ? ProcessTypeA<U, T> :
U extends TypeB<T> ? ProcessTypeB<U, T> :
U extends TypeC<T> ? ProcessTypeC<U, T> :
U;
}
这很方便,但如今,每次调用 foo 时,TypeScript 都必须重新运行条件类型判断。此外,关联任意两个 SomeType 实例都需要重新关联 foo 返回类型的结构。
如果将此示例中的返回类型提取到类型别名中,编译器可以缓存更多信息:
type FooResult<U, T> =
U extends TypeA<T> ? ProcessTypeA<U, T> :
U extends TypeB<T> ? ProcessTypeB<U, T> :
U extends TypeC<T> ? ProcessTypeC<U, T> :
U;
interface SomeType<T> {
foo<U>(x: U): FooResult<U, T>;
}
