typescript-type-safety

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TypeScript Type Safety

TypeScript 类型安全

Overview

概述

Zero tolerance for
any
types.
Every
any
is a runtime bug waiting to happen.
Replace
any
with proper types using interfaces,
unknown
with type guards, or generic constraints. Use
@ts-expect-error
with explanation only when absolutely necessary.
对
any
类型零容忍。
每个
any
都是一个随时可能爆发的运行时bug。
使用接口、搭配类型守卫的
unknown
或泛型约束来替换
any
。只有在绝对必要时,才配合说明使用
@ts-expect-error
。

When to Use

适用场景

Use when you see:
  • : any
    in function parameters or return types
  • as any
    type assertions
  • TypeScript errors you're tempted to ignore
  • External libraries without proper types
  • Catch blocks with implicit
    any
Don't use for:
  • Already properly typed code
  • Third-party
    .d.ts
    files (contribute upstream instead)
当你遇到以下情况时使用:
  • 函数参数或返回类型中的
    : any
  • as any
    类型断言
  • 你想忽略的TypeScript错误
  • 没有合理类型定义的外部库
  • 隐式
    any
    的catch块
不适用场景:
  • 已具备合理类型定义的代码
  • 第三方
    .d.ts
    文件(应向上游贡献修复)

Type Safety Hierarchy

类型安全层级

Prefer in this order:
  1. Explicit interface/type definition
  2. Generic type parameters with constraints
  3. Union types
  4. unknown
    (with type guards)
  5. never
    (for impossible states)
Never use:
any
优先顺序如下:
  1. 显式接口/类型定义
  2. 带约束的泛型类型参数
  3. 联合类型
  4. unknown
    (搭配类型守卫)
  5. never
    (用于不可能的状态)
绝对不要使用:
any

Quick Reference

速查参考

PatternBadGood
Error handling
catch (error: any)
catch (error) { if (error instanceof Error) ... }
Unknown data
JSON.parse(str) as any
const data = JSON.parse(str); if (isValid(data)) ...
Type assertions
(request as any).user
(request as AuthRequest).user
Double casting
return data as unknown as Type
Align interfaces instead: make types compatible
External libs
const server = fastify() as any
declare module 'fastify' { ... }
Generics
function process(data: any)
function process<T extends Record<string, unknown>>(data: T)
模式错误示例正确示例
错误处理
catch (error: any)
catch (error) { if (error instanceof Error) ... }
未知数据
JSON.parse(str) as any
const data = JSON.parse(str); if (isValid(data)) ...
类型断言
(request as any).user
(request as AuthRequest).user
双重类型转换
return data as unknown as Type
对齐接口:让类型兼容
外部库
const server = fastify() as any
declare module 'fastify' { ... }
泛型
function process(data: any)
function process<T extends Record<string, unknown>>(data: T)

Implementation

实践方案

Error Handling

错误处理

typescript
// ❌ BAD
try {
  await operation();
} catch (error: any) {
  console.error(error.message);
}

// ✅ GOOD - Use unknown and type guard
try {
  await operation();
} catch (error) {
  if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error('Unknown error:', String(error));
  }
}

// ✅ BETTER - Helper function
function toError(error: unknown): Error {
  if (error instanceof Error) return error;
  return new Error(String(error));
}

try {
  await operation();
} catch (error) {
  const err = toError(error);
  console.error(err.message);
}
typescript
// ❌ 错误示例
try {
  await operation();
} catch (error: any) {
  console.error(error.message);
}

// ✅ 正确示例 - 使用unknown和类型守卫
try {
  await operation();
} catch (error) {
  if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error('未知错误:', String(error));
  }
}

// ✅ 更优方案 - 辅助函数
function toError(error: unknown): Error {
  if (error instanceof Error) return error;
  return new Error(String(error));
}

try {
  await operation();
} catch (error) {
  const err = toError(error);
  console.error(err.message);
}

Unknown Data Validation

未知数据校验

typescript
// ❌ BAD
const data = await response.json() as any;
console.log(data.user.name);

// ✅ GOOD - Type guard
interface UserResponse {
  user: {
    name: string;
    email: string;
  };
}

function isUserResponse(data: unknown): data is UserResponse {
  return (
    typeof data === 'object' &&
    data !== null &&
    'user' in data &&
    typeof data.user === 'object' &&
    data.user !== null &&
    'name' in data.user &&
    typeof data.user.name === 'string'
  );
}

const data = await response.json();
if (isUserResponse(data)) {
  console.log(data.user.name); // Type-safe
}
typescript
// ❌ 错误示例
const data = await response.json() as any;
console.log(data.user.name);

// ✅ 正确示例 - 类型守卫
interface UserResponse {
  user: {
    name: string;
    email: string;
  };
}

function isUserResponse(data: unknown): data is UserResponse {
  return (
    typeof data === 'object' &&
    data !== null &&
    'user' in data &&
    typeof data.user === 'object' &&
    data.user !== null &&
    'name' in data.user &&
    typeof data.user.name === 'string'
  );
}

const data = await response.json();
if (isUserResponse(data)) {
  console.log(data.user.name); // 类型安全
}

Module Augmentation

模块扩展

typescript
// ❌ BAD
const user = (request as any).user;
const db = (server as any).pg;

// ✅ GOOD - Augment third-party types
import { FastifyRequest, FastifyInstance } from 'fastify';

interface AuthUser {
  user_id: string;
  username: string;
  email: string;
}

declare module 'fastify' {
  interface FastifyRequest {
    user?: AuthUser;
  }

  interface FastifyInstance {
    pg: PostgresPlugin;
  }
}

// Now type-safe everywhere
const user = request.user; // AuthUser | undefined
const db = server.pg;      // PostgresPlugin
typescript
// ❌ 错误示例
const user = (request as any).user;
const db = (server as any).pg;

// ✅ 正确示例 - 扩展第三方类型
import { FastifyRequest, FastifyInstance } from 'fastify';

interface AuthUser {
  user_id: string;
  username: string;
  email: string;
}

declare module 'fastify' {
  interface FastifyRequest {
    user?: AuthUser;
  }

  interface FastifyInstance {
    pg: PostgresPlugin;
  }
}

// 现在所有地方都类型安全
const user = request.user; // AuthUser | undefined
const db = server.pg;      // PostgresPlugin

Generic Constraints

泛型约束

typescript
// ❌ BAD
function merge(a: any, b: any): any {
  return { ...a, ...b };
}

// ✅ GOOD - Constrained generic
function merge<
  T extends Record<string, unknown>,
  U extends Record<string, unknown>
>(a: T, b: U): T & U {
  return { ...a, ...b };
}
typescript
// ❌ 错误示例
function merge(a: any, b: any): any {
  return { ...a, ...b };
}

// ✅ 正确示例 - 带约束的泛型
function merge<
  T extends Record<string, unknown>,
  U extends Record<string, unknown>
>(a: T, b: U): T & U {
  return { ...a, ...b };
}

Type Alignment (Avoid Double Casts)

类型对齐(避免双重类型转换)

typescript
// ❌ BAD - Double cast indicates misaligned types
interface SearchPackage {
  id: string;
  type: string;  // Too loose
}

interface RegistryPackage {
  id: string;
  type: PackageType;  // Specific enum
}

return data.packages as unknown as RegistryPackage[];  // Hiding incompatibility

// ✅ GOOD - Align types from the source
interface SearchPackage {
  id: string;
  type: PackageType;  // Use same specific type
}

interface RegistryPackage {
  id: string;
  type: PackageType;  // Now compatible
}

return data.packages;  // No cast needed - types match
Rule: If you need
as unknown as Type
, your interfaces are misaligned. Fix the root cause, don't hide it with double casts.
typescript
// ❌ 错误示例 - 双重类型转换表明类型不匹配
interface SearchPackage {
  id: string;
  type: string;  // 过于松散
}

interface RegistryPackage {
  id: string;
  type: PackageType;  // 特定枚举
}

return data.packages as unknown as RegistryPackage[];  // 隐藏不兼容性

// ✅ 正确示例 - 从源头对齐类型
interface SearchPackage {
  id: string;
  type: PackageType;  // 使用相同的特定类型
}

interface RegistryPackage {
  id: string;
  type: PackageType;  // 现在类型兼容
}

return data.packages;  // 无需类型转换 - 类型匹配
规则: 如果你需要使用
as unknown as Type
,说明你的接口定义不匹配。修复根本问题,不要用双重类型转换来掩盖。

ESM Import Extensions

ESM 导入扩展名

Always use
.js
extension for relative imports in ESM projects.
Node.js ESM requires explicit file extensions. TypeScript compiles
.ts
→
.js
, so imports must reference the output extension.
typescript
// ❌ BAD - Will fail at runtime in ESM
import { helper } from './utils';
import { CLIError } from '../utils/cli-error';
import type { Package } from './types/package';

// ✅ GOOD - Explicit .js extensions
import { helper } from './utils.js';
import { CLIError } from '../utils/cli-error.js';
import type { Package } from './types/package.js';
Why this is a TypeScript/type safety issue:
  • TypeScript doesn't catch missing extensions at compile time
  • Errors only appear at runtime:
    ERR_MODULE_NOT_FOUND
  • CI builds fail but local development works (cached modules)
  • This is one of the most common "works locally, fails in CI" issues
TSConfig for ESM:
json
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    // OR
    "module": "ESNext",
    "moduleResolution": "bundler"
  }
}
Common Import Mistakes:
PatternIssueFix
import { x } from './file'
Missing extension
import { x } from './file.js'
import { x } from './dir'
Missing index
import { x } from './dir/index.js'
import pkg from 'pkg/subpath'
Package exportCheck package.json
exports
field
Linting for Import Extensions:
bash
undefined
在ESM项目中,相对导入必须始终使用
.js
扩展名。
Node.js ESM要求显式的文件扩展名。TypeScript会将
.ts
编译为
.js
,因此导入必须引用输出文件的扩展名。
typescript
// ❌ 错误示例 - 在ESM运行时会失败
import { helper } from './utils';
import { CLIError } from '../utils/cli-error';
import type { Package } from './types/package';

// ✅ 正确示例 - 显式使用.js扩展名
import { helper } from './utils.js';
import { CLIError } from '../utils/cli-error.js';
import type { Package } from './types/package.js';
这为何是TypeScript/类型安全问题:
  • TypeScript在编译时不会捕获缺失的扩展名
  • 错误仅在运行时出现:
    ERR_MODULE_NOT_FOUND
  • CI构建失败但本地开发正常(缓存模块)
  • 这是最常见的“本地正常,CI失败”问题之一
ESM 对应的TSConfig配置:
json
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    // 或者
    "module": "ESNext",
    "moduleResolution": "bundler"
  }
}
常见导入错误:
模式问题修复方案
import { x } from './file'
缺失扩展名
import { x } from './file.js'
import { x } from './dir'
缺失index文件
import { x } from './dir/index.js'
import pkg from 'pkg/subpath'
包导出问题检查package.json的
exports
字段
导入扩展名的 lint 检查:
bash
undefined

Find imports missing .js extension

查找缺失.js扩展名的导入

grep -rn "from '..?/[^'][^j][^s]'" --include=".ts" src/
grep -rn "from '..?/[^'][^j][^s]'" --include=".ts" src/

ESLint rule (if using eslint)

ESLint规则(如果使用eslint)

"import/extensions": ["error", "always", { "ignorePackages": true }]

"import/extensions": ["error", "always", { "ignorePackages": true }]

undefined
undefined

Common Mistakes

常见错误

MistakeWhy It FailsFix
Using
any
for third-party libs
Loses all type safetyUse module augmentation or
@types/*
package
as any
for complex types
Hides real type errorsCreate proper interface or use
unknown
as unknown as Type
double casts
Misaligned interfacesAlign types at source - same enums/unions
Skipping catch block typesUnsafe error accessUse
unknown
with type guards or toError helper
Generic functions without constraintsAllows invalid operationsAdd
extends
constraint
Ignoring
ts-ignore
accumulation
Tech debt compoundsFix root cause, use
@ts-expect-error
with comment
Missing
.js
import extensions
ESM runtime failuresAlways use
.js
for relative imports
错误做法失败原因修复方案
对第三方库使用
any
完全丧失类型安全使用模块扩展或
@types/*
包
对复杂类型使用
as any
隐藏真实的类型错误创建合理的接口或使用
unknown
as unknown as Type
双重类型转换
接口定义不匹配从源头对齐类型——使用相同的枚举/联合类型
忽略catch块的类型不安全的错误访问使用
unknown
搭配类型守卫或toError辅助函数
无约束的泛型函数允许无效操作添加
extends
约束
忽略
ts-ignore
的累积
技术债务不断增加修复根本问题,使用带注释的
@ts-expect-error
缺失.js导入扩展名ESM运行时失败相对导入始终使用.js

TSConfig Strict Settings

TSConfig 严格模式设置

Enable all strict options for maximum type safety:
json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}
启用所有严格选项以获得最大类型安全:
json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

Type Audit Workflow

类型审计流程

  1. Find:
    grep -r ": any\|as any" --include="*.ts" src/
  2. Categorize: Group by pattern (errors, requests, external libs)
  3. Define: Create interfaces/types for each category
  4. Replace: Systematic replacement with proper types
  5. Validate:
    npm run build
    must succeed
  6. Test: All tests must pass
  1. 查找:
    grep -r ": any\|as any" --include="*.ts" src/
  2. 分类:按模式分组(错误处理、请求、外部库等)
  3. 定义:为每个分类创建接口/类型
  4. 替换:系统地用合理类型替换
  5. 验证:
    npm run build
    必须成功
  6. 测试:所有测试必须通过

Real-World Impact

实际业务影响

Before type safety:
  • Runtime errors from undefined properties
  • Silent failures from type mismatches
  • Hours debugging production issues
  • Difficult refactoring
After type safety:
  • Errors caught at compile time
  • IntelliSense shows all available properties
  • Confident refactoring with compiler help
  • Self-documenting code

Remember: Type safety isn't about making TypeScript happy - it's about preventing runtime bugs. Every
any
you eliminate is a production bug you prevent.
类型安全优化前:
  • 未定义属性导致的运行时错误
  • 类型不匹配导致的静默失败
  • 花费数小时调试生产问题
  • 重构困难
类型安全优化后:
  • 编译时捕获错误
  • 智能提示显示所有可用属性
  • 在编译器的帮助下放心重构
  • 代码自文档化

谨记: 类型安全不是为了让TypeScript满意——而是为了预防运行时bug。你消除的每一个
any
,都是在预防一个生产环境中的bug。