原文地址:https://rahuulmiishra.medium.com/6-typescript-tips-to-write-safer-cleaner-code-24c23c470107
原文作者: Arthur
那些区分“能编译通过”和“运行时也能正常工作”的关键模式。别再到处用 any 了,开始写真正能保护你的 TypeScript 代码吧。
TypeScript 的类型系统强大得令人发指——但大多数开发者只用到了皮毛。他们在任何报错的地方都甩一个 any,用 as 让编译器闭嘴,然后困惑为什么运行时还是会出错。
以下 6 个模式能立刻让你的 TypeScript 代码更安全、更易读,并且在 bug 进入生产环境之前就真正捕获它们。
1. 用 unknown 代替 any
当你把某个值类型标注为 any 时,你实际上是在告诉 TypeScript:“我不在乎,别检查任何东西。”这完全违背了使用 TypeScript 的初衷。
unknown 是 any 的类型安全版本。你可以把任何值赋给它——和 any 一样——但 TypeScript 不会允许你对它做任何操作,直到你先收窄(narrow)了类型。
any 的问题:使用 any 会静默禁用该变量上的所有类型检查。而且它会传染——如果你把一个 any 类型的值传入函数,返回值类型通常也会变成 any。它就像病毒一样扩散。
// 反面示例:any 让一切静默通过
let data: any = fetchSomething();
data.toFixed(2); // 没有报错……但如果 data 是字符串呢?
data.toUpperCase(); // 没有报错……但如果 data 是数字呢?
console.log(data.foo.bar.baz); // 没有报错……运行时直接炸了 💥
// 正确示例:unknown 强制你先检查类型
let data: unknown = fetchSomething();
// data.toFixed(2); ← 报错:Object is of type 'unknown'
if (typeof data === "string") {
console.log(data.toUpperCase()); // ✅ 安全 — TypeScript 知道它是 string
}
if (typeof data === "number") {
console.log(data.toFixed(2)); // ✅ 安全 — TypeScript 知道它是 number
}
何时使用 unknown
- API 响应——你永远不知道服务端实际返回了什么
- 用户输入——表单值、URL 参数、
JSON.parse() 的结果
- 第三方库——当类型定义缺失或不可靠时
- 错误处理——在 TS 4.4+ 中,
catch(e) 默认就是 unknown 类型
小贴士:在 tsconfig 中启用 noImplicitAny。这个编译器选项会强制你显式标注类型,而不是静默地默认为 any。配合 unknown 使用,可以捕获一大类 bug。
2. 优先使用 satisfies 而非 as
as 关键字是一个类型断言(Type Assertion)——你在告诉 TypeScript“相信我,我比你懂”。问题是?TypeScript 真的会相信你,即使你是错的。
satisfies(在 TypeScript 4.9 中引入)能够验证一个值是否符合某个类型,同时不会对类型进行拓宽(widening)。你既能获得类型检查,又能保留收窄后的推断类型。
type Color = "red" | "green" | "blue";
type Theme = {
primary: Color;
secondary: Color;
surface: string;
};
as 与 satisfies 的对比
// 反面示例:`as` 让这段代码通过了——"grn" 根本不是有效的 Color!
const broken = {
primary: "red",
secondary: "grn", // ← 拼写错误,但没有报错
surface: "#fff"
} as Theme;
// 正确示例:`satisfies` 立刻捕获拼写错误
const safe = {
primary: "red",
secondary: "grn", // ← 报错:'"grn"' is not assignable to type 'Color'
surface: "#fff"
} satisfies Theme;
杀手级特性:保留字面量类型
const theme = {
primary: "red",
secondary: "blue",
surface: "#f0f0f0"
} satisfies Theme;
// theme.primary 的类型是 "red"(字面量类型),而不是 Color(联合类型)
// 这意味着你在下游代码中能获得更好的自动补全和更精确的类型检查
何时可以用 as:当你在操作 DOM API 时(如 document.getElementById('x') as HTMLInputElement),或者当 TypeScript 确实无法推断类型而你比它知道得更多时,可以使用 as。但首选应该是 satisfies。
3. 用 is 编写自定义类型守卫(Type Guard)
typeof 和 instanceof 的能力是有限的。当你需要区分自定义类型或接口时,TypeScript 的 is 关键字允许你构建自己的类型收窄函数。
返回类型 pet is Cat 告诉 TypeScript:“如果这个函数返回 true,那么参数一定是 Cat 类型。”在 if 代码块内部,你就能获得完整的 Cat 方法和属性。
type Species = "cat" | "dog";
interface Pet {
species: Species;
name: string;
}
class Cat implements Pet {
species: Species = "cat";
name: string;
constructor(name: string) {
this.name = name;
}
purr(): void {
console.log(`${this.name} purrs softly...`);
}
}
class Dog implements Pet {
species: Species = "dog";
name: string;
constructor(name: string) {
this.name = name;
}
fetch(): void {
console.log(`${this.name} fetches the ball!`);
}
}
// 类型守卫函数
function isCat(pet: Pet): pet is Cat {
return pet.species === "cat";
}
function isDog(pet: Pet): pet is Dog {
return pet.species === "dog";
}
// 使用示例 — TypeScript 在 if 块内自动收窄类型
function interact(pet: Pet) {
if (isCat(pet)) {
pet.purr(); // ✅ TypeScript 知道 pet 是 Cat
} else if (isDog(pet)) {
pet.fetch(); // ✅ TypeScript 知道 pet 是 Dog
}
}
实际应用场景
- API 响应校验——在使用响应数据前,先检查它是否符合预期结构
- 可辨识联合类型(Discriminated Union)——根据判别属性收窄联合类型
- 事件处理——将 DOM 事件目标收窄为特定元素类型
- 错误类型——区分
ApiError、NetworkError、ValidationError
// 实际例子:API 响应校验
interface ApiSuccess { status: "ok"; data: unknown }
interface ApiError { status: "error"; message: string }
type ApiResponse = ApiSuccess | ApiError;
function isSuccess(res: ApiResponse): res is ApiSuccess {
return res.status === "ok";
}
const response = await fetchData();
if (isSuccess(response)) {
console.log(response.data); // ✅ 收窄为 ApiSuccess
} else {
console.error(response.message); // ✅ 收窄为 ApiError
}
类型守卫 + filter 技巧:类型守卫与数组方法完美配合:const cats = pets.filter(isCat); —— TypeScript 会自动推断出 Cat[] 类型。
4. 放弃 Enum,使用联合类型(Union Type)
TypeScript 的枚举(enum)看起来很友好,但它们附带不少包袱:会生成额外的 JavaScript 代码,数值枚举容易出错,而且对 tree-shaking 不友好。
简单的字符串联合类型就能提供同等的安全性、更好的可读性,以及零运行时开销。
// 反面示例:数值枚举 — 容易出问题
enum Status {
Pending, // 0
Active, // 1
Suspended, // 2
}
// 这段代码编译不报错 — 99 根本不是有效的 Status!
const s: Status = 99; // ← 没有报错。静默 bug。
// 正确示例:联合类型 — 零运行时开销,完全类型安全
type Status = "pending" | "active" | "suspended";
function setStatus(status: Status) {
console.log(`Setting status: ${status}`);
}
setStatus("active"); // ✅ 正常
setStatus("pending"); // ✅ 正常
setStatus("invalid"); // ❌ 报错:Argument of type '"invalid"' is not assignable
const enum 呢? const enum 会在编译时内联值(不产生运行时对象),比普通 enum 好一些,但它在 --isolatedModules 模式下存在一些怪癖(而 Vite、Next.js 等都依赖该模式)。联合类型在简洁性和兼容性方面仍然胜出。
当你需要类似 enum 的对象时
如果你需要一个运行时对象(用于遍历或反向查找),可以使用 as const:
const STATUS = {
Pending: "pending",
Active: "active",
Suspended: "suspended",
} as const;
type Status = (typeof STATUS)[keyof typeof STATUS];
// type Status = "pending" | "active" | "suspended"
// 运行时遍历正常工作:
Object.values(STATUS).forEach(s => console.log(s));
5. 用 Record 正确地标注对象类型
别再用 Object 或 {} 作为类型了。Object 接受任何东西(包括原始类型),而 {} 意味着“任何非空值”。这两者都不是你真正想要的。
// 反面示例:Object 接受 Date、RegExp,什么都行
const config: Object = new Date(); // 没有报错 ⚠️
const also: Object = 42; // 没有报错 ⚠️
// 反面示例:{} 表示“任何非 null、非 undefined 的值”
const what: {} = "hello"; // 没有报错 ⚠️
// 正确示例:Record<string, unknown> — 明确的键值契约
type Config = Record<string, unknown>;
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000,
debug: true,
};
// 等效的索引签名写法:
type Config2 = { [key: string]: unknown };
为已知键使用类型化的 Record
Record 在约束键的场景下真正大放异彩:
type Role = "admin" | "editor" | "viewer";
type Permissions = Record<Role, string[]>;
const perms: Permissions = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
// superadmin: ["all"] ← 报错:不在 Role 中
};
// 少了某个键?报错!
// const bad: Permissions = { admin: ["read"] };
// ← 报错:Missing properties 'editor' and 'viewer'
用 Partial<Record<K, V>> 处理可选键:如果并非每个键都需要值,可以用 Partial 包裹。例如:Partial<Record<Role, string[]>> 会让所有角色键变为可选。
6. 用模板字面量类型(Template Literal Type)生成类型组合
TypeScript 的模板字面量类型允许你动态构造字符串类型。将两个联合类型组合在一起,TypeScript 会自动生成所有可能的组合。
type Size = "sm" | "md" | "lg";
type Color = "primary" | "secondary" | "danger";
type ButtonVariant = `${Size}-${Color}`;
// = "sm-primary" | "sm-secondary" | "sm-danger"
// | "md-primary" | "md-secondary" | "md-danger"
// | "lg-primary" | "lg-secondary" | "lg-danger"
const btn: ButtonVariant = "md-primary"; // ✅
const bad: ButtonVariant = "xl-primary"; // ❌ 报错
实际应用示例
// CSS 工具类
type Spacing = 0 | 1 | 2 | 4 | 8;
type Direction = "t" | "r" | "b" | "l" | "x" | "y";
type MarginClass = `m${Direction}-${Spacing}`;
// "mt-0" | "mt-1" | "mt-2" | ... | "my-8" (30 种组合)
// 事件处理函数名
type DomEvent = "click" | "focus" | "blur" | "change";
type HandlerName = `on${Capitalize<DomEvent>}`;
// "onClick" | "onFocus" | "onBlur" | "onChange"
// API 路由路径
type Resource = "users" | "posts" | "comments";
type Method = "get" | "create" | "update" | "delete";
type ApiAction = `${Method}_${Resource}`;
// "get_users" | "create_users" | ... | "delete_comments"
内置字符串操作类型:TypeScript 提供了 Capitalize、Uncapitalize、Uppercase 和 Lowercase 等工具类型,可在模板字面量中使用。将它们组合起来可以实现强大的模式,比如 on${Capitalize<EventName>}。
速查表
| 技巧 |
避免使用 |
推荐使用 |
| 未知类型 |
any |
unknown |
| 类型验证 |
as |
satisfies |
| 类型收窄 |
手动判断 |
自定义类型守卫(is) |
| 常量集合 |
enum |
联合类型 / as const |
| 对象类型 |
Object / {} |
Record<K, V> |
| 字符串组合 |
手动罗列 |
模板字面量类型 |
好的 TypeScript 代码不在于写更多的类型,而在于写对的类型。本文的每个模式都只做一件事:让编译器替你抓 bug,这样你的用户就不用撞上它们了。想了解更多关于前端框架和工程化的实用技巧,欢迎来 云栈社区 交流探讨。