TypeScript 更安全的 6 个写法:别再用 any 了
TypeScript 很强大,但大多数开发者只用到了皮毛。遇到报错就甩一个 any,用 as 让编译器闭嘴,然后纳闷为什么运行时还是出错。
下面这 6 个写法,能让你的 TypeScript 代码更安全,在 bug 跑到用户面前之前就抓住它。
1. 用 unknown 代替 any
把某个值标成 any,等于告诉 TypeScript:“别管我,什么都别检查。”这跟不用 TypeScript 有什么区别?
unknown 是 any 的安全版本。你可以把任何值赋给它,但 TypeScript 不允许你对它做任何操作,除非你先做类型判断。
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()); // 安全
}
if (typeof data === "number") {
console.log(data.toFixed(2)); // 安全
}什么时候用 unknown:API 响应、用户输入、URL 参数、JSON.parse() 的结果、catch 里的错误对象。
2. 用 satisfies 代替 as
as 是类型断言,你告诉 TypeScript“相信我,我比你懂”。问题是 TypeScript 真信你,哪怕你是错的。
satisfies 是 TypeScript 4.9 引入的。它验证值是否符合某个类型,同时保留更精确的推断类型。
as 的问题:
type Color = "red" | "green" | "blue";
type Theme = {
primary: Color;
secondary: Color;
surface: string;
};
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;satisfies 还有个好处:保留字面量类型。上面例子中 theme.primary 的类型是 "red",不是 Color 联合类型,后续代码能得到更精确的自动补全。
什么时候可以用 as:操作 DOM 时,比如 document.getElementById('x') as HTMLInputElement。其他情况优先用 satisfies。
3. 用 is 写自定义类型守卫
typeof 和 instanceof 不够用的时候,可以用 is 自己写类型判断函数。
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";
}
function interact(pet: Pet) {
if (isCat(pet)) {
pet.purr(); // TypeScript 知道是 Cat
} else if (isDog(pet)) {
pet.fetch(); // TypeScript 知道是 Dog
}
}实际应用:校验 API 响应、处理可辨识联合类型、区分不同的错误类型。
小技巧:类型守卫和数组的 filter 配合很好用:const cats = pets.filter(isCat),TypeScript 会自动推断出 Cat[] 类型。
4. 用联合类型代替 Enum
TypeScript 的 enum 看起来方便,但有代价:会生成额外的 JavaScript 代码,数值枚举容易出问题,对 tree-shaking 不友好。
enum 的问题:
enum Status {
Pending, // 0
Active, // 1
Suspended, // 2
}
const s: Status = 99; // 没报错,但 99 根本不是有效的 Status用联合类型改正:
type Status = "pending" | "active" | "suspended";
function setStatus(status: Status) {
console.log(`Setting status: ${status}`);
}
setStatus("active"); // 正常
setStatus("invalid"); // 报错如果需要运行时对象来遍历或反向查找,可以用 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 什么都能接,{} 表示“任何非空值”,都不是你想要的。
// 错误示范
const config: Object = new Date(); // 没报错
const what: {} = "hello"; // 没报错
// 正确写法
type Config = Record<string, unknown>;
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000,
debug: true,
};Record 在约束键的场景更好用:
type Role = "admin" | "editor" | "viewer";
type Permissions = Record<Role, string[]>;
const perms: Permissions = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
// 少一个键会报错
};6. 用模板字面量类型组合字符串
TypeScript 的模板字面量类型可以动态构造字符串类型。把两个联合类型放一起,TypeScript 自动生成所有可能的组合。
type Size = "sm" | "md" | "lg";
type Color = "primary" | "secondary" | "danger";
type ButtonVariant = `${Size}-${Color}`;
// 结果是 "sm-primary" | "sm-secondary" | "sm-danger" | "md-primary" | ...
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}`;
// 事件处理函数名
type DomEvent = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<DomEvent>}`;
// 结果是 "onClick" | "onFocus" | "onBlur"TypeScript 提供了 Capitalize、Uncapitalize、Uppercase、Lowercase 这些工具类型,可以在模板字面量里用。
速查表
| 要做什么 | 避免用 | 推荐用 |
|---|---|---|
| 未知类型 | any | unknown |
| 类型验证 | as | satisfies |
| 类型收窄 | 手动判断 | is 类型守卫 |
| 常量集合 | enum | 联合类型或 as const |
| 对象类型 | Object 或 {} | Record<K, V> |
| 字符串组合 | 手动列举 | 模板字面量类型 |
好的 TypeScript 代码不在于写更多类型,而在于写对的类型。每个模式做一件事:让编译器替你抓 bug,这样用户就不用撞上它们了。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!