本文面向已有 TS 基础、每天在生产环境写类型的开发者。不讲interface和type的区别,只讲那些能让你在 Code Review 时被追问"这是怎么做到的"的硬核玩法。
上个月团队里一个实习生指着我的类型代码问:"这个 brand 是什么新关键字?"
我说是 TypeScript 6.0 的原生名义类型。他愣了一下:"TypeScript 6.0 出了?"
是的,2026 年 3 月 23 日 TypeScript 6.0 正式发布了——而很多人还在用 5.3 的思维写类型。
这篇不是版本发布说明的搬运工,而是从真实工程痛点出发,串联 TS 5.x 到 6.0 最值得掌握的 6 个实战模式。
看看你项目里有没有这种代码:
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,删错文件了这就是结构类型系统的诅咒——只要长得一样,编译器就认为一样。
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">;// 这个 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()
// 这才是真正的"我全都要"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 可以批量管理资源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,只有运行时才知道
}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 稳定后再用。TS 6.0 的默认值发生了地震级变化。如果你直接 npm i typescript@latest 然后跑 tsc,大概率炸一片。
strict 默认变为 true// 升级前你的 tsconfig 可能是这样的
{
"compilerOptions": {
"target": "es2020",
// 没写 strict,之前默认 false
}
}
// 升级后——突然冒出几百个类型错误
// 解法:显式声明
{
"compilerOptions": {
"strict": true, // 现代项目建议直接开启
// 或者保守过渡:
// "strict": false // 先稳住,再逐步收紧
}
}types 默认变为 []——最容易踩的坑// 升级后 Node 项目的 process、Buffer、fs 全部报错
// ❌ Cannot find name 'process'. Did you mean to install '@types/node'?
// 解法:显式声明需要的类型包
{
"compilerOptions": {
"types": ["node"] // 不要写 ["*"],那等于没升级
}
}moduleResolution: "classic"// 所有项目必须迁移到现代方案
{
"compilerOptions": {
"moduleResolution": "bundler" // Vite/Webpack 项目
// 或
"moduleResolution": "nodenext" // Node ESM 项目
}
}{
"compilerOptions": {
"target": "es2025",
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"rootDir": "./src",
"outDir": "./dist",
"types": ["node"],
"verbatimModuleSyntax": true,
"noImplicitOverride": true
},
"include": ["src"]
}TypeScript 6.0 最大的历史意义不是新增了多少特性——而是它最后一个用 TypeScript/JavaScript 编写的编译器版本。
TypeScript 7.0 的编译器底层将用 Go 语言重写,大型项目编译速度预计提升 10 倍。届时 TypeScript 的角色将从"编译器 + 类型检查器"演变为"纯类型检查器"——编译交给 Go,类型检查交给 TS。
如果你还在用 TS 4.x 甚至 3.x 的思维写类型,现在是时候升级你的武器库了。

—— 评论区 ——