Voocii博客
首页博客AI 热榜作品集读书友链工具关于

© 2026 Voocii. Built with Next.js & tRPC.

GitHubXEmailRSS


TypeScript 高级语言技巧汇总

前端rick-hayekrick-hayek2025年7月22日

TypeScript 的类型系统已经强大到可以在编译期做很多"计算"。这篇文章汇总一些工程中常用、好用的高级写法,附带示例代码,并且逐一讨论它们在运行时和编译期是否有性能损失——这是很多文章不会讲清楚的地方。


一、类型层面的技巧(编译期,零运行时开销)

TypeScript 的类型系统在编译后会被完全擦除,所以本节所有技巧运行时开销为 0,代价只体现在编译速度上。

1.1 条件类型(Conditional Types)

type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<123>;     // false

条件类型是类型体操的基础,可以配合 infer 提取类型信息:

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type Result = UnwrapPromise<Promise<number>>; // number

1.2 分布式条件类型(Distributive Conditional Types)

当条件类型作用于联合类型上时,会自动"分发"到每个成员:

type ToArray<T> = T extends unknown ? T[] : never;

type Result = ToArray<string | number>; 
// 等价于 string[] | number[],而不是 (string | number)[]

如果不想要这种分发行为,可以用 [T] 包裹阻止分布:

type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;

type Result2 = ToArrayNonDist<string | number>; 
// (string | number)[]

1.3 映射类型 + 键重映射(Key Remapping)

TS 4.1+ 支持在映射类型里用 as 子句重命名键,常用来做 getter/setter 生成:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

配合 -? -readonly 可以移除可选/只读修饰符:

type Required<T> = { [K in keyof T]-?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };

1.4 模板字面量类型(Template Literal Types)

可以在类型层面做字符串拼接、解析:

type EventName<T extends string> = `on${Capitalize<T>}`;

type ClickEvent = EventName<"click">; // "onClick"

// 反向解析路由参数
type ExtractParams<T extends string> = 
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;

type Params = ExtractParams<"/user/:id/post/:postId">; 
// "id" | "postId"

1.5 递归类型(Recursive Types)

TS 支持有限深度的递归类型,常用于深度 Partial、Readonly:

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

type DeepReadonly<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

⚠️ 注意:递归深度过深(一般 >50 层)会触发 Type instantiation is excessively deep 错误,这是一个真实的编译期限制,不是性能损失但会导致编译失败。

1.6 类型守卫与类型谓词(Type Predicates)

function isString(val: unknown): val is string {
  return typeof val === "string";
}

// 结合数组 filter,TS 能正确收窄类型
const mixed: (string | number)[] = ["a", 1, "b", 2];
const strings = mixed.filter(isString); // string[]

TS 5.5+ 还支持自动推断的类型谓词(无需手写 is),编译器会在满足条件时自动识别。

1.7 satisfies 操作符(TS 4.9+)

satisfies 既能做类型检查,又不会像类型标注那样"扩宽"字面量类型:

type Config = {
  mode: "dark" | "light";
  retries: number;
};

// 用 : Config 标注会丢失字面量精度
const config1: Config = { mode: "dark", retries: 3 };
config1.mode; // 类型是 "dark" | "light"

// 用 satisfies 保留字面量精度,同时做类型检查
const config2 = { mode: "dark", retries: 3 } satisfies Config;
config2.mode; // 类型是 "dark"(精确字面量)

这在需要精确类型推导(比如给 Redux action 或路由表做类型约束)时非常实用。

1.8 const 断言(as const)

const routes = ["/home", "/about", "/contact"] as const;
// 类型是 readonly ["/home", "/about", "/contact"]
// 而不是 string[]

type Route = typeof routes[number];
// "/home" | "/about" | "/contact"

as const 常和模板字面量类型配合,把运行时的常量数组转换成编译期的联合类型,做路由、权限枚举校验非常好用。


二、语法糖(编译期转译,可能有极小运行时差异)

2.1 可选链与空值合并

const city = user?.address?.city ?? "未知城市";

编译到 ES2020 及以上目标时,?. 和 ?? 会被保留为原生语法,没有额外开销;编译到更老的目标(如 ES5)会被转译成多次 null/undefined 判断,有极轻微的运行时开销(多几次条件判断),但可以忽略不计。

2.2 装饰器(Decorators)

function logMethod(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`调用 ${key}`, args);
    return original.apply(this, args);
  };
}

class Service {
  @logMethod
  fetchData(id: number) {
    return id;
  }
}

⚠️ 装饰器有真实的运行时开销:它们在类定义时执行一次(不是每次调用),但如果装饰器内部包裹了方法(如上面的日志装饰器),那么每次方法调用都会多一层函数调用和闭包访问,开销通常是微秒级,高频调用路径(如渲染循环、热路径计算)需要谨慎使用。

2.3 枚举(enum)vs 联合类型字面量

// 数字枚举:编译后生成双向映射对象,有运行时体积和查表开销
enum Color { Red, Green, Blue }

// const enum:编译期内联,无运行时产物(但项目引用/隔离编译场景下有限制)
const enum Direction { Up, Down }

// 联合类型字面量:零运行时开销,纯类型层面
type Status = "pending" | "success" | "error";

性能对比是本节最值得关注的点:

  • 普通 enum 会编译出一个真实的 JS 对象(双向映射),有内存占用和轻微查找开销,在 tree-shaking 时也不容易被优化掉。
  • const enum 在编译期被内联为字面量,理论上零开销,但在 isolatedModules(如 Babel、esbuild、SWC 单文件转译场景)下会被禁用或报错,Vite/Next.js 等现代工具链通常不推荐使用。
  • 联合类型字面量("a" | "b" | "c")是目前社区推荐的做法:类型安全 + 零运行时开销,缺点是不能像 enum 一样做反向查值或运行时遍历(需要额外定义一个 as const 数组配合 typeof 使用,见 1.8)。

2.4 泛型函数与泛型约束

function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  for (const key of keys) {
    result[key] = obj[key];
  }
  return result;
}

泛型本身没有运行时开销(编译后擦除),但泛型函数内部逻辑(比如上面的 for 循环)该有什么开销还是有什么开销,泛型只是让这段逻辑在类型层面更安全。


三、性能相关的高级技巧专项分析

技巧编译期开销运行时开销备注
条件类型 / infer / 模板字面量类型中~高(复杂时显著拖慢 tsc)无类型全部在编译期擦除
深度递归类型高,可能超出深度限制报错无注意善用 tail-recursion 优化写法
satisfies极低无纯类型检查,无产物变化
as const极低无只影响类型推导
可选链 ?. / ??低目标为 ES2020+ 时为 0;转译到 ES5 时有极小开销现代浏览器/Node 均原生支持
装饰器低有,每次方法调用多一层闭包高频路径慎用
数字 enum低有内存 + 查表开销,且不利于 tree-shaking建议用联合类型字面量替代
const enum极低无(内联)现代构建工具链下有兼容性风险
泛型中(复杂泛型推导拖慢 IDE 响应)无类型擦除,逻辑本身开销不变

核心结论:TypeScript 绝大多数"高级技巧"都发生在类型层面,编译后被完全擦除,不会带来任何运行时性能损失。真正需要关注运行时性能的只有三类:

  1. 枚举(尤其是数字枚举)——会生成真实对象,建议优先用联合类型字面量 + as const。
  2. 装饰器——本质是包装函数,会引入额外的调用层级,热路径慎用。
  3. 过度复杂的类型体操——虽然不影响运行时,但会显著拖慢 tsc 编译速度和编辑器的类型推导响应速度(IDE 卡顿),在大型项目中属于隐性的"开发体验性能损失",需要用 type-coverage、tsc --extendedDiagnostics 等工具监控。

四、实践建议

  • 优先用**联合类型字面量 + as const**替代传统 enum,兼顾类型安全和运行时性能。
  • satisfies 应该成为定义配置对象、路由表、状态机的默认选择,替代直接的类型标注。
  • 类型体操类工具类型(DeepPartial、ExtractParams 等)建议统一放进 types/utils.ts,避免在业务代码里到处手写递归类型导致编译变慢。
  • 装饰器目前分为 Stage 3(TS 5.0+ 原生支持的新提案)和 experimentalDecorators(旧版,NestJS/Angular 仍在用)两套实现,选型时注意版本兼容性,二者语义和执行时机有差异。
  • 复杂类型定义写完后,建议用 // @ts-expect-error 配合单测断言类型行为,防止后续重构悄悄破坏类型推导。

TypeScript 类型系统的强大之处在于:绝大多数"聪明"的写法都是编译期的免费午餐。真正要花心思权衡的性能问题,反而集中在少数几个会生成运行时产物的特性上(enum、装饰器)。把这条线分清楚,才能既写出优雅的类型代码,又不给运行时性能挖坑。

评论 (0)

暂无评论,快来抢沙发吧!

目录
  • 一、类型层面的技巧(编译期,零运行时开销)
  • 二、语法糖(编译期转译,可能有极小运行时差异)
  • 三、性能相关的高级技巧专项分析
  • 四、实践建议