TypeScript 工程化进阶实战
TypeScript 早已不是可选的加分项,而是中大型前端项目的标配。但很多团队停留在给变量加类型注解的层面,没有发挥类型系统的真正威力。本文从 strict 模式配置讲起,深入泛型、工具类型与类型体操,最后覆盖 tsconfig 工程化配置与常见迁移坑。
一、strict 模式:类型安全的起点
很多团队的 tsconfig 里只开了 "strict": true,但不知道 strict 其实是一组开关的合集。它同时开启了 noImplicitAny、strictNullChecks、strictFunctionTypes 等七项检查。下面是一个推荐的完整配置:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}
noUnusedLocals 和 noUnusedParameters 会让未使用的变量和参数直接报错,这在大型项目里非常有用——它帮你删掉了大量死代码。
// @ts-nocheck 注释老文件,然后逐个模块开启 strict。可以用 allowJs: true 让 TS 渐进式接管 JS 文件。
二、泛型:写出可复用的类型安全代码
泛型是 TypeScript 最强大也最容易让人困惑的特性。它的核心思想是:把类型也当作参数传入函数。下面是一个实际项目中常见的 API 请求封装:
// 不带泛型:返回 any,失去类型安全
function fetchUser(url: string) {
return fetch(url).then(r => r.json());
}
// 带泛型:调用时指定返回类型
async function request<T>(url: string, options?: RequestInit): Promise<T> {
const resp = await fetch(url, options);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json() as Promise<T>;
}
interface User {
id: number;
name: string;
email: string;
}
// 使用时自动获得完整类型提示
const user = await request<User>("/api/user/1");
console.log(user.name); // 有类型提示
console.log(user.age); // 报错:User 上不存在 age
三、工具类型:内置的类型体操
TypeScript 内置了一组工具类型,能帮你快速从已有类型派生出新类型,不需要手写重复代码。
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Partial:所有属性变可选
type UserUpdate = Partial<User>;
// 等价于 { id?: number; name?: string; email?: string; age?: number; }
// Pick:只选部分属性
type UserPreview = Pick<User, "id" | "name">;
// 等价于 { id: number; name: string; }
// Omit:排除部分属性
type UserWithoutId = Omit<User, "id">;
// 等价于 { name: string; email: string; age: number; }
// Readonly:所有属性变只读
type FrozenUser = Readonly<User>;
// Record:构造键值映射类型
type UserMap = Record<number, User>;
// 等价于 { [key: number]: User }
这五个工具类型覆盖了日常开发 80% 的派生类型需求。掌握它们,你就不用再手写大量重复的 interface 了。
四、条件类型与模板字面量
当工具类型不够用时,条件类型让你能写出根据输入类型自动计算输出类型的逻辑。
// 约束 API 返回类型
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; message: string };
function handleResponse<T>(resp: ApiResponse<T>): T | null {
if (resp.status === "success") {
return resp.data;
}
console.error(resp.message);
return null;
}
// 模板字面量类型:根据字符串模板推导类型
type EventName = `on${Capitalize<string>}`;
// 可能是 onClick、onSubmit、onChange 等等
五、与 Vue / React 配合
现代前端框架都对 TypeScript 有很好的原生支持。Vue 3 的 <script setup lang="ts"> 和 React 的函数组件加泛型,让组件的 props 和状态都有完整类型提示。
// React 组件
interface ButtonProps {
variant: "primary" | "danger" | "ghost";
size?: "small" | "medium" | "large";
onClick?: () => void;
children: React.ReactNode;
}
function Button({ variant, size = "medium", onClick, children }: ButtonProps) {
return (
<button className={`btn-${variant} btn-${size}`} onClick={onClick}>
{children}
</button>
);
}
// 使用时:variant 写错会立刻报错
<Button variant="primar">确定</Button>
// 类型错误:"primar" 不是合法的 variant 值
六、常见坑与调试
坑一:滥用 any。看到类型报错就写 any 是最常见的反模式——这等于放弃了 TypeScript 的所有保护。正确做法是用 unknown,在使用前再做类型收窄。
// 不好
function parse(data: any) {
return data.user.name;
}
// 好
function parse(data: unknown) {
if (typeof data === "object" && data !== null && "user" in data) {
const user = (data as { user: { name: string } }).user;
return user.name;
}
throw new Error("数据格式不对");
}
坑二:类型断言用太多。as Type 是在告诉编译器"相信我,这个类型是对的"。用多了就等于在类型系统上打补丁。尽量让类型自动推导,而不是手动断言。
坑三:忽略 tsconfig 里的 skipLibCheck。这个选项跳过对 node_modules 里 .d.ts 文件的类型检查,能大幅加快编译速度。但它也意味着第三方库的类型错误不会被发现,这是合理的权衡。
七、总结
TypeScript 的价值不在于"给 JS 加了类型",而在于它在代码运行之前就帮你发现了大量潜在错误。从 strict 模式起步,善用泛型和工具类型,把类型系统当成你的第一个测试套件。记住三条原则:别用 any、让类型自动推导、把 tsconfig 的严格选项拉满。