typescript-strict-migrator

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TypeScript Strict Migrator

TypeScript 严格模式迁移指南

Incrementally migrate to TypeScript strict mode for maximum type safety.
逐步迁移至TypeScript严格模式,以实现最高级别的类型安全。

Core Workflow

核心工作流程

  1. Audit current state: Check existing type errors
  2. Enable incrementally: One flag at a time
  3. Fix errors: Systematic approach per flag
  4. Add type guards: Runtime type checking
  5. Use utility types: Proper type transformations
  6. Document patterns: Team guidelines
  1. 审计当前状态:检查现有类型错误
  2. 逐步启用:每次启用一个标志
  3. 修复错误:针对每个标志采用系统化方案
  4. 添加类型守卫:运行时类型检查
  5. 使用工具类型:正确的类型转换
  6. 记录模式:团队指南

Strict Mode Flags

严格模式标志

json
// 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
  }
}
json
// 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
  }
}

Incremental Migration Strategy

渐进式迁移策略

Phase 1: Basic Strict Flags

阶段1:基础严格模式标志

json
// tsconfig.json - Phase 1
{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": true,
    "alwaysStrict": true
  }
}
typescript
// 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;
}
json
// tsconfig.json - Phase 1
{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": true,
    "alwaysStrict": true
  }
}
typescript
// 之前:隐式any类型
function processData(data) {
  return data.map(item => item.value);
}

// 之后:显式类型
function processData(data: DataItem[]): number[] {
  return data.map(item => item.value);
}

interface DataItem {
  value: number;
  label: string;
}

Phase 2: Strict Null Checks

阶段2:严格空值检查

json
// tsconfig.json - Phase 2
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}
typescript
// 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;
}
json
// tsconfig.json - Phase 2
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}
typescript
// 之前:潜在空值错误
function getUserName(user: User) {
  return user.profile.name;  // 若profile为undefined则报错
}

// 之后:正确的空值处理
function getUserName(user: User): string | undefined {
  return user.profile?.name;
}

// 使用非空断言(谨慎使用)
function getUserNameOrThrow(user: User): string {
  if (!user.profile?.name) {
    throw new Error('用户未设置名称');
  }
  return user.profile.name;
}

Phase 3: Function Types

阶段3:函数类型

json
// tsconfig.json - Phase 3
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true
  }
}
typescript
// 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);
};
json
// tsconfig.json - Phase 3
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true
  }
}
typescript
// 之前:逆变问题
type Handler = (event: Event) => void;
const mouseHandler: Handler = (event: MouseEvent) => {
  console.log(event.clientX);  // 启用strictFunctionTypes后会报错
};

// 之后:正确的协变处理
type Handler<T extends Event = Event> = (event: T) => void;
const mouseHandler: Handler<MouseEvent> = (event) => {
  console.log(event.clientX);
};

Phase 4: Property Initialization

阶段4:属性初始化

json
// tsconfig.json - Phase 4
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}
typescript
// 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();
  }
}
json
// tsconfig.json - Phase 4
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}
typescript
// 之前:未初始化的属性
class UserService {
  private apiClient: ApiClient;  // 错误:未初始化

  constructor() {}
}

// 之后:明确赋值
class UserService {
  private apiClient: ApiClient;

  constructor(apiClient: ApiClient) {
    this.apiClient = apiClient;
  }
}

// 或使用明确赋值断言
class UserService {
  private apiClient!: ApiClient;  // 在init()中初始化

  async init() {
    this.apiClient = await createApiClient();
  }
}

Type Guards

类型守卫

Basic Type Guards

基础类型守卫

typescript
// 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');
}
typescript
// 类型守卫函数
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);
}

// 使用示例
function processInput(input: unknown) {
  if (isString(input)) {
    return input.toUpperCase();  // input被推断为string类型
  }
  if (isNumber(input)) {
    return input.toFixed(2);  // input被推断为number类型
  }
  throw new Error('输入类型无效');
}

Object Type Guards

对象类型守卫

typescript
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;
}
typescript
interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

interface ApiResponse<T> {
  data: T;
  success: boolean;
}

// 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')
  );
}

// API响应的守卫函数
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)
  );
}

// 使用示例
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('API响应格式无效');
  }

  return data.data;
}

Discriminated Unions

可辨识联合

typescript
// 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
}
typescript
// 可辨识联合模式
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 };
}

// 通过辨识符实现类型守卫
function isSuccess<T, E>(result: Result<T, E>): result is { success: true; data: T } {
  return result.success === true;
}

// 使用示例
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已知data存在
} else {
  console.error(result.error.message);  // TypeScript已知error存在
}

Utility Types for Migration

迁移用工具类型

typescript
// 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>
typescript
// 将属性设为必填
type RequiredUser = Required<User>;

// 将属性设为可选
type PartialUser = Partial<User>;

// 选取特定属性
type UserCredentials = Pick<User, 'email' | 'id'>;

// 排除特定属性
type PublicUser = Omit<User, 'password' | 'internalId'>;

// 将属性设为只读
type ReadonlyUser = Readonly<User>;

// 深度只读
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? DeepReadonly<T[P]>
    : T[P];
};

// 非空类型
type DefiniteString = NonNullable<string | null | undefined>;  // string

// 提取与排除
type AdminRole = Extract<User['role'], 'admin'>;  // 'admin'
type NonAdminRole = Exclude<User['role'], 'admin'>;  // 'user'

// 记录类型
type UserById = Record<string, User>;

// 参数与返回值类型
type FetchParams = Parameters<typeof fetch>;  // [input: RequestInfo, init?: RequestInit]
type FetchReturn = ReturnType<typeof fetch>;  // Promise<Response>

Common Migration Patterns

常见迁移模式

Handling Optional Chaining

可选链处理

typescript
// 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;
}
typescript
// 之前:不安全的属性访问
const userName = user.profile.settings.displayName;

// 之后:使用可选链安全访问
const userName = user?.profile?.settings?.displayName;

// 结合空值合并运算符
const userName = user?.profile?.settings?.displayName ?? '匿名用户';

// 结合类型收窄
function getDisplayName(user: User | null): string {
  if (!user?.profile?.settings?.displayName) {
    return '匿名用户';
  }
  return user.profile.settings.displayName;
}

Assertion Functions

断言函数

typescript
// 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);
}
typescript
// 断言函数
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
  if (value === undefined || value === null) {
    throw new Error('值未定义');
  }
}

function assertIsUser(value: unknown): asserts value is User {
  if (!isUser(value)) {
    throw new Error('值不是User类型');
  }
}

// 使用示例
function processUser(maybeUser: unknown) {
  assertIsUser(maybeUser);
  // maybeUser现在被推断为User类型
  console.log(maybeUser.name);
}

Error Handling

错误处理

typescript
// 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';
}
typescript
// 之前:catch中使用any类型
try {
  await riskyOperation();
} catch (error) {
  console.error(error.message);  // 启用useUnknownInCatchVariables后会报错
}

// 之后:正确的错误处理
try {
  await riskyOperation();
} catch (error) {
  if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error('未知错误:', String(error));
  }
}

// 辅助函数
function getErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  if (typeof error === 'string') return error;
  return '发生未知错误';
}

Index Signatures

索引签名

typescript
// 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
typescript
// 之前:不安全的索引访问
const users: Record<string, User> = {};
const user = users['unknown-id'];
console.log(user.name);  // 启用noUncheckedIndexedAccess后会报错

// 之后:正确的空值检查
const user = users['unknown-id'];
if (user) {
  console.log(user.name);
}

// 或使用断言(仅当确定存在时使用)
const user = users['known-id']!;

// 更佳方案:使用Map
const usersMap = new Map<string, User>();
const user = usersMap.get('some-id');  // 天然为User | undefined类型

Migration Script

迁移脚本

typescript
// 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);
typescript
// 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)
  );

  // 逐个启用严格模式标志
  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;
}

// 使用示例
const analysis = analyzeProject('./tsconfig.json');
console.log('严格模式分析结果:', analysis);

Best Practices

最佳实践

  1. Incremental adoption: One flag at a time
  2. Start with noImplicitAny: Easiest to fix
  3. Add type guards: Runtime safety
  4. Use assertion functions: Fail fast
  5. Avoid non-null assertions: Use sparingly
  6. Document patterns: Team consistency
  7. CI enforcement: Prevent regression
  8. Use unknown over any: Better type safety
  1. 渐进式采用:每次启用一个标志
  2. 从noImplicitAny开始:最容易修复
  3. 添加类型守卫:保障运行时安全
  4. 使用断言函数:快速失败
  5. 避免非空断言:谨慎使用
  6. 记录模式:保持团队一致性
  7. CI强制执行:防止回归
  8. 优先使用unknown而非any:更好的类型安全

Output Checklist

输出检查清单

Every strict migration should include:
  • Baseline error count per flag
  • Migration plan with phases
  • Type guard utilities
  • Assertion functions
  • Error handling patterns
  • Index access handling
  • Optional chaining usage
  • Updated tsconfig.json
  • Team documentation
  • CI strict checking
每次严格模式迁移都应包含:
  • 各标志的基准错误数
  • 分阶段迁移计划
  • 类型守卫工具
  • 断言函数
  • 错误处理模式
  • 索引访问处理方案
  • 可选链使用示例
  • 更新后的tsconfig.json
  • 团队文档
  • CI严格检查配置