TypeScript Strict 模式实战:开启后我发现了 50 个潜在 Bug
很多项目为了"方便"关闭了 TypeScript 的 strict 模式。上周我给一个 50,000 行的项目开启了 strict,结果发现了 50 个潜在 Bug。
为什么要开启 Strict
一个真实案例
// 关闭 strict 时的代码
function getUser(id: string) {
const user = db.findUser(id);
return user.name; // 看起来没问题
}
// 运行时错误:Cannot read property 'name' of undefined// 开启 strict 后
function getUser(id: string) {
const user = db.findUser(id);
return user.name; // ❌ Object is possibly 'undefined'
}
// 修复后
function getUser(id: string) {
const user = db.findUser(id);
if (!user) {
throw new Error('User not found');
}
return user.name; // ✅ 安全
}Strict 模式包含什么
{
"compilerOptions": {
"strict": true
}
}strict: true 实际上开启了以下所有选项:
| 选项 | 作用 |
|------|------|
| strictNullChecks | 检查 null/undefined |
| strictFunctionTypes | 检查函数参数类型 |
| strictBindCallApply | 检查 bind/call/apply |
| strictPropertyInitialization | 检查类属性初始化 |
| noImplicitAny | 禁止隐式 any |
| noImplicitThis | 禁止隐式 this |
| alwaysStrict | 强制使用严格模式 |
常见问题和解决方案
问题 1:Object is possibly 'undefined'
// ❌ 错误
const users = [1, 2, 3];
const first = users.find(x => x > 0);
console.log(first.toFixed()); // Object is possibly 'undefined'
// ✅ 解决方案 1:可选链
console.log(first?.toFixed());
// ✅ 解决方案 2:类型守卫
if (first !== undefined) {
console.log(first.toFixed());
}
// ✅ 解决方案 3:非空断言(慎用)
console.log(first!.toFixed());问题 2:Property does not exist on type
// ❌ 错误
interface User {
name: string;
}
function printUser(user: User) {
console.log(user.age); // Property 'age' does not exist
}
// ✅ 解决方案 1:扩展接口
interface User {
name: string;
age?: number;
}
// ✅ 解决方案 2:类型守卫
function hasAge(user: User): user is User & { age: number } {
return 'age' in user;
}问题 3:Parameter implicitly has 'any' type
// ❌ 错误
function process(data) { // Parameter 'data' implicitly has 'any' type
return data.toString();
}
// ✅ 解决方案:添加类型
function process(data: unknown) {
if (typeof data === 'string') {
return data;
}
return String(data);
}问题 4:Type 'X' is not assignable to type 'Y'
// ❌ 错误
type Status = 'active' | 'inactive';
const status: Status = 'pending'; // Type '"pending"' is not assignable
// ✅ 解决方案 1:使用正确的值
const status: Status = 'active';
// ✅ 解决方案 2:扩展类型
type Status = 'active' | 'inactive' | 'pending';问题 5:Element implicitly has an 'any' type
// ❌ 错误
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
const key = 'apiUrl';
const value = config[key]; // Element implicitly has 'any' type
// ✅ 解决方案 1:类型断言
const value = config[key as keyof typeof config];
// ✅ 解决方案 2:Record 类型
const config: Record<string, string | number> = {
apiUrl: 'https://api.example.com',
timeout: 5000
};渐进式开启 Strict
如果项目太大,一次性修复所有错误不现实,可以渐进式开启:
第一步:开启 noImplicitAny
{
"compilerOptions": {
"noImplicitAny": true
}
}修复所有隐式 any 问题。
第二步:开启 strictNullChecks
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true
}
}修复所有 null/undefined 问题。
第三步:开启完整 strict
{
"compilerOptions": {
"strict": true
}
}实用工具类型
NonNullable
type User = { name: string; age?: number };
type RequiredAge = NonNullable<User['age']>; // numberPartial / Required
interface User {
name: string;
age: number;
email: string;
}
type PartialUser = Partial<User>; // 所有属性可选
type RequiredUser = Required<User>; // 所有属性必填Pick / Omit
type UserPreview = Pick<User, 'name' | 'email'>;
type UserWithoutEmail = Omit<User, 'email'>;Record
type UserMap = Record<string, User>;
type StatusCount = Record<'active' | 'inactive', number>;最佳实践
1. 优先使用 unknown 而不是 any
// ❌ 不好
function parse(json: string): any {
return JSON.parse(json);
}
// ✅ 好
function parse(json: string): unknown {
return JSON.parse(json);
}
// 使用时需要类型守卫
const data = parse('{"name": "John"}');
if (isUser(data)) {
console.log(data.name);
}2. 使用类型守卫
interface User {
type: 'user';
name: string;
}
interface Admin {
type: 'admin';
permissions: string[];
}
type Person = User | Admin;
function isAdmin(person: Person): person is Admin {
return person.type === 'admin';
}
function process(person: Person) {
if (isAdmin(person)) {
console.log(person.permissions); // ✅ 类型收窄
}
}3. 使用 const 断言
// ❌ 类型是 string[]
const colors = ['red', 'green', 'blue'];
// ✅ 类型是 readonly ['red', 'green', 'blue']
const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number]; // 'red' | 'green' | 'blue'4. 使用 satisfies 操作符
// ❌ 丢失了具体类型信息
const config: Record<string, string | number> = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
config.apiUrl.toUpperCase(); // ❌ 可能是 number
// ✅ 保留具体类型信息
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
} satisfies Record<string, string | number>;
config.apiUrl.toUpperCase(); // ✅ 确定是 string迁移检查清单
- [ ] 开启 `strict: true`
- [ ] 修复所有 `Object is possibly 'undefined'` 错误
- [ ] 修复所有 `Property does not exist` 错误
- [ ] 修复所有 `implicitly has 'any' type` 错误
- [ ] 添加缺失的类型定义
- [ ] 移除所有 `as any` 和 `@ts-ignore`
- [ ] 运行测试确保功能正常
总结
开启 TypeScript strict 模式可以:
1. 提前发现 Bug:编译时而不是运行时
2. 改善代码质量:强制思考边界情况
3. 提升开发体验:更好的类型提示
建议:新项目从一开始就开启 strict,老项目渐进式迁移。
项目规模:50,000 行 TypeScript
发现 Bug:50 个
迁移时间:2 周
#TypeScript #StrictMode #类型安全 #最佳实践
读者评论 3