关于 typescript-strict-migrator
供团队逐步迁移到 TypeScript strict 模式的技能,涵盖编译器选项、类型安全模式和迁移实践。
主要功能
- 01分阶段启用 strict 模式
- 02编译器选项指导
- 03类型守卫与断言
- 04工具类型模式
- 05迁移检查清单
适用场景
- 01规划渐进式 strict 迁移
- 02安全处理空值与索引访问
- 03统一并记录团队实践
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ patricio0312rev/ skills --skill typescript-strict-migrator安装后在 Claude Code 中输入/typescript-strict-migrator确认已加载。
详细介绍
TypeScript 严格模式迁移器
逐步迁移到 TypeScript 严格模式,以获得最高级别的类型安全性。
核心工作流
- 审查当前状态:检查现有类型错误
- 逐步启用:每次启用一个标志
- 修复错误:针对每个标志系统地处理
- 添加类型守卫:进行运行时类型检查
- 使用工具类型:进行恰当的类型转换
- 记录模式:制定团队指南
严格模式标志
// tsconfig.json - Full strict mode
{
"compilerOptions": {
// Master flag (enables all below)
"strict": true,
// Individual flags (enabled by strict)
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
// Additional strict-ish flags (not in strict)
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true
}
}
渐进式迁移策略
阶段 1:基本严格标志
// tsconfig.json - Phase 1
{
"compilerOptions": {
"strict": false,
"noImplicitAny": true,
"alwaysStrict": true
}
}
// Before: implicit any
function processData(data) {
return data.map(item => item.value);
}
// After: explicit types
function processData(data: DataItem[]): number[] {
return data.map(item => item.value);
}
interface DataItem {
value: number;
label: string;
}
阶段 2:严格空值检查
// tsconfig.json - Phase 2
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true
}
}
// Before: potential null errors
function getUserName(user: User) {
return user.profile.name; // Error if profile is undefined
}
// After: proper null handling
function getUserName(user: User): string | undefined {
return user.profile?.name;
}
// With non-null assertion (use sparingly)
function getUserNameOrThrow(user: User): string {
if (!user.profile?.name) {
throw new Error('User has no name');
}
return user.profile.name;
}
阶段 3:函数类型
// tsconfig.json - Phase 3
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true
}
}
// Before: contravariance issues
type Handler = (event: Event) => void;
const mouseHandler: Handler = (event: MouseEvent) => {
console.log(event.clientX); // Error with strictFunctionTypes
};
// After: proper variance
type Handler<T extends Event = Event> = (event: T) => void;
const mouseHandler: Handler<MouseEvent> = (event) => {
console.log(event.clientX);
};
阶段 4:属性初始化
// tsconfig.json - Phase 4
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}
// Before: uninitialized properties
class UserService {
private apiClient: ApiClient; // Error: not initialized
constructor() {}
}
// After: definite assignment
class UserService {
private apiClient: ApiClient;
constructor(apiClient: ApiClient) {
this.apiClient = apiClient;
}
}
// Or with definite assignment assertion
class UserService {
private apiClient!: ApiClient; // Initialized in init()
async init() {
this.apiClient = await createApiClient();
}
}
类型守卫
基本类型守卫
// Type guard functions
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isNumber(value: unknown): value is number {
return typeof value === 'number';
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function isArray<T>(value: unknown, itemGuard: (item: unknown) => item is T): value is T[] {
return Array.isArray(value) && value.every(itemGuard);
}
// Usage
function processInput(input: unknown) {
if (isString(input)) {
return input.toUpperCase(); // input is string
}
if (isNumber(input)) {
return input.toFixed(2); // input is number
}
throw new Error('Invalid input type');
}
对象类型守卫
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
interface ApiResponse<T> {
data: T;
success: boolean;
}
// Type guard for User
function isUser(value: unknown): value is User {
return (
isObject(value) &&
typeof value.id === 'string' &&
typeof value.name === 'string' &&
typeof value.email === 'string' &&
(value.role === 'admin' || value.role === 'user')
);
}
// Type guard for API response
function isApiResponse<T>(
value: unknown,
dataGuard: (data: unknown) => data is T
): value is ApiResponse<T> {
return (
isObject(value) &&
typeof value.success === 'boolean' &&
'data' in value &&
dataGuard(value.data)
);
}
// Usage
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
if (!isApiResponse(data, isUser)) {
throw new Error('Invalid API response');
}
return data.data;
}
可辨识联合类型
// Discriminated union pattern
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function createSuccess<T>(data: T): Result<T> {
return { success: true, data };
}
function createError<E = Error>(error: E): Result<never, E> {
return { success: false, error };
}
// Type guard via discriminant
function isSuccess<T, E>(result: Result<T, E>): result is { success: true; data: T } {
return result.success === true;
}
// Usage
async function processRequest(): Promise<Result<User>> {
try {
const user = await fetchUser('123');
return createSuccess(user);
} catch (error) {
return createError(error instanceof Error ? error : new Error(String(error)));
}
}
const result = await processRequest();
if (isSuccess(result)) {
console.log(result.data.name); // TypeScript knows data exists
} else {
console.error(result.error.message); // TypeScript knows error exists
}
用于迁移的工具类型
// Making properties required
type RequiredUser = Required<User>;
// Making properties optional
type PartialUser = Partial<User>;
// Pick specific properties
type UserCredentials = Pick<User, 'email' | 'id'>;
// Omit specific properties
type PublicUser = Omit<User, 'password' | 'internalId'>;
// Make properties readonly
type ReadonlyUser = Readonly<User>;
// Deep readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
// NonNullable
type DefiniteString = NonNullable<string | null | undefined>; // string
// Extract and Exclude
type AdminRole = Extract<User['role'], 'admin'>; // 'admin'
type NonAdminRole = Exclude<User['role'], 'admin'>; // 'user'
// Record type
type UserById = Record<string, User>;
// Parameters and ReturnType
type FetchParams = Parameters<typeof fetch>; // [input: RequestInfo, init?: RequestInit]
type FetchReturn = ReturnType<typeof fetch>; // Promise<Response>
常见迁移模式
处理可选链
// Before: unsafe access
const userName = user.profile.settings.displayName;
// After: safe access with optional chaining
const userName = user?.profile?.settings?.displayName;
// With nullish coalescing
const userName = user?.profile?.settings?.displayName ?? 'Anonymous';
// With type narrowing
function getDisplayName(user: User | null): string {
if (!user?.profile?.settings?.displayName) {
return 'Anonymous';
}
return user.profile.settings.displayName;
}
断言函数
// Assertion function
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
if (value === undefined || value === null) {
throw new Error('Value is not defined');
}
}
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new Error('Value is not a User');
}
}
// Usage
function processUser(maybeUser: unknown) {
assertIsUser(maybeUser);
// maybeUser is now User
console.log(maybeUser.name);
}
错误处理
// Before: any in catch
try {
await riskyOperation();
} catch (error) {
console.error(error.message); // Error with useUnknownInCatchVariables
}
// After: proper error handling
try {
await riskyOperation();
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error('Unknown error:', String(error));
}
}
// Helper function
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === 'string') return error;
return 'Unknown error occurred';
}
索引签名
// Before: unsafe index access
const users: Record<string, User> = {};
const user = users['unknown-id'];
console.log(user.name); // Error with noUncheckedIndexedAccess
// After: proper null check
const user = users['unknown-id'];
if (user) {
console.log(user.name);
}
// Or with assertion
const user = users['known-id']!; // Only if you're certain
// Better: use Map
const usersMap = new Map<string, User>();
const user = usersMap.get('some-id'); // User | undefined by design
迁移脚本
// scripts/analyze-strict.ts
import * as ts from 'typescript';
import * as path from 'path';
interface StrictAnalysis {
noImplicitAny: number;
strictNullChecks: number;
strictFunctionTypes: number;
strictPropertyInitialization: number;
total: number;
}
function analyzeProject(configPath: string): StrictAnalysis {
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
const parsedConfig = ts.parseJsonConfigFileContent(
configFile.config,
ts.sys,
path.dirname(configPath)
);
// Enable strict flags one by one
const strictOptions = {
noImplicitAny: true,
strictNullChecks: true,
strictFunctionTypes: true,
strictPropertyInitialization: true,
};
const analysis: StrictAnalysis = {
noImplicitAny: 0,
strictNullChecks: 0,
strictFunctionTypes: 0,
strictPropertyInitialization: 0,
total: 0,
};
for (const [flag, _] of Object.entries(strictOptions)) {
const options = {
...parsedConfig.options,
[flag]: true,
};
const program = ts.createProgram(parsedConfig.fileNames, options);
const diagnostics = ts.getPreEmitDiagnostics(program);
analysis[flag as keyof StrictAnalysis] = diagnostics.length;
analysis.total += diagnostics.length;
}
return analysis;
}
// Usage
const analysis = analyzeProject('./tsconfig.json');
console.log('Strict mode analysis:', analysis);
最佳实践
- 渐进采用:每次启用一个标志
- 从 noImplicitAny 开始:最容易修复
- 添加类型守卫:确保运行时安全
- 使用断言函数:尽早失败
- 避免非空断言:谨慎使用
- 记录模式:确保团队一致性
- 在 CI 中强制检查:防止回归
- 优先使用 unknown 而非 any:获得更好的类型安全性
输出检查清单
每次严格模式迁移都应包含:
- 每个标志的基线错误数量
- 分阶段的迁移计划
- 类型守卫工具
- 断言函数
- 错误处理模式
- 索引访问处理
- 可选链用法
- 更新后的 tsconfig.json
- 团队文档
- CI 严格检查
评论
支持 Markdown