TypeScript 最佳实践:让你的代码更安全、更优雅

TypeScript 最佳实践:让你的代码更安全、更优雅

前言

TypeScript 已经成为现代前端开发的标配。但是,仅仅使用 TypeScript 并不能保证代码质量。本文将分享一些实用的 TypeScript 最佳实践,帮助你写出更安全、更优雅的代码。

类型定义最佳实践

1. 优先使用 interface 而非 type

虽然 interfacetype 在很多场景下可以互换,但 interface 有更好的扩展性:

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

// 可以轻松扩展
interface AdminUser extends User {
  permissions: string[]
}

// ❌ 避免:使用 type
type User = {
  id: string
  name: string
  email: string
}

// 扩展需要使用交叉类型
type AdminUser = User & {
  permissions: string[]
}

2. 使用字面量类型提高类型精确度

// ❌ 过于宽泛
interface Button {
  type: string
  size: string
}

// ✅ 精确的字面量类型
interface Button {
  type: 'primary' | 'secondary' | 'ghost'
  size: 'small' | 'medium' | 'large'
}

// 使用 const 断言创建只读对象
const BUTTON_TYPES = {
  PRIMARY: 'primary',
  SECONDARY: 'secondary',
  GHOST: 'ghost'
} as const

type ButtonType = typeof BUTTON_TYPES[keyof typeof BUTTON_TYPES]

3. 善用泛型约束

// 基础泛型
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

// 条件类型
type IsArray<T> = T extends any[] ? true : false

// 实用的泛型工具类型
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}

// 高级示例:提取 Promise 的返回类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T

// 使用示例
async function fetchUser(): Promise<User> {
  // ...
}

type FetchedUser = UnwrapPromise<ReturnType<typeof fetchUser>> // User

代码组织最佳实践

1. 模块化类型定义

// types/user.ts
export interface User {
  id: string
  name: string
  email: string
}

export interface UserProfile extends User {
  avatar?: string
  bio?: string
  createdAt: Date
}

// types/api.ts
export interface ApiResponse<T> {
  data: T
  status: number
  message?: string
}

export interface PaginatedResponse<T> extends ApiResponse<T[]> {
  page: number
  pageSize: number
  total: number
}

// 使用
import type { User } from './types/user'
import type { ApiResponse } from './types/api'

async function getUser(id: string): Promise<ApiResponse<User>> {
  // ...
}

2. 使用命名空间组织相关类型

// 将相关类型组织在一起
namespace UserAPI {
  export interface User {
    id: string
    name: string
  }
  
  export interface CreateUserRequest {
    name: string
    email: string
    password: string
  }
  
  export interface UpdateUserRequest {
    name?: string
    email?: string
  }
  
  export type UserResponse = ApiResponse<User>
}

// 使用
function createUser(data: UserAPI.CreateUserRequest): Promise<UserAPI.UserResponse> {
  // ...
}

3. 分离业务逻辑和类型定义

// models/user.model.ts - 纯类型定义
export interface IUser {
  id: string
  name: string
  email: string
}

// services/user.service.ts - 业务逻辑
import type { IUser } from '../models/user.model'

export class UserService {
  async getUser(id: string): Promise<IUser> {
    // 业务逻辑
  }
  
  async createUser(data: Omit<IUser, 'id'>): Promise<IUser> {
    // 业务逻辑
  }
}

类型安全最佳实践

1. 避免使用 any

// ❌ 避免
function processData(data: any) {
  return data.value // 没有类型检查
}

// ✅ 使用 unknown
function processData(data: unknown) {
  // 需要类型守卫
  if (isValidData(data)) {
    return data.value
  }
  throw new Error('Invalid data')
}

// 类型守卫
function isValidData(data: unknown): data is { value: string } {
  return (
    typeof data === 'object' &&
    data !== null &&
    'value' in data &&
    typeof (data as any).value === 'string'
  )
}

2. 使用严格的配置

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

3. 使用类型守卫和断言函数

// 类型守卫
function isString(value: unknown): value is string {
  return typeof value === 'string'
}

// 断言函数
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('Value must be a string')
  }
}

// 使用示例
function processValue(value: unknown) {
  if (isString(value)) {
    // value 的类型是 string
    console.log(value.toUpperCase())
  }
  
  // 或者使用断言
  assertIsString(value)
  // 之后 value 的类型就是 string
  console.log(value.toUpperCase())
}

高级类型技巧

1. 映射类型和条件类型

// 将所有属性变为可选
type Partial<T> = {
  [P in keyof T]?: T[P]
}

// 将所有属性变为只读
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

// 排除某些属性
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

// 条件类型示例
type NonNullable<T> = T extends null | undefined ? never : T

// 递归类型
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}

2. 模板字面量类型

// 创建特定格式的字符串类型
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type APIEndpoint = '/users' | '/posts' | '/comments'

type APIRoute = `${HTTPMethod} ${APIEndpoint}`
// 类型为: "GET /users" | "GET /posts" | ... | "DELETE /comments"

// 实用示例:CSS 单位
type CSSUnit = 'px' | 'em' | 'rem' | '%'
type CSSValue = `${number}${CSSUnit}`

function setWidth(value: CSSValue) {
  // ...
}

setWidth('100px') // ✅
setWidth('2em')   // ✅
setWidth('100')   // ❌ 类型错误

3. 使用 infer 进行类型推断

// 提取函数返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never

// 提取 Promise 中的类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T

// 提取数组元素类型
type ArrayElement<T> = T extends (infer U)[] ? U : never

// 实用示例:提取函数参数类型
type Parameters<T extends (...args: any) => any> = 
  T extends (...args: infer P) => any ? P : never

function greet(name: string, age: number) {
  return `Hello ${name}, you are ${age} years old`
}

type GreetParams = Parameters<typeof greet> // [string, number]

React + TypeScript 最佳实践

1. 组件类型定义

// 函数组件
interface ButtonProps {
  variant: 'primary' | 'secondary'
  size?: 'small' | 'medium' | 'large'
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
  children: React.ReactNode
}

const Button: React.FC<ButtonProps> = ({ 
  variant, 
  size = 'medium', 
  onClick, 
  children 
}) => {
  return (
    <button 
      className={`btn btn-${variant} btn-${size}`}
      onClick={onClick}
    >
      {children}
    </button>
  )
}

// 使用泛型的组件
interface ListProps<T> {
  items: T[]
  renderItem: (item: T) => React.ReactNode
  keyExtractor: (item: T) => string
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map(item => (
        <li key={keyExtractor(item)}>
          {renderItem(item)}
        </li>
      ))}
    </ul>
  )
}

2. Hook 类型定义

// 自定义 Hook
function useLocalStorage<T>(
  key: string, 
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      return initialValue
    }
  })

  const setValue = (value: T | ((prev: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value
      setStoredValue(valueToStore)
      window.localStorage.setItem(key, JSON.stringify(valueToStore))
    } catch (error) {
      console.error(error)
    }
  }

  return [storedValue, setValue]
}

// 使用
const [user, setUser] = useLocalStorage<User | null>('user', null)

性能优化技巧

1. 使用 const enum 减少运行时开销

// const enum 在编译时会被内联
const enum Direction {
  Up,
  Down,
  Left,
  Right
}

// 编译后直接替换为数字
let direction = Direction.Up // 编译为: let direction = 0

2. 条件导入类型

// 只导入类型,不会影响运行时
import type { User } from './types'

// 条件导入
if (process.env.NODE_ENV === 'development') {
  import('./dev-tools').then(({ setupDevTools }) => {
    setupDevTools()
  })
}

总结

TypeScript 不仅仅是给 JavaScript 加上类型,更是一种思维方式的转变。通过遵循这些最佳实践,你可以:

  1. 提高代码质量:减少运行时错误
  2. 改善开发体验:更好的 IDE 支持和自动补全
  3. 增强代码可维护性:类型即文档
  4. 提升团队协作:统一的代码规范

记住,TypeScript 的目标是帮助我们写出更好的代码,而不是增加负担。合理使用这些技巧,让 TypeScript 成为你的得力助手!