为什么要重写编译器

TypeScript 正在经历诞生以来最大的一次底层变革:官方把编译器从自举的 TypeScript/JavaScript 移植到了 Go,代号 tsgo(TypeScript Native)。原因很现实——tsc 与 VS Code 的语言服务跑在 V8 上,单线程、GC 压力大,在中大型仓库里一次全量检查动辄几十秒,编辑器跳转与补全也会随项目规模线性变慢。改用 Go 之后,可以天然利用多核并行、更紧凑的内存布局和更低的 GC 开销,官方给出的目标是编辑器启动与类型检查提速一个数量级,同时在语言层面保持 100% 兼容。

快速体验原生编译器

原生预览版以 npm 包形式分发,无需改动现有 tsconfig.json:

# 安装原生预览编译器
npm i -D @typescript/native-preview

# 类型检查(等价于 tsc --noEmit)
npx tsgo --noEmit

# 增量构建/工程引用
npx tsgo --build

# 监听模式
npx tsgo --noEmit --watch

在 VS Code 中可以安装 TypeScript Native 预览插件,或在设置里开启实验性选项,让编辑器后台直接使用 Go 版语言服务。CI 上的收益最直观:把 tsc --noEmit 换成 tsgo --noEmit,很多仓库的流水线时间会直接砍半。

现代 TS 里最值得掌握的三个类型特性

1. satisfies:既校验结构,又保留字面量类型

type Route = { path: string; method: 'GET' | 'POST' };

const routes = {
  home:  { path: '/', method: 'GET' },
  login: { path: '/login', method: 'POST' },
} satisfies Record<string, Route>;

routes.home.method;
// 类型收窄为 'GET',而不是宽泛的 'GET' | 'POST'

satisfies 解决了“用类型标注会丢失字面量推断,不用标注又无法校验”的两难,是配置表、路由表、主题表的最佳拍档。

2. const 类型参数:让泛型默认按字面量推断

function tuple<const T extends readonly unknown[]>(...args: T): T {
  return args;
}

const t = tuple(1, 'a', true);
// 推断为 readonly [1, "a", true],而不是 (string | number | boolean)[]

以前要拿到元组类型必须写 as const,现在库作者可以直接在函数签名层面“默认 const”。

3. NoInfer:阻止无关位置的泛型推断

function createStore<T>(initial: T, update: (v: NoInfer<T>) => T) {
  let value = initial;
  return { get: () => value, set: (v: NoInfer<T>) => (value = update(v)) };
}

const store = createStore({ count: 0 }, (prev) => ({ count: prev.count + 1 }));

NoInfer<T> 让第二个参数只做“检查”不参与推断,避免推断结果被意外 widen。

类型体操实战:类型安全的对象路径

下面这套递归类型可以为任意嵌套对象生成合法路径联合类型,常用于表单校验、国际化键名和状态选择器:

type Path<T> = T extends object
  ? {
      [K in keyof T & string]: T[K] extends object
        ? K | `${K}.${Path<T[K]>}`
        : K;
    }[keyof T & string]
  : never;

type User = { id: number; profile: { name: string; city: string } };

// "id" | "profile" | "profile.name" | "profile.city"
type UserPath = Path<User>;

function getByPath<T extends object, K extends Path<T>>(obj: T, path: K): unknown {
  return path.split('.').reduce<any>((acc, key) => acc?.[key], obj);
}

getByPath({ id: 1, profile: { name: 'Ada', city: 'London' } } as User, 'profile.city');
// getByPath(user, 'profile.age') -> 编译报错

迁移建议与踩坑

  1. 先并行跑:CI 里同时执行 tsc --noEmit 与 tsgo --noEmit,对比诊断输出,确认无误后再切换。
  2. 插件生态:依赖 TS 内部 API 的自定义 transformer、旧版 lint 插件仍需要 JS 版编译器,建议留在灰度阶段。
  3. 渐进收紧:趁提速后有余量,把 strict、noUncheckedIndexedAccess、exactOptionalPropertyTypes 全部打开,类型体操才真正有价值。

小结

tsgo 的意义不只是“更快”,它让类型系统从“拖慢开发”变成“可以放心加码”。配合 satisfies、const 类型参数与 NoInfer 这些现代特性,TypeScript 正在把 JS 生态的开发体验推向一个新的台阶。