侧边栏壁纸
博主昵称
YunZheng

独处未必孤独,喜欢就是自由

TypeScript 6 新特性工程实战brand、using、match

2026年07月09日 30阅读 0评论 1点赞
创创云-专业云计算服务器提供商

把 TypeScript 当武器使{6 个让你的同事看不太懂的实战模式}

本文面向已有 TS 基础、每天在生产环境写类型的开发者。不讲 interfacetype 的区别,只讲那些能让你在 Code Review 时被追问"这是怎么做到的"的硬核玩法。

为什么写这篇

上个月团队里一个实习生指着我的类型代码问:"这个 brand 是什么新关键字?"

我说是 TypeScript 6.0 的原生名义类型。他愣了一下:"TypeScript 6.0 出了?"

是的,2026 年 3 月 23 日 TypeScript 6.0 正式发布了——而很多人还在用 5.3 的思维写类型。

这篇不是版本发布说明的搬运工,而是从真实工程痛点出发,串联 TS 5.x 到 6.0 最值得掌握的 6 个实战模式。


模式一:用 Branded Types 消灭"字符串恐怖主义"

痛点

看看你项目里有没有这种代码:

async function getUser(id: string) { /* ... */ }
async function getOrder(id: string) { /* ... */ }
async function deleteFile(id: string) { /* ... */ }

// 三个函数都接受 string,你确定不会把 userId 传进 deleteFile?
const userId = "usr_abc123"
const orderId = "ord_xyz789"

getUser(orderId)     // ✅ 编译器说 OK,运行时直接爆炸
deleteFile(userId)   // ✅ 编译器说 OK,删错文件了

这就是结构类型系统的诅咒——只要长得一样,编译器就认为一样。

解法:TS 6.0 原生 brand 关键字

TypeScript 6.0 终于把"品牌类型"变成了语言内置功能:

// 定义品牌类型——编译期零成本
type UserId = brand<string, "UserId">;
type OrderId = brand<string, "OrderId">;
type FileId = brand<string, "FileId">;

// 品牌构造函数:运行时校验 + 编译期标记
function UserId(value: string): UserId {
  if (!value.startsWith("usr_")) throw new Error(`Invalid UserId: ${value}`);
  return value as UserId;
}

function OrderId(value: string): OrderId {
  if (!value.startsWith("ord_")) throw new Error(`Invalid OrderId: ${value}`);
  return value as OrderId;
}

// 现在函数签名变得诚实了
async function getUser(id: UserId) { /* ... */ }
async function getOrder(id: OrderId) { /* ... */ }
async function deleteFile(id: FileId) { /* ... */ }

// 用起来
const uid = UserId("usr_abc123");
const oid = OrderId("ord_xyz789");

getUser(uid);     // ✅ 正确
getUser(oid);     // ❌ TS Error: 'OrderId' is not assignable to 'UserId'
getUser("usr_abc123"); // ❌ TS Error: 'string' is not assignable to 'UserId'

编译期就拦住,连单元测试都不用写。

进阶:品牌类型 + 领域建模

// 不只是 ID——任何有语义约束的基础类型都该被品牌化
type Email = brand<string, "Email">;
type PositiveNumber = brand<number, "Positive">;
type NonEmptyString = brand<string, "NonEmpty">;

// 类型安全的转账函数
function transfer(
  from: UserId,
  to: UserId,
  amount: PositiveNumber
): Transaction {
  // 编译器保证:from/to 不会混淆,amount 不会是负数
  return { from, to, amount };
}

迁移提示

TS 6.0 之前可以用这个兼容写法:

// 兼容 TS < 6.0 的品牌类型模式
declare const __brand: unique symbol;
type Brand<B> = { [__brand]: B };
type Branded<T, B> = T & Brand<B>;

type UserId = Branded<string, "UserId">;

模式二:Template Literal Types —— 让字符串也拥有类型

痛点

// 这个 URL 写错了,但编译期完全不知道
fetch("/api/usr/42/posts/7")    // 应该是 /api/users,不是 /api/usr

// 路由参数传错了,也查不出来
router.push("/user/:id", { uid: "42" })  // 参数名到底是 id 还是 uid?

解法:编译期路由参数校验

核心思路:用类型系统提取路径中的参数名,编译期检查调用是否正确

// 1. 定义路由表,as const 锁定字面量
const routes = {
  home: "/",
  userDetail: "/user/:userId",
  postDetail: "/user/:userId/posts/:postId",
  settings: "/settings/:tab",
} as const;

type RouteName = keyof typeof routes;

// 2. 递归提取路径中的 :param 参数名
type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;

// 验证:
type Test = ExtractParams<"/user/:userId/posts/:postId">;
//   ^? "userId" | "postId" ✅

// 3. 把参数名联合类型转成对象类型
type ParamsFor<T extends string> = {
  [K in ExtractParams<T>]: string;
};

// 4. 组装类型安全的导航函数
function navigate<N extends RouteName>(
  name: N,
  ...args: ExtractParams<(typeof routes)[N]> extends never
    ? []
    : [params: ParamsFor<(typeof routes)[N]>]
): void {
  const pattern = routes[name];
  const params = (args[0] ?? {}) as Record<string, string>;
  const path = pattern.replace(/:(\w+)/g, (_, k) => params[k] ?? `:${k}`);
  console.log("→", path);
}

// 5. 效果:写错就红线
navigate("home");                                    // ✅ 不需要参数
navigate("userDetail", { userId: "42" });            // ✅ 提示需要 userId
navigate("postDetail", { userId: "42", postId: "7" }); // ✅ 两个都提示
navigate("postDetail", { userId: "42" });            // ❌ 缺少 postId
navigate("home", { userId: "42" });                  // ❌ home 不需要参数

扩展:这个模式能干什么?

场景类型体操
SQL 排序字段校验type OrderBy = \${Column} ${"ASC" \"DESC"}\``
CSS 属性校验type CSSValue = \${number}${"px" \"rem" \"vh"}\``
事件名自动生成type Handler = \on${Capitalize}\``
API 端点拼接type Endpoint = \${Method} ${Path}\``

通用公式:infer 拆字符串 → 递归提取结构 → 映射成类型,零运行时成本。


模式三:satisfies —— 类型世界的"质检员"

痛点

// 我要一个调色板配置
type Palette = Record<string, string | [number, number, number]>;

// 方式一:类型注解——丢掉精确类型
const p1: Palette = {
  red: [255, 0, 0],
  green: "#00ff00",
};
p1.red.toUpperCase(); // ❌ 运行时报错!red 是数组,但 TS 认为是 string | [number,number,number]

// 方式二:as 断言——放弃类型检查
const p2 = {
  red: [255, 0, 0],
  green: "#00ff00",  // 不小心写错了?没人告诉你
} as Palette;

解法:satisfies 两全其美

const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255],
} satisfies Palette;

// ✅ 编译期检查:所有值必须符合 Palette 定义
// ✅ 精确推断:每个属性保留自己的具体类型
palette.red.toUpperCase();  // ❌ TS Error: red 是 number[],没有 toUpperCase
palette.green.toUpperCase(); // ✅ green 推断为 string,可以用字符串方法
palette.red.map(x => x * 2); // ✅ red 是 number[],可以 .map()

// 这才是真正的"我全都要"

实战:API 响应类型

interface ApiResponse {
  status: number;
  data: unknown;
}

// 每个端点的响应都满足 ApiResponse 结构,但 data 类型各不相同
const responses = {
  getUser: { status: 200, data: { id: "1", name: "Alice" } },
  getPosts: { status: 200, data: [{ id: "1", title: "Hello" }] },
  error: { status: 404, data: { message: "Not found" } },
} satisfies Record<string, ApiResponse>;

// TS 知道每个属性的精确 data 类型
responses.getUser.data.name;   // ✅ 有 name 属性
responses.getPosts.data[0].title; // ✅ 有 title 属性
responses.error.data.message;  // ✅ 有 message 属性

核心原则:能写 satisfies 的地方,不要写 as


模式四:using 关键字 —— 再也不写 finally

痛点

// 这种代码你写过多少次?
async function processFile(path: string) {
  const file = await openFile(path);
  try {
    const data = await file.read();
    return process(data);
  } finally {
    await file.close(); // 如果这里忘了呢?
  }
}

解法:显式资源管理

// 定义一个可释放的资源
class DatabaseConnection implements AsyncDisposable {
  private connected = false;

  async connect() {
    this.connected = true;
    console.log("Connected");
  }

  async query(sql: string) {
    if (!this.connected) throw new Error("Not connected");
    return `Result of: ${sql}`;
  }

  // 关键方法:作用域结束时自动调用
  async [Symbol.asyncDispose]() {
    this.connected = false;
    console.log("Disconnected");
  }
}

// 使用 using——无需 try/finally
async function runQuery() {
  await using db = new DatabaseConnection();
  await db.connect();

  const users = await db.query("SELECT * FROM users");
  const orders = await db.query("SELECT * FROM orders");

  // 即使中间抛异常,db 也会自动断开
  return { users, orders };
}

关键点:

  • 即使中间抛出异常,清理逻辑也一定会执行
  • 多个 using 按声明顺序的反向(LIFO)自动清理
  • 搭配 AsyncDisposableStack 可以批量管理资源

模式五:Pattern Matching —— 告别 switch-case 地狱

痛点

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle": return Math.PI * shape.radius ** 2;
    case "rectangle": return shape.width * shape.height;
    case "triangle": return (shape.base * shape.height) / 2;
  }
  // 如果加了一种新 shape 但忘了加 case,只有运行时才知道
}

解法:TS 6.0 实验性 match 表达式

tsconfig.json 中开启:

{
  "compilerOptions": {
    "experimentalPatternMatching": true
  }
}

然后:

function area(shape: Shape): number {
  return match(shape) {
    when { kind: "circle", radius: let r } => Math.PI * r * r,
    when { kind: "rectangle", width: let w, height: let h } => w * h,
    when { kind: "triangle", base: let b, height: let h } => (b * h) / 2,
    // ✅ TS 自动检查穷尽性:如果 Shape 增加新变体,这里会报错
  };
}

更高级:结合条件守卫

const result = match(apiResponse) {
  when { status: 200, data: let d } => Success(d),
  when { status: 404 } => NotFound(),
  when { status: let s } if s >= 500 => ServerError(s),
  else => UnknownError(response),
};
注意match 目前是实验性特性,基于 TC39 Stage 3 提案,语法可能在标准化前有变动。生产环境建议等到 TS 7.0 稳定后再用。

模式六:TypeScript 6.0 的破坏性变更 —— 升级生存指南

TS 6.0 的默认值发生了地震级变化。如果你直接 npm i typescript@latest 然后跑 tsc,大概率炸一片。

最危险的三个变更

1. strict 默认变为 true
// 升级前你的 tsconfig 可能是这样的
{
  "compilerOptions": {
    "target": "es2020",
    // 没写 strict,之前默认 false
  }
}

// 升级后——突然冒出几百个类型错误
// 解法:显式声明
{
  "compilerOptions": {
    "strict": true,  // 现代项目建议直接开启
    // 或者保守过渡:
    // "strict": false  // 先稳住,再逐步收紧
  }
}
2. types 默认变为 []——最容易踩的坑
// 升级后 Node 项目的 process、Buffer、fs 全部报错
// ❌ Cannot find name 'process'. Did you mean to install '@types/node'?

// 解法:显式声明需要的类型包
{
  "compilerOptions": {
    "types": ["node"]  // 不要写 ["*"],那等于没升级
  }
}
3. 移除 moduleResolution: "classic"
// 所有项目必须迁移到现代方案
{
  "compilerOptions": {
    "moduleResolution": "bundler"  // Vite/Webpack 项目
    // 或
    "moduleResolution": "nodenext"  // Node ESM 项目
  }
}

推荐的升级 tsconfig.json

{
  "compilerOptions": {
    "target": "es2025",
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": true,
    "rootDir": "./src",
    "outDir": "./dist",
    "types": ["node"],
    "verbatimModuleSyntax": true,
    "noImplicitOverride": true
  },
  "include": ["src"]
}

写在最后:TypeScript 7.0 要来了

TypeScript 6.0 最大的历史意义不是新增了多少特性——而是它最后一个用 TypeScript/JavaScript 编写的编译器版本

TypeScript 7.0 的编译器底层将用 Go 语言重写,大型项目编译速度预计提升 10 倍。届时 TypeScript 的角色将从"编译器 + 类型检查器"演变为"纯类型检查器"——编译交给 Go,类型检查交给 TS。

如果你还在用 TS 4.x 甚至 3.x 的思维写类型,现在是时候升级你的武器库了。


参考

ts

1
打赏

—— 评论区 ——

昵称
邮箱
网址
取消
人生倒计时