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。