TypeScript 最佳实践:编写更安全、更易维护的代码
掌握最关键的 TypeScript 模式——严格模式、工具类型、泛型、判别联合类型,以及如何避免常见的类型系统漏洞。
TypeScript 在大多数专业 JavaScript 项目中已从可选变为必备。但"技术上算是 TypeScript"和"真正能捕获 Bug 的 TypeScript"之间存在天壤之别。这种差距归结为几个关键模式,正是它们解锁了类型系统的真正威力。
始终启用严格模式
TypeScript 配置中单项收益最高的改动:
// tsconfig.json
{
"compilerOptions": {
"strict": true
}
}
strict 启用了一系列检查:
strictNullChecks—null和undefined不可赋值给其他类型noImplicitAny— 变量不能隐式拥有any类型strictFunctionTypes— 更严格的函数参数检查strictPropertyInitialization— 类属性必须完成初始化
没有 strictNullChecks,TypeScript 会漏掉最常见的一类运行时错误。从第一天起就开启它——事后在大型代码库中补开会非常痛苦。
避免 any——改用 unknown
any 是类型系统的逃生出口,它会屏蔽所有错误,让 TypeScript 失去意义:
// ❌ any 会禁用所有类型检查
function processData(data: any) {
data.nonExistentMethod(); // 没有报错!运行时崩溃。
}
// ✅ unknown 强制你在使用前先收窄类型
function processData(data: unknown) {
if (typeof data === "string") {
console.log(data.toUpperCase()); // 安全——已收窄为 string
}
if (data instanceof Error) {
console.log(data.message); // 安全——已收窄为 Error
}
}
unknown 是 any 的类型安全版本。它强制你在使用之前先检查值的类型。对于 API 响应、JSON 解析以及任何来自外部来源的数据,都应使用它。
用判别联合类型实现穷举处理
TypeScript 最强大的模式之一:
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: User[] };
type ErrorState = { status: "error"; message: string };
type ApiState = LoadingState | SuccessState | ErrorState;
function render(state: ApiState) {
switch (state.status) {
case "loading":
return <Spinner />;
case "success":
return <UserList users={state.data} />; // 此处 state.data 可用
case "error":
return <ErrorMessage msg={state.message} />; // 此处 state.message 可用
default:
// 如果新增状态但未处理,TypeScript 会在此处报错
const _exhaustive: never = state;
throw new Error(`Unhandled state: ${_exhaustive}`);
}
}
末尾的 never 赋值是穷举性检查——如果你向 ApiState 新增了一个变体但没有在 switch 中处理它,TypeScript 会给出编译时错误。这是整个语言中最有价值的安全保障之一。
工具类型
TypeScript 内置了多种类型转换工具:
interface User {
id: number;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
createdAt: Date;
}
// 将所有属性变为可选
type PartialUser = Partial<User>;
// 将所有属性变为必填
type RequiredUser = Required<User>;
// 将所有属性变为只读
type ReadonlyUser = Readonly<User>;
// 挑选特定属性
type UserPreview = Pick<User, "id" | "name">;
// 排除特定属性
type UserWithoutDates = Omit<User, "createdAt">;
// 从对象的值创建类型
type UserRole = User["role"]; // "admin" | "editor" | "viewer"
// 将属性变为可空
type NullableUser = { [K in keyof User]: User[K] | null };
// Record 类型:具有特定键和值类型的对象
type RolePermissions = Record<UserRole, string[]>;
// 从联合类型中提取/排除
type AdminOrEditor = Extract<UserRole, "admin" | "editor">; // "admin" | "editor"
type NonAdmin = Exclude<UserRole, "admin">; // "editor" | "viewer"
用泛型编写可复用的类型安全代码
泛型让你能编写适用于任意类型、同时保持完全类型安全的函数和类:
// 不使用泛型——丢失类型信息
function first(arr: any[]): any {
return arr[0];
}
// 使用泛型——保留类型
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // 类型:number | undefined
const str = first(["a", "b"]); // 类型:string | undefined
约束泛型
// T 必须拥有 'id' 属性
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
// K 必须是 T 的键
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", role: "admin" };
const name = getProperty(user, "name"); // 类型:string
const role = getProperty(user, "role"); // 类型:string
// getProperty(user, "missing"); // ❌ TypeScript 报错——"missing" 不是有效的键
类型守卫与类型收窄
// 类型谓词——自定义类型守卫
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
"email" in value
);
}
// 用法
const data: unknown = await fetchUser();
if (isUser(data)) {
console.log(data.name); // TypeScript 在此处知道 data 是 User 类型
}
对于运行时的 API 响应校验,建议将 TypeScript 接口与 Zod 的运行时 schema 验证配合使用。使用我们的 JSON to Zod 转换工具直接从 JSON 生成 Zod schema——粘贴一个示例 JSON 响应,即可立即获得类型安全的 schema。
模板字面量类型
TypeScript 4.1+ 支持在类型层面进行字符串操作:
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus" | "onBlur"
type CSSProperty = "margin" | "padding";
type CSSDirection = "top" | "right" | "bottom" | "left";
type CSSKey = `${CSSProperty}-${CSSDirection}`;
// "margin-top" | "margin-right" | ... | "padding-left"
satisfies 运算符(TypeScript 4.9+)
在不拓宽类型的前提下,验证某个值是否符合指定类型:
type ColorMap = Record<string, string>;
// ❌ 不使用 satisfies——类型被拓宽为 Record<string, string>
const colors: ColorMap = {
primary: "#3b82f6",
secondary: "#8b5cf6",
};
colors.primary.toUpperCase(); // 可以运行,但丢失了 string 字面量类型
// ✅ 使用 satisfies——既验证类型,又保留字面量类型
const colors = {
primary: "#3b82f6",
secondary: "#8b5cf6",
} satisfies ColorMap;
colors.primary.toUpperCase(); // ✅ TypeScript 知道它是 string 类型
colors.nonexistent; // ❌ 报错——该键不存在
从数据生成类型
无需手动根据 API 响应编写 TypeScript 接口:
- 将示例 JSON 响应粘贴到我们的 JSON to TypeScript 转换工具,即可立即获得接口定义
- 或使用 JSON to Zod 生成 Zod schema——Zod 通过
z.infer<typeof schema>自动推断 TypeScript 类型
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
role: z.enum(["admin", "editor", "viewer"]),
});
// 类型自动推断——无需重复定义
type User = z.infer<typeof UserSchema>;
// 在运行时验证的同时提供 TypeScript 类型
const user = UserSchema.parse(await response.json());
应避免的常见反模式
| 反模式 | 问题 | 解决方案 |
|---|---|---|
as any |
禁用类型检查 | 使用 unknown + 类型收窄 |
// @ts-ignore |
屏蔽错误而非修复 | 修复类型错误 |
as SomeType(不安全强转) |
绕过类型推断 | 使用类型守卫 |
用 interface 定义联合类型 |
不适用于判别联合类型 | 使用 type |
过于宽泛的 object |
无结构信息 | 定义具体的接口 |
用 Function 作为类型 |
无参数/返回值信息 | 使用具体的函数签名 |
值得了解的 TypeScript 配置项
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true, // array[n] 返回 T | undefined
"exactOptionalPropertyTypes": true, // 区分属性缺失与值为 undefined
"noImplicitReturns": true, // 所有代码路径必须有返回值
"noFallthroughCasesInSwitch": true, // 禁止 switch 中意外的 fallthrough
"verbatimModuleSyntax": true // 纯类型导入必须使用 import type
}
}
TypeScript 的价值与你使用它的严格程度成正比。本文介绍的这些模式——严格模式、判别联合类型、泛型、穷举性检查——正是让 TypeScript 真正发现 Bug,而不仅仅是增加语法负担的关键所在。