TypeScript 最佳实践 2025:从类型安全到开发效率

September 22, 2025

TypeScript 最佳实践 2025:从类型安全到开发效率

经过多年的 TypeScript 项目实战,我总结了一套真正能提升代码质量和开发效率的实践。这不是教科书式的规则,而是来自实际项目中的经验教训。

1. 类型设计:从数据流开始思考

优先使用 Type 而非 Interface

虽然两者在大多数情况下可以互换,但我更倾向于使用 type

// ✅ 推荐:使用 type
type User = {
  id: string
  name: string
  email: string
}

// 联合类型、交叉类型更直观
type Admin = User & { role: 'admin'; permissions: string[] }
type Guest = { sessionId: string }
type AppUser = Admin | Guest

// ❌ 使用 interface 会更繁琐
interface Admin extends User {
  role: 'admin'
  permissions: string[]
}

何时使用 Interface?

  • 需要声明合并(Declaration Merging)时
  • 定义类的契约时

善用 Discriminated Unions

这是 TypeScript 最强大的特性之一:

type LoadingState = {
  status: 'loading'
}

type SuccessState<T> = {
  status: 'success'
  data: T
}

type ErrorState = {
  status: 'error'
  error: Error
}

type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState

// 使用时,TypeScript 会自动收窄类型
function renderUser(state: AsyncState<User>) {
  switch (state.status) {
    case 'loading':
      return <Spinner />

    case 'success':
      // TypeScript 知道这里 state.data 存在
      return <UserProfile user={state.data} />

    case 'error':
      // TypeScript 知道这里 state.error 存在
      return <ErrorMessage error={state.error} />
  }
}

这比使用可选属性好得多:

// ❌ 不好:需要到处检查
type AsyncState<T> = {
  loading?: boolean
  data?: T
  error?: Error
}

function renderUser(state: AsyncState<User>) {
  if (state.loading) return <Spinner />
  if (state.error) return <ErrorMessage error={state.error} />
  if (state.data) return <UserProfile user={state.data} />
  // 还要处理都不存在的情况?
}

2. 减少类型断言,增加类型推断

让 TypeScript 为你工作

// ❌ 过度使用类型断言
const data = JSON.parse(response) as User
const element = document.getElementById('app') as HTMLDivElement

// ✅ 使用类型守卫
function isUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    'name' in data &&
    'email' in data
  )
}

const data = JSON.parse(response)
if (isUser(data)) {
  // TypeScript 知道 data 是 User
  console.log(data.email)
}

// ✅ 使用泛型
function getElementById<T extends HTMLElement>(id: string): T | null {
  return document.getElementById(id) as T | null
}

const app = getElementById<HTMLDivElement>('app')

利用 const assertions

// ❌ 类型过于宽泛
const routes = {
  home: '/',
  about: '/about',
  blog: '/blog'
}
// routes.home 的类型是 string

// ✅ 使用 const assertion
const routes = {
  home: '/',
  about: '/about',
  blog: '/blog'
} as const
// routes.home 的类型是 '/'

// 现在可以创建精确的类型
type Route = typeof routes[keyof typeof routes] // '/' | '/about' | '/blog'

3. 泛型:强大但要克制

好的泛型使用

// ✅ 清晰的泛型约束
function mapObject<T extends Record<string, any>, U>(
  obj: T,
  mapper: (value: T[keyof T], key: keyof T) => U
): Record<keyof T, U> {
  const result = {} as Record<keyof T, U>
  for (const key in obj) {
    result[key] = mapper(obj[key], key)
  }
  return result
}

// 使用
const user = { name: 'Alice', age: 30 }
const lengths = mapObject(user, v => String(v).length)
// lengths: { name: number, age: number }

避免过度泛型化

// ❌ 过度复杂
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}

// ✅ 使用简单的方案
type UpdateUserInput = Partial<Pick<User, 'name' | 'email'>>

4. Utility Types 的实战应用

TypeScript 内置了许多实用的工具类型,善用它们可以大大减少重复代码:

type User = {
  id: string
  name: string
  email: string
  password: string
  createdAt: Date
  updatedAt: Date
}

// 创建用户时不需要 id 和时间戳
type CreateUserInput = Omit<User, 'id' | 'createdAt' | 'updatedAt'>

// 更新用户时所有字段都是可选的
type UpdateUserInput = Partial<CreateUserInput>

// API 返回时不包含密码
type UserResponse = Omit<User, 'password'>

// 只读的用户数据
type ReadonlyUser = Readonly<User>

// 提取函数参数类型
async function createUser(input: CreateUserInput) {
  // ...
}
type CreateUserParams = Parameters<typeof createUser>[0]

// 提取函数返回类型
type CreateUserResult = Awaited<ReturnType<typeof createUser>>

5. 严格模式配置

tsconfig.json 中启用所有严格检查:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

noUncheckedIndexedAccess 的重要性

const users = ['Alice', 'Bob']

// 没有 noUncheckedIndexedAccess
const user = users[10] // 类型是 string,实际是 undefined

// 启用后
const user = users[10] // 类型是 string | undefined

// 强制你处理边界情况
if (user) {
  console.log(user.toUpperCase())
}

6. React + TypeScript 最佳实践

组件 Props 定义

// ✅ 推荐方式
type ButtonProps = {
  children: React.ReactNode
  variant?: 'primary' | 'secondary'
  onClick?: () => void
} & Omit<React.ComponentPropsWithoutRef<'button'>, 'onClick'>

export function Button({ variant = 'primary', children, ...props }: ButtonProps) {
  return (
    <button className={`btn btn-${variant}`} {...props}>
      {children}
    </button>
  )
}

// ❌ 避免使用 FC
const Button: React.FC<ButtonProps> = ({ children }) => {
  // FC 有一些问题,比如隐式的 children
}

自定义 Hooks 类型

// ✅ 返回元组时使用 const assertion
function useToggle(initial = false) {
  const [value, setValue] = useState(initial)
  const toggle = useCallback(() => setValue(v => !v), [])
  return [value, toggle] as const
  // 返回类型是 [boolean, () => void] 而不是 (boolean | (() => void))[]
}

// ✅ 返回对象时更清晰
function useAsync<T>(asyncFunction: () => Promise<T>) {
  const [state, setState] = useState<AsyncState<T>>({ status: 'loading' })

  useEffect(() => {
    asyncFunction()
      .then(data => setState({ status: 'success', data }))
      .catch(error => setState({ status: 'error', error }))
  }, [])

  return state
}

7. 类型体操的实际应用

有时候需要一些高级类型操作,但要确保它们真的有用:

// 从对象中提取特定类型的键
type PickByType<T, U> = {
  [P in keyof T as T[P] extends U ? P : never]: T[P]
}

type User = {
  id: string
  name: string
  age: number
  isActive: boolean
}

type StringFields = PickByType<User, string> // { id: string; name: string }
type NumberFields = PickByType<User, number> // { age: number }

// 实际应用:自动生成表单字段类型
type FormFields<T> = {
  [K in keyof T]: {
    value: T[K]
    error?: string
    touched: boolean
  }
}

type UserForm = FormFields<User>
// {
//   id: { value: string; error?: string; touched: boolean }
//   name: { value: string; error?: string; touched: boolean }
//   ...
// }

8. 类型测试

是的,你应该测试你的类型!

// 使用 @ts-expect-error 验证类型错误
type Assert<T, Expected> = T extends Expected
  ? Expected extends T
    ? true
    : never
  : never

// 测试
type Test1 = Assert<PickByType<User, string>, { id: string; name: string }> // true

// @ts-expect-error - 应该报错
type Test2 = Assert<PickByType<User, string>, { id: string }> // 类型不匹配

// 或使用 expect-type 库
import { expectTypeOf } from 'expect-type'

expectTypeOf<PickByType<User, string>>().toEqualTypeOf<{ id: string; name: string }>()

9. 常见陷阱

枚举的问题

// ❌ 避免使用 enum
enum Color {
  Red,
  Green,
  Blue
}
// 会生成运行时代码,增加包体积

// ✅ 使用 const object
const Color = {
  Red: 'red',
  Green: 'green',
  Blue: 'blue'
} as const

type Color = typeof Color[keyof typeof Color]

可选链的类型陷阱

type User = {
  address?: {
    street: string
  }
}

const user: User = {}

// ❌ 类型是 string | undefined,但运行时是 undefined
const street = user.address?.street

// ✅ 明确处理
const street = user.address?.street ?? 'Unknown'

10. 性能考虑

避免深度递归类型

// ❌ 可能导致编译性能问题
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? DeepReadonly<T[P]>
    : T[P]
}

// ✅ 限制递归深度
type DeepReadonly<T, Depth extends number= 3> = {
  readonly [P in keyof T]: Depth extends 0
    ? T[P]
    : T[P] extends object
    ? DeepReadonly<T[P], Prev<Depth>>
    : T[P]
}

结论

TypeScript 的价值不在于写更多的类型,而在于:

  1. 及早发现错误:编译时而非运行时
  2. 更好的 IDE 支持:智能提示和自动补全
  3. 自文档化:类型即文档
  4. 重构信心:类型系统保证修改的安全性

关键是找到类型安全和开发效率的平衡点。不要为了类型而类型,而是让类型为你的开发流程服务。

记住:最好的类型是那些你不需要写的类型 —— 让 TypeScript 的类型推断为你工作。

推荐资源