9. TypeScript:从类型系统到工程实践

TypeScript 最开始解决的问题其实很简单:

JavaScript 太灵活了。

一个变量今天可能是:

ini 复制代码
let value = 'hello';

过一会儿又变成:

ini 复制代码
value = 123;

对于小型脚本来说,这种灵活性没有太大问题。

但随着项目越来越大:

  • 函数越来越多
  • 模块越来越多
  • 开发人员越来越多
  • API 越来越复杂
  • 数据结构越来越复杂

"类型是什么"逐渐变成了一个必须回答的问题。

于是 TypeScript 出现了。

但到了 2026 年,如果还把 TypeScript 理解成:

"JavaScript 加上类型"

其实已经不够了。

现代 TypeScript 更值得学习的是:

python 复制代码
JavaScript
   ↓
Type System
   ↓
Static Analysis
   ↓
Type Inference
   ↓
Generic Programming
   ↓
Type-level Programming
   ↓
Runtime Boundary
   ↓
Engineering Toolchain

尤其是在 AI Coding 时代,TypeScript 的价值不仅是"帮我们提示错误"。

它还逐渐成为:

人与 AI、代码与工程之间的一层可验证约束。


第一章:为什么现代 JavaScript 需要 TypeScript

1.1 JavaScript 的灵活性也是复杂度的来源

JavaScript 是动态类型语言。

例如:

ini 复制代码
let value = 'hello';

value = 100;

value = {
  name: 'Tom',
};

从语言角度来看,这完全合法。

但如果这个变量被大量函数共同使用:

javascript 复制代码
function process(value) {
  // ...
}

我们很难仅仅通过 JavaScript 本身知道:

复制代码
value 到底应该是什么?

例如:

javascript 复制代码
function getUserName(user) {
  return user.name;
}

调用者可能传入:

css 复制代码
getUserName({
  name: 'Tom',
});

也可能传入:

css 复制代码
getUserName({
  username: 'Tom',
});

甚至:

scss 复制代码
getUserName(null);

真正运行到代码时才可能发现问题。

这就是动态类型语言非常典型的问题:

很多错误直到运行时才能暴露。


1.2 TypeScript 到底解决什么问题?

TypeScript 在 JavaScript 的基础上增加了静态类型系统。

例如:

ini 复制代码
const name: string = 'Tom';

const age: number = 18;

如果:

ini 复制代码
const age: number = '18';

TypeScript 就可以在开发阶段发现问题。

因此 TypeScript 最核心的价值可以理解成:

sql 复制代码
JavaScript
+
Static Type System
+
Static Analysis

它可以帮助我们:

  • 提前发现类型错误
  • 获得更准确的 IDE 提示
  • 描述复杂的数据结构
  • 建立函数和模块之间的契约
  • 提高大型项目的可维护性
  • 为重构提供更多静态信息

1.3 TypeScript 并不是新的运行时

这是学习 TypeScript 必须建立的第一个认知。

TypeScript:

ini 复制代码
const name: string = 'Tom';

最终不会让浏览器执行:

c 复制代码
string

浏览器最终执行的仍然是 JavaScript。

也就是说:

css 复制代码
TypeScript
   ↓
Type Checking / Transform
   ↓
JavaScript
   ↓
Runtime

TypeScript 的类型信息主要存在于:

编译阶段 / 类型检查阶段

而不是 JavaScript 的运行时。

所以:

ini 复制代码
const age: number = 18;

并不会在浏览器运行时自动保证:

typescript 复制代码
age 一定是 number

这就引出了后面非常重要的一条边界:

静态类型安全 ≠ 运行时数据安全。


1.4 TypeScript 与 Flow

早期 React 生态中,Flow 也是非常重要的类型检查方案。

原文中特别展示了 React 源码中的 Flow 类型定义,例如:

bash 复制代码
export type ComponentType<-P> =
  React$ComponentType<P>;

export type ElementType =
  React$ElementType;

export type Element<+C> =
  React$Element<C>;

Flow 与 TypeScript 都试图解决类似的问题:

python 复制代码
JavaScript
   ↓
Type Annotation
   ↓
Static Type Checking

两者都可以:

  • 描述变量类型
  • 描述函数参数
  • 描述返回值
  • 描述对象结构
  • 提供 IDE 类型提示
  • 在开发阶段发现错误

不过今天学习这部分时,更适合把 Flow 放在:

JavaScript 类型系统的发展历史与 React 生态背景

中理解,而不是把 Flow 和 TypeScript 作为两个同等主流的工程选择展开。

真正需要掌握的是:

为什么大型 JavaScript 项目需要静态类型系统,以及 TypeScript 是如何解决这个问题的。


第二章:TypeScript 类型系统

TypeScript 真正的核心不是:

c 复制代码
const name: string;

而是:

如何用类型系统描述真实业务。

这一章从基础类型一直进入泛型和类型体操。


2.1 基础类型

最基础的类型包括:

ini 复制代码
let name: string = 'Tom';

let age: number = 18;

let isAdmin: boolean = false;

数组:

typescript 复制代码
const numbers: number[] = [1, 2, 3];

const names: Array<string> = [
  'Tom',
  'Jack',
];

元组:

typescript 复制代码
const user: [string, number] = [
  'Tom',
  18,
];

2.2 any、unknown 与 never

any

ini 复制代码
let value: any = 'hello';

value = 123;
value = {};

any 会关闭很多 TypeScript 的检查。

因此:

any 可以解决问题,但也可能绕过类型系统。

大型项目中应该尽量避免无意义的 any 扩散。


unknown

如果一个值来自未知来源:

ini 复制代码
const value: unknown = getValue();

TypeScript 不允许你直接使用:

ini 复制代码
value.toUpperCase();

必须先进行类型判断:

ini 复制代码
if (typeof value === 'string') {
  console.log(value.toUpperCase());
}

因此:

sql 复制代码
any
→ 放弃类型检查

unknown
→ 要求先确认类型

现代 TypeScript 中,面对"不知道类型的数据",通常更应该优先考虑 unknown。


never

never 表示:

不可能存在的值。

例如一个永远抛出异常的函数:

typescript 复制代码
function fail(message: string): never {
  throw new Error(message);
}

它也经常用于穷尽性检查:

php 复制代码
type Status =
  | 'loading'
  | 'success'
  | 'error';

function handleStatus(status: Status) {
  switch (status) {
    case 'loading':
      return;

    case 'success':
      return;

    case 'error':
      return;

    default: {
      const exhaustiveCheck: never = status;
      return exhaustiveCheck;
    }
  }
}

如果以后新增:

arduino 复制代码
'cancelled'

这里就会出现类型错误。

这是一种非常实用的类型系统技巧。


2.3 Union:联合类型

联合类型表示:

一个值可以属于多个类型中的一个。

ini 复制代码
let myFavoriteNumber: string | number;

myFavoriteNumber = 'one';

myFavoriteNumber = 1;

实际业务中非常常见:

ini 复制代码
type ID = string | number;

然后:

javascript 复制代码
function getUser(id: ID) {
  // ...
}

2.4 Intersection:交叉类型

交叉类型可以把多个类型组合起来。

原文中的例子:

ini 复制代码
type NameProtocol = {
  name: string;
};

type PersonLikeProtocol = {
  age: number;
  say: () => void;
};

type Student =
  NameProtocol &
  PersonLikeProtocol;

那么:

javascript 复制代码
const student: Student = {
  name: 'Tom',
  age: 18,

  say() {
    console.log('hello');
  },
};

必须同时满足:

diff 复制代码
NameProtocol
+
PersonLikeProtocol

2.5 Interface 与 Type

TypeScript 中经常会出现:

typescript 复制代码
interface User {
  name: string;
  age: number;
}

也可以写:

ini 复制代码
type User = {
  name: string;
  age: number;
};

两者在大量普通业务场景中都可以描述对象结构。

type 的表达能力更广,可以直接表达:

ini 复制代码
type ID = string | number;

而 interface 更适合描述:

  • 对象结构
  • 类契约
  • 可扩展的对象类型

例如:

kotlin 复制代码
interface User {
  name: string;
}

interface User {
  age: number;
}

接口可以进行声明合并。

实际项目中不需要把它们当成"谁绝对正确"的问题。

更重要的是:

根据类型表达的意图选择合适的工具。


2.6 类型守卫与类型收窄

TypeScript 最有价值的能力之一,就是:

Type Narrowing

例如:

typescript 复制代码
function print(value: string | number) {
  if (typeof value === 'string') {
    console.log(value.toUpperCase());
  } else {
    console.log(value.toFixed(2));
  }
}

进入:

ini 复制代码
if (typeof value === 'string')

之后,TypeScript 就知道:

c 复制代码
value → string

这就是类型收窄。

常见的类型守卫包括:

javascript 复制代码
typeof
instanceof
in
Array.isArray()

2.7 类型保护

原文中有一个非常实用的例子:

javascript 复制代码
function isHTMLElement(
  e: EventTarget | null
): e is HTMLElement {
  return e instanceof HTMLElement;
}

然后:

ini 复制代码
const dom = document.querySelector('dom');

dom?.addEventListener('mousedown', (ev) => {
  const target = ev.target;

  if (isHTMLElement(target)) {
    target.classList.add('active');
  }
});

这里:

csharp 复制代码
e is HTMLElement

就是 TypeScript 的:

User-defined Type Guard

它告诉 TypeScript:

如果这个函数返回 true,那么这里的 e 可以认为是 HTMLElement。

这在处理:

  • DOM
  • API 数据
  • 联合类型
  • 不同业务状态

时都非常有价值。


2.8 泛型:TypeScript 真正的核心能力

原文将泛型作为重点内容,这是非常正确的。

泛型解决的问题是:

我不知道具体类型是什么,但我希望保留这个类型之间的关系。

最简单的例子:

r 复制代码
function identity<T>(arg: T): T {
  return arg;
}

使用:

ini 复制代码
const result1 = identity(123);

const result2 = identity('hello');

TypeScript 可以推导:

typescript 复制代码
result1 → number

result2 → string

这里的重点不是:

"T 代表一个类型。"

而是:

输入是什么类型,输出就保持对应的类型关系。


2.9 泛型约束 extends

原文中的例子:

typescript 复制代码
function sayHello<P extends { name: string }>(
  person: P
) {
  console.log(person.name);
}

这里:

css 复制代码
P extends { name: string }

不是表示:

P 一定就是 { name: string }

而是表示:

P 至少必须满足 { name: string } 这个约束。

因此:

php 复制代码
sayHello({
  name: 'Tom',
  age: 18,
});

是可以的。

因为:

css 复制代码
{
  name: string;
  age: number;
}

满足:

c 复制代码
{
  name: string;
}

这也是 TypeScript 类型系统中非常重要的:

Structural Typing(结构化类型系统)


2.10 泛型类:Queue

原文通过 Queue 来解释泛型,这个例子非常适合保留。

没有泛型时:

kotlin 复制代码
class Queue {
  private data: unknown[] = [];

  push(item: unknown) {
    this.data.push(item);
  }

  pop() {
    return this.data.shift();
  }
}

问题是:

arduino 复制代码
const queue = new Queue();

queue.push(0);
queue.push('1');

任何类型都可以塞进去。

但我们真正想要的是:

复制代码
放进去什么类型
 ↓
拿出来还是什么类型

于是:

kotlin 复制代码
class Queue<T> {
  private data: T[] = [];

  push(item: T) {
    this.data.push(item);
  }

  pop(): T | undefined {
    return this.data.shift();
  }
}

使用:

arduino 复制代码
const queue = new Queue<number>();

queue.push(0);

queue.push('1');
// Error

读取:

ini 复制代码
const value = queue.pop();

if (value !== undefined) {
  value.toFixed(2);
}

这样类型关系就建立起来了:

scss 复制代码
Queue<number>
    ↓
push(number)
    ↓
pop(): number | undefined

2.11 keyof

keyof 可以获取一个类型的属性键。

例如:

ini 复制代码
interface User {
  name: string;
  age: number;
  sex: number;
}

type UserKeys = keyof User;

得到:

bash 复制代码
type UserKeys =
  | 'name'
  | 'age'
  | 'sex';

于是可以:

r 复制代码
function getValue<T, K extends keyof T>(
  obj: T,
  key: K
): T[K] {
  return obj[key];
}

使用:

ini 复制代码
const user = {
  name: 'Tom',
  age: 18,
};

const name = getValue(user, 'name');

const age = getValue(user, 'age');

TypeScript 可以继续推导:

typescript 复制代码
name → string
age → number

这就是类型系统真正强大的地方:

类型之间可以建立关系。


2.12 typeof 在类型系统中的作用

JavaScript 中:

csharp 复制代码
typeof value

是运行时操作。

但 TypeScript 中:

csharp 复制代码
typeof value

还可以出现在类型位置。

例如:

ini 复制代码
const config = {
  apiUrl: '/api',
  timeout: 3000,
};

type Config = typeof config;

于是:

复制代码
Config

等价于:

css 复制代码
{
  apiUrl: string;
  timeout: number;
}

这在配置对象、常量映射等场景非常实用。


2.13 Indexed Access Type

如果有:

typescript 复制代码
interface User {
  name: string;
  age: number;
}

可以:

ini 复制代码
type UserName = User['name'];

得到:

c 复制代码
string

还可以:

ini 复制代码
type UserValue = User[keyof User];

得到:

typescript 复制代码
string | number

它是很多高级类型工具的基础。


2.14 Mapped Types

原文中的 Pick:

scala 复制代码
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

这段代码非常值得理解。

例如:

ini 复制代码
interface User {
  name: string;
  age: number;
  sex: number;
}

type UserBase = Pick<User, 'name' | 'age'>;

得到:

css 复制代码
{
  name: string;
  age: number;
}

这里:

csharp 复制代码
[P in K]

就是:

Mapped Type

它可以根据已有类型批量生成新的类型。


2.15 Conditional Types

条件类型:

java 复制代码
T extends U
  ? X
  : Y

例如:

typescript 复制代码
type IsString<T> =
  T extends string
    ? true
    : false;

那么:

ini 复制代码
type A = IsString<string>;
// true

type B = IsString<number>;
// false

条件类型是 TypeScript 类型体操的重要基础。


2.16 infer

infer 是高级类型处理中非常重要的关键字。

例如获取函数返回值:

typescript 复制代码
type MyReturnType<T> =
  T extends (...args: any[]) => infer R
    ? R
    : never;

使用:

ini 复制代码
type Func = () => string;

type Result = MyReturnType<Func>;

得到:

c 复制代码
string

因为:

复制代码
infer R

让 TypeScript 在条件类型中:

推导出某个位置上的类型。


2.17 获取函数参数类型

原文中的例子:

r 复制代码
type ParamType<T> =
  T extends (arg: infer P) => any
    ? P
    : T;

使用:

ini 复制代码
interface User {
  name: string;
  age: number;
}

type Func = (user: User) => void;

type Param = ParamType<Func>;

得到:

sql 复制代码
User

这类技巧就是:

Type-level Programming

也就是所谓的:

类型体操。


第三章:TypeScript 工程配置与模块系统

TypeScript 不只是类型语法。

真正进入项目以后,很快就会遇到:

vbnet 复制代码
tsconfig.json
module
target
lib
moduleResolution
strict
noEmit
declaration
paths

因此:

会写类型 ≠ 会使用 TypeScript。


3.1 tsconfig.json

一个最基础的配置:

json 复制代码
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "strict": true
  }
}

其中:

复制代码
target

描述:

TypeScript 最终输出 JavaScript 时面向什么 ECMAScript 环境。

而:

arduino 复制代码
module

描述:

模块代码应该如何处理。


3.2 strict

原文中特别强调:

json 复制代码
{
  "compilerOptions": {
    "strict": true
  }
}

这是现代 TypeScript 项目非常重要的配置。

strict 会开启一组更严格的类型检查规则,包括:

  • strictNullChecks
  • noImplicitAny
  • strictFunctionTypes
  • strictBindCallApply
  • strictPropertyInitialization
  • 等

TypeScript 官方文档也明确说明,strict 是一组严格类型检查选项的总开关;未来版本也可能继续在这一模式下增加更严格的检查。

因此新项目通常应该优先考虑:

json 复制代码
{
  "compilerOptions": {
    "strict": true
  }
}

而不是:

json 复制代码
{
  "compilerOptions": {
    "strict": false
  }
}

然后大量依赖:

arduino 复制代码
any

逃避类型问题。


3.3 module 与 moduleResolution

这是现代 TypeScript 中非常容易配置错误的地方。

需要区分:

arduino 复制代码
module

和:

复制代码
moduleResolution

module

描述:

TypeScript 如何理解/输出模块格式。

例如:

vbnet 复制代码
ESNext
CommonJS
NodeNext
Preserve

moduleResolution

描述:

TypeScript 如何寻找 import / require 对应的模块。

现代项目中常见:

复制代码
nodenext
bundler

TypeScript 官方目前建议:

  • 面向现代 Node.js:使用 node16 / nodenext
  • 使用现代 Bundler:使用 bundler

并且 moduleResolution 应该和实际运行环境或 Bundler 的模块解析行为匹配。


3.4 Bundler 模式

如果项目使用:

  • Vite
  • Webpack
  • esbuild
  • SWC
  • 其他现代 Bundler

可以考虑:

json 复制代码
{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler",
    "noEmit": true
  }
}

为什么?

因为 Bundler 的模块解析方式和 Node.js 原生 ESM 并不完全一样。

TypeScript 的:

makefile 复制代码
moduleResolution: bundler

就是为了模拟现代 Bundler 常见的模块解析行为。


3.5 Node.js 项目

如果 TypeScript 代码最终直接面向现代 Node.js 运行环境,则应该理解:

diff 复制代码
ESM
+
CommonJS
+
package.json
+
exports
+
imports
+
moduleResolution

例如:

json 复制代码
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true
  }
}

TypeScript 的 Node 模式会根据 Node.js 对 ESM / CommonJS 的规则进行模块解析。

所以现在不能再简单地把:

ini 复制代码
module = CommonJS

当成所有 Node.js 项目的默认答案。


3.6 TypeScript 不等于编译器 + Bundler

这是现代工程里非常重要的一点。

TypeScript 可以负责:

diff 复制代码
Type Checking
+
TypeScript → JavaScript

但现代项目中,经常由:

复制代码
Vite
Webpack
esbuild
SWC
Oxc

等工具承担实际构建流程。

于是可能出现:

css 复制代码
TypeScript
 ↓
Type Checking

Bundler
 ↓
Transform / Bundle
 ↓
Production Assets

甚至:

json 复制代码
{
  "compilerOptions": {
    "noEmit": true
  }
}

也就是说:

TypeScript 可以只负责类型检查,而不负责最终 JavaScript 输出。

这是理解现代 TypeScript 工程非常重要的变化。


第四章:TypeScript 高级能力与运行时边界

这一章是整篇文章最重要的部分之一。

因为真正的大型项目会遇到一个问题:

TypeScript 类型检查得很好,但是 API 返回的数据真的可靠吗?

答案是:

不一定。


4.1 TypeScript 类型不会自动保护运行时数据

例如:

typescript 复制代码
interface User {
  name: string;
  age: number;
}

我们写:

ini 复制代码
const user: User = data;

这并不意味着:

kotlin 复制代码
data

在运行时真的就是:

css 复制代码
{
  name: string;
  age: number;
}

尤其当数据来自:

  • HTTP API
  • 用户输入
  • LocalStorage
  • URL
  • 数据库
  • 第三方服务
  • WebSocket
  • 文件

时,TypeScript 无法在运行时替你验证真实数据。


4.2 一个典型问题

ini 复制代码
interface User {
  name: string;
  age: number;
}

const user: User = JSON.parse(response);

console.log(user.name);

TypeScript 看起来认为:

sql 复制代码
user → User

但:

javascript 复制代码
JSON.parse(response)

的数据来源是运行时。

它完全可能是:

json 复制代码
{
  "name": 123,
  "age": "hello"
}

TypeScript 并不会自动阻止这种数据进入程序。

因此:

python 复制代码
Static Type

和:

复制代码
Runtime Validation

是两个不同的问题。


4.3 Zod

原文推荐了 Zod,并通过它解决 Runtime Check。

例如:

csharp 复制代码
import { z } from 'zod';

const User = z.object({
  username: z.string(),
});

验证:

css 复制代码
User.parse({
  username: 'Ludwig',
});

如果数据错误:

css 复制代码
User.parse({
  username: 123,
});

就会抛出验证错误。

也可以使用:

ini 复制代码
User.safeParse(data);

得到:

yaml 复制代码
{
  success: true,
  data: ...
}

或者:

yaml 复制代码
{
  success: false,
  error: ...
}

4.4 Schema → Type

Zod 更重要的一个特点是:

Schema 可以参与类型推导。

例如:

ini 复制代码
const User = z.object({
  username: z.string(),
});

type User = z.infer<typeof User>;

这样就形成:

python 复制代码
Schema
 ↓
Runtime Validation
 ↓
Type Inference
 ↓
TypeScript Type

这比单独维护:

kotlin 复制代码
interface User {}

和:

ini 复制代码
const userSchema = ...

两套定义更加统一。

原文提到 tRPC 采用 Zod 来解决类型安全问题,这个例子真正值得学习的也是这个思想:

让运行时 Schema 和静态类型建立联系。


4.5 类型体操应该怎么学?

TypeScript 类型体操非常有意思。

例如:

ini 复制代码
type Partial<T> = {
  [P in keyof T]?: T[P];
};

又或者:

r 复制代码
type ReturnType<T> =
  T extends (...args: any[]) => infer R
    ? R
    : any;

继续深入可以学习:

  • keyof
  • typeof
  • Indexed Access
  • Conditional Types
  • Mapped Types
  • Template Literal Types
  • infer
  • Recursive Types
  • Distributive Conditional Types

但需要注意:

类型越复杂,不代表代码越高级。

类型体操的目标应该是:

diff 复制代码
减少重复
+
建立约束
+
提高类型推导能力
+
改善开发体验

而不是:

为了展示 TypeScript 技巧,把一个简单问题写成几十行类型。


4.6 TypeScript 的高级类型边界

例如:

ini 复制代码
type DeepReadonly<T> = {
  readonly [K in keyof T]:
    T[K] extends object
      ? DeepReadonly<T[K]>
      : T[K];
};

这类类型非常强大。

但是一旦类型逻辑越来越复杂:

diff 复制代码
Mapped Type
+
Conditional Type
+
Infer
+
Recursive Type
+
Union Distribution

就会带来新的问题:

  • 编译速度下降
  • 类型错误难以理解
  • IDE 响应变慢
  • 维护成本增加
  • 新成员难以理解

所以高级 TypeScript 的核心不是:

能不能写复杂类型。

而是:

知道什么时候值得写复杂类型。


第五章:装饰器、编译器与 2026 年 TypeScript

最后这一章把原文中的:

  • Decorator
  • Reflect
  • TypeScript Compiler

以及 2026 年的 TypeScript 工程实践放在一起理解。


5.1 TypeScript 装饰器

原文使用了:

kotlin 复制代码
@Controller()
export class AppController {}

以及:

java 复制代码
@Get()

这种 NestJS 风格的装饰器。

装饰器的核心思想可以理解成:

对类、方法、属性等声明进行额外描述或行为扩展。

例如:

javascript 复制代码
function Log() {
  // ...
}

class UserService {
  @Log()
  getUser() {
    // ...
  }
}

框架可以进一步利用这些信息构建:

复制代码
Controller
 ↓
Metadata
 ↓
Route
 ↓
Runtime Registration

这也是 NestJS、Angular 等框架大量使用装饰器的原因。


5.2 2026 年必须区分两种 Decorator

这是原文需要重点升级的地方。

过去 TypeScript 大量使用:

json 复制代码
{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

这种方式属于 TypeScript 历史上的 legacy / experimental decorators。

而 TypeScript 5.0 已经支持基于新的 Decorators 提案的标准化语义。

两者并不是简单的同一套 API。

尤其需要注意:

新 Decorator 与旧的 experimentalDecorators 语义存在明显差异。

例如新的 Decorator:

  • 不依赖 experimentalDecorators
  • 类型检查和生成方式不同
  • 不兼容 emitDecoratorMetadata
  • 不支持参数装饰器

TypeScript 官方对此有明确说明。

所以今天阅读旧的 NestJS / TypeScript 教程时,必须注意:

复制代码
Legacy Decorators

和:

复制代码
Standard Decorators

不要混为一谈。


5.3 reflect-metadata 也要区分

原文使用:

arduino 复制代码
import 'reflect-metadata';

然后:

javascript 复制代码
Reflect.defineMetadata(...)
Reflect.getMetadata(...)

需要特别说明:

reflect-metadata 提供的 Metadata API 并不是 JavaScript 标准 Reflect 对象本身的一部分。

TypeScript 官方文档也明确说明,reflect-metadata 是一个额外库,并不是 ECMAScript 标准的一部分。

所以:

javascript 复制代码
Reflect.get()
Reflect.set()
Reflect.apply()

和:

javascript 复制代码
Reflect.getMetadata()
Reflect.defineMetadata()

是两套不同概念。

前者属于 ECMAScript 的 Reflect。

后者来自 reflect-metadata 生态。


5.4 TypeScript 编译器到底做了什么?

原文最后给出了 TypeScript Compiler 的几个核心模块:

复制代码
Scanner
Parser
Binder
Checker
Emitter

这部分非常值得保留。

可以把 TypeScript 编译器简单理解成:

dart 复制代码
TypeScript Source
        ↓
     Scanner
        ↓
      Parser
        ↓
       AST
        ↓
      Binder
        ↓
   Symbol / Scope
        ↓
      Checker
        ↓
   Type Checking
        ↓
      Emitter
        ↓
 JavaScript / Declaration

5.5 Scanner

Scanner 负责把源代码读取并识别成 Token。

例如:

ini 复制代码
const name = 'Tom';

可以抽象成:

ini 复制代码
const
name
=
'Tom'
;

这就是词法分析阶段。


5.6 Parser

Parser 将 Token 进一步组织成:

AST(Abstract Syntax Tree)

例如:

scss 复制代码
VariableDeclaration
 ├── Identifier(name)
 └── StringLiteral(Tom)

后续的类型检查和代码生成都建立在这样的语法结构之上。


5.7 Binder

Binder 负责建立符号之间的关系。

例如:

ini 复制代码
const name = 'Tom';

function printName() {
  console.log(name);
}

TypeScript 需要知道:

复制代码
name
 ↓
对应哪个声明?

Binder 就负责建立这种符号关系。

可以简单理解成:

sql 复制代码
Syntax
 ↓
Symbols
 ↓
Scope Relationships

5.8 Checker

Checker 是 TypeScript 类型系统的核心部分。

例如:

ini 复制代码
const age: number = '18';

Parser 可以理解这段代码的语法。

但:

typescript 复制代码
number
≠
string

是类型层面的问题。

因此:

复制代码
Parser
→ 语法正确吗?

Checker
→ 类型正确吗?

这是理解 TypeScript 编译器非常重要的区别。


5.9 Emitter

最后:

复制代码
Emitter

负责生成输出。

例如:

复制代码
TypeScript
 ↓
JavaScript

也可以生成:

复制代码
.d.ts

等声明文件。

不过现代工程中,最终 JavaScript 的生成并不一定完全由 TypeScript 自己完成。

例如一个 Vite 项目可能是:

css 复制代码
TypeScript
 ↓
Type Checking

Vite / Oxc / esbuild / SWC
 ↓
Transform / Bundle
 ↓
JavaScript

所以学习 TypeScript Compiler 原理时,应该把:

Type Checker

和:

Build Tool

区分开。


2026 年重新理解 TypeScript

TypeScript 最开始解决的是:

JavaScript 类型不安全。

后来逐渐发展成:

复制代码
类型定义
 ↓
类型推导
 ↓
泛型
 ↓
高级类型
 ↓
类型体操
 ↓
工程配置
 ↓
Runtime Validation
 ↓
Compiler

而今天真正值得关注的是:

TypeScript 已经不只是"写类型"。


6.1 TypeScript 是工程约束层

一个现代 Web 应用的数据流可能是:

javascript 复制代码
用户输入
   ↓
HTTP Request
   ↓
API
   ↓
JSON
   ↓
Runtime Validation
   ↓
TypeScript Type
   ↓
Business Logic
   ↓
Database

其中:

复制代码
TypeScript

解决的是:

开发阶段的数据结构和代码关系。

而:

复制代码
Zod / Schema Validation

解决的是:

运行时真实数据是否符合预期。

所以一个成熟系统通常是:

diff 复制代码
Static Type
+
Runtime Validation

而不是:

ini 复制代码
TypeScript
= Runtime Safety

6.2 TypeScript 与 AI Coding

2026 年学习 TypeScript,还有一个新的意义。

AI 可以非常快地生成:

csharp 复制代码
interface User {
  id: string;
  name: string;
}

甚至可以直接生成:

bash 复制代码
type DeepPartial<T> = ...

但真正的问题是:

这个类型到底是不是业务真正需要的?

例如 AI 很容易生成:

ini 复制代码
type UserResponse =
  | {
      success: true;
      data: User;
    }
  | {
      success: false;
      error: string;
    };

这看起来很合理。

但你还需要判断:

  • API 实际返回是不是这样?
  • 后端有没有统一错误格式?
  • 这个类型是否和运行时 Schema 一致?
  • 是否存在未覆盖的状态?
  • 是否真的需要这么复杂的泛型?
  • 类型是否已经开始影响可读性?

因此 AI Coding 时代:

TypeScript 的价值不只是让 AI 写出类型,而是让开发者能够验证 AI 写出的类型是否真的表达了系统。


TypeScript 学习路线

如果按照实际开发能力,可以这样学习:

python 复制代码
第一阶段
基础类型
 ↓
Interface / Type
 ↓
Union / Intersection
 ↓
Type Narrowing

然后:

javascript 复制代码
第二阶段
Generic
 ↓
extends
 ↓
keyof
 ↓
typeof
 ↓
Indexed Access

继续:

python 复制代码
第三阶段
Conditional Types
 ↓
Mapped Types
 ↓
infer
 ↓
Utility Types
 ↓
Template Literal Types

再进入:

复制代码
第四阶段
tsconfig
 ↓
ESM / CommonJS
 ↓
moduleResolution
 ↓
Bundler
 ↓
Node

最后:

python 复制代码
第五阶段
Runtime Validation
 ↓
Decorator
 ↓
Compiler
 ↓
Type-level Programming
 ↓
大型项目类型架构

总结

TypeScript 真正值得掌握的,不是:

go 复制代码
interface 怎么写
type 怎么写

而是下面这条完整链路:

markdown 复制代码
JavaScript
    ↓
静态类型
    ↓
类型推导
    ↓
泛型
    ↓
高级类型
    ↓
类型体操
    ↓
工程配置
    ↓
Runtime Validation
    ↓
Compiler

最终形成一个非常重要的认知:

TypeScript 不是为了让 JavaScript 看起来像 Java。

它真正解决的是:

如何让大型 JavaScript 系统中的数据、函数、模块和业务关系变得更加明确、可检查、可重构。

而到了 AI Coding 时代,这个能力反而更加重要。

因为 AI 可以快速生成大量代码,但:

markdown 复制代码
AI 负责生成
      ↓
TypeScript 负责约束
      ↓
Compiler 负责检查
      ↓
Runtime Validation 负责验证真实数据
      ↓
开发者负责判断设计是否正确

这才是 2026 年重新理解 TypeScript 最值得掌握的地方。

相关推荐
颜进强1 小时前
14 · NestJS ExecutionContext 执行上下文:守卫、拦截器、过滤器拿到的"同一个 context",为什么能力不一样?
前端·后端·ai编程
程序员Flycan1 小时前
🚀 跨域终结者:前端代理服务器(Proxy)原理解析与配置总结
前端
怕浪猫1 小时前
顶级模型一句话,AI 写出了能玩的 QQ飞车
前端·面试·github
huakoh1 小时前
MCP 报错分不清?先看响应里是 result 还是 error
前端
呃呃呃呃ex1 小时前
10. 现代前端工程化:ES6 React 项目实战与原理解析
前端·javascript
粥里有勺糖1 小时前
视野修炼-技术周刊第134期 | 豆豆眼头像
前端·github·aigc
浪遏1 小时前
D2C 系统架构复盘:从设计稿到代码,我们踩过的坑和最终方案
前端·javascript·后端
光影少年1 小时前
Fabric渲染流程
前端·react native·react.js
IT_陈寒1 小时前
我的JavaScript代码为啥在forEach里没按预期执行?
前端·人工智能·后端