【翻译】编写易于编译的TypeScript代码

【翻译】编写易于编译的TypeScript代码

原文地址:Performance,本文只摘录《编写易于编译的代码》一部分。


有简单的方法可以配置 TypeScript,以确保更快的编译和编辑体验。越早采用这些实践,效果越好。除了最佳实践之外,还有一些常见的技巧用于调查缓慢的编译/编辑体验,一些常见的修复方法,以及一些常见的方法,帮助 TypeScript 团队作为最后手段调查问题。

编写易于编译的代码

请注意,以下并非一套完美的规则。根据您的代码库,每条规则都可能存在例外情况。

优先使用 interface 而非交叉类型

很多时候,一个简单的对象类型别名与 interface 表现得非常相似。

ts
interface Foo { prop: string }

type Bar = { prop: string }

然而,一旦您需要组合两个或多个类型,您可以选择通过 interface 扩展这些类型,或者在类型别名中使它们相交,这时差异就开始变得重要了。

interface 创建单一的扁平对象类型,用于检测属性冲突,而解决这些冲突通常至关重要!另一方面,交集只是递归地合并属性,在某些情况下甚至不会产生 never 结果。interface 的显示效果也始终更佳,而指向交集的类型别名无法在其他交集的部分内容中显示。此外,interface 之间的类型关系会被缓存,而交集类型本身则会被缓存。最后一个值得注意的区别是,在检查目标交集类型时,会先检查每个组成部分,然后再检查“有效”/“扁平化”类型。

因此,建议使用 interface s/ extends 扩展类型,而不是创建交叉类型。

ts
type Foo = Bar & Baz & { // [!code --]
    someProp: string // [!code --]
} // [!code --]
interface Foo extends Bar, Baz { // [!code ++]
    someProp: string // [!code ++]
} // [!code ++]

使用类型注解

添加类型注解,尤其是返回类型注解,可以为编译器节省大量工作。部分原因是命名类型通常比匿名类型(编译器可能会推断)更简洁,从而减少了读取和写入声明文件的时间(例如,在增量构建中)。类型推断非常方便,因此没有必要普遍启用类型注解——但是,如果您发现代码中存在性能瓶颈,尝试使用类型注解可能很有用。

ts
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` 需要引用来自另一个模块的类型。例如,以下文件的声明需要发出……

ts
// 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 文件:

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 类型。这涉及:

  1. 确定该类型是否可以通过本地名称访问。
  2. 查找类型 type 是否可通过 import(...) 访问。
  3. 计算导入该文件的最合理路径。
  4. 生成新节点以表示该类型引用。
  5. 打印这些类型引用节点。

对于一个非常大的项目,每个模块中可能会反复出现这种情况。

优先使用基本类型而非联合类型

联合类型非常棒——它们可以让你表达一个类型的可能值范围。

ts
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 传递参数时,都必须将其与联合体中的每个元素进行比较。对于包含两个元素的联合体来说,这很简单也很省事。但是,如果联合体包含十几个元素,就会严重影响编译速度。例如,要从联合体中消除冗余成员,必须对元素进行两两比较,这相当于二次方复杂度。当对大型联合体进行交集运算时,可能会出现这种检查,因为对每个联合体成员进行交集运算会导致生成非常庞大的类型,而这些类型随后需要进行缩减。避免这种情况的一种方法是使用子类型,而不是联合体。

ts
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 元素类型进行建模时。在这种情况下,最好创建一个包含公共成员的基类 HtmlElementDivElementImgElement 等元素都继承自该基类,而不是创建一个像 DivElement | /.../ | ImgElement | /.../

给复杂类型命名

复杂类型可以写在任何允许类型注解的地方。

ts
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 返回类型的结构。

如果将此示例中的返回类型提取到类型别名中,编译器可以缓存更多信息:

ts
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>;
}
新故事即将发生
或许你应该看看 shadcn-vue