Skip to content

TypeScript 最佳实践

概述

TypeScript 最佳实践是一系列经过验证的开发建议,可以帮助你编写更安全、更易维护、更高效的 TypeScript 代码。遵循这些实践可以让你充分利用 TypeScript 的类型系统,减少错误,提高代码质量。本文档涵盖了从类型定义到项目组织的各个方面。

类型定义最佳实践

优先使用接口而非类型别名(用于对象)

对于对象类型的定义,优先使用 interface,因为它支持声明合并,更适合扩展:

typescript
// ✅ 推荐:使用接口定义对象类型
interface User {
  name: string;
  age: number;
}

// 可以扩展
interface User {
  email?: string; // 声明合并
}

// ❌ 不推荐:使用类型别名定义对象
type User = {
  name: string;
  age: number;
};

提示

当需要联合类型、交叉类型或工具类型时,使用 type 更合适。

使用类型别名简化复杂类型

对于复杂的类型定义,使用类型别名可以提高可读性:

typescript
// ✅ 推荐:使用类型别名简化复杂类型
type EventHandler = (event: Event) => void;
type StringOrNumber = string | number;
type Nullable<T> = T | null;

// 使用
const onClick: EventHandler = (e) => {
  console.log('clicked');
};

避免使用 any,优先使用 unknown

any 会禁用类型检查,应该尽量避免使用。当需要表示"未知类型"时,使用 unknown

typescript
// ❌ 不推荐:使用 any
function processData(data: any) {
  return data.value; // 没有类型检查
}

// ✅ 推荐:使用 unknown
function processData(data: unknown) {
  if (typeof data === 'object' && data !== null && 'value' in data) {
    return (data as { value: string }).value; // 需要类型守卫
  }
  throw new Error('Invalid data');
}

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

使用字面量类型可以让类型更加精确:

typescript
// ✅ 推荐:使用字面量类型
type Status = 'pending' | 'success' | 'error';

function handleStatus(status: Status) {
  // 类型更精确,IDE 可以提供自动补全
}

// ❌ 不推荐:使用 string
function handleStatus(status: string) {
  // 类型太宽泛,容易出错
}

函数最佳实践

明确函数返回类型

即使 TypeScript 可以推断返回类型,明确声明返回类型可以提高代码可读性:

typescript
// ✅ 推荐:明确返回类型
function calculateTotal(items: Item[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// ❌ 不推荐:依赖类型推断
function calculateTotal(items: Item[]) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

使用函数重载而非联合类型参数

当函数有多种调用方式时,使用函数重载比联合类型更清晰:

typescript
// ✅ 推荐:使用函数重载
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
  return String(value);
}

// ❌ 不推荐:使用联合类型
function format(value: string | number): string {
  return String(value);
}

使用可选参数而非重载(简单情况)

对于简单的可选参数,直接使用可选参数比函数重载更简洁:

typescript
// ✅ 推荐:使用可选参数
function greet(name: string, greeting?: string): string {
  return greeting ? `${greeting}, ${name}!` : `Hello, ${name}!`;
}

// ❌ 不推荐:过度使用重载
function greet(name: string): string;
function greet(name: string, greeting: string): string;
function greet(name: string, greeting?: string): string {
  return greeting ? `${greeting}, ${name}!` : `Hello, ${name}!`;
}

类最佳实践

使用访问修饰符明确可见性

明确使用 publicprivateprotected 修饰符,提高代码可读性:

typescript
// ✅ 推荐:明确访问修饰符
class User {
  public name: string;        // 公开属性
  private age: number;        // 私有属性
  protected email: string;    // 受保护属性

  constructor(name: string, age: number, email: string) {
    this.name = name;
    this.age = age;
    this.email = email;
  }
}

// ❌ 不推荐:依赖默认行为
class User {
  name: string;  // 默认是 public,但不明确
  age: number;
}

使用参数属性简化构造函数

当构造函数只是简单赋值时,使用参数属性可以简化代码:

typescript
// ✅ 推荐:使用参数属性
class User {
  constructor(
    public name: string,
    private age: number,
    protected email: string
  ) {}
}

// ❌ 不推荐:手动赋值
class User {
  name: string;
  age: number;
  email: string;

  constructor(name: string, age: number, email: string) {
    this.name = name;
    this.age = age;
    this.email = email;
  }
}

优先使用私有字段(#)而非 private

对于真正的私有成员,使用私有字段(#)比 private 更安全:

typescript
// ✅ 推荐:使用私有字段
class Counter {
  #count: number = 0;  // 真正的私有,编译后仍然私有

  increment() {
    this.#count++;
  }
}

// ❌ 不推荐:使用 private(编译后可能被访问)
class Counter {
  private count: number = 0;  // 编译后可能被访问

  increment() {
    this.count++;
  }
}

泛型最佳实践

使用有意义的泛型参数名

使用描述性的泛型参数名,而不是单个字母:

typescript
// ✅ 推荐:使用描述性名称
function getProperty<TObj, TKey extends keyof TObj>(
  obj: TObj,
  key: TKey
): TObj[TKey] {
  return obj[key];
}

// ❌ 不推荐:使用单字母
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

提示

对于简单的泛型函数,使用 TUV 等单字母是可以接受的。但对于复杂的泛型,使用描述性名称更好。

使用泛型约束而非 any

使用泛型约束来限制类型参数,而不是使用 any

typescript
// ✅ 推荐:使用泛型约束
function getLength<T extends { length: number }>(item: T): number {
  return item.length;
}

// ❌ 不推荐:使用 any
function getLength(item: any): number {
  return item.length;
}

模块和导入最佳实践

使用命名导出而非默认导出

命名导出提供更好的重构支持和自动补全:

typescript
// ✅ 推荐:使用命名导出
export function calculateTotal(items: Item[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

export interface User {
  name: string;
  age: number;
}

// 导入
import { calculateTotal, User } from './utils';

// ❌ 不推荐:使用默认导出
export default function calculateTotal(items: Item[]): number {
  // ...
}

// 导入时名称可能不一致
import calc from './utils';
import calculateTotal from './utils';  // 名称可以随意更改

使用路径别名简化导入

使用路径别名可以让导入路径更清晰:

typescript
// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"],
      "@utils/*": ["./src/utils/*"]
    }
  }
}

// ✅ 推荐:使用路径别名
import { formatDate } from '@utils/date';
import { User } from '@/types';

// ❌ 不推荐:使用相对路径
import { formatDate } from '../../../utils/date';
import { User } from '../../types';

类型守卫和类型断言最佳实践

优先使用类型守卫而非类型断言

类型守卫更安全,可以在运行时检查类型:

typescript
// ✅ 推荐:使用类型守卫
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function processValue(value: unknown) {
  if (isString(value)) {
    // TypeScript 知道 value 是 string
    console.log(value.toUpperCase());
  }
}

// ❌ 不推荐:使用类型断言
function processValue(value: unknown) {
  const str = value as string;  // 不安全,可能出错
  console.log(str.toUpperCase());
}

使用 satisfies 操作符(TypeScript 4.9+)

使用 satisfies 操作符可以在保持类型推断的同时进行类型检查:

typescript
// ✅ 推荐:使用 satisfies
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
} satisfies Config;

// TypeScript 会检查 config 是否符合 Config 类型
// 同时保持 config 的类型推断(而不是 Config 类型)

// ❌ 不推荐:使用类型注解
const config: Config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
};
// config 的类型是 Config,失去了字面量类型的精确性

错误处理最佳实践

使用 Result 类型处理错误

使用 Result 类型可以更好地处理错误,避免抛出异常:

typescript
// ✅ 推荐:使用 Result 类型
type Result<T, E = Error> = 
  | { success: true; data: T }
  | { success: false; error: E };

function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { success: false, error: new Error('Division by zero') };
  }
  return { success: true, data: a / b };
}

// 使用
const result = divide(10, 2);
if (result.success) {
  console.log(result.data);  // TypeScript 知道这是 number
} else {
  console.error(result.error);
}

// ❌ 不推荐:抛出异常
function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error('Division by zero');
  }
  return a / b;
}

性能最佳实践

避免深度嵌套的类型

深度嵌套的类型会影响编译性能,使用类型别名来简化:

typescript
// ❌ 不推荐:深度嵌套
type ComplexType = {
  user: {
    profile: {
      settings: {
        theme: {
          colors: {
            primary: string;
            secondary: string;
          };
        };
      };
    };
  };
};

// ✅ 推荐:使用类型别名分解
type ThemeColors = {
  primary: string;
  secondary: string;
};

type Theme = {
  colors: ThemeColors;
};

type Settings = {
  theme: Theme;
};

type Profile = {
  settings: Settings;
};

type ComplexType = {
  user: {
    profile: Profile;
  };
};

使用 const 断言提高类型推断

使用 as const 可以让 TypeScript 推断出更精确的字面量类型:

typescript
// ✅ 推荐:使用 const 断言
const colors = ['red', 'green', 'blue'] as const;
// 类型:readonly ["red", "green", "blue"]

type Color = typeof colors[number];  // "red" | "green" | "blue"

// ❌ 不推荐:不使用 const 断言
const colors = ['red', 'green', 'blue'];
// 类型:string[]

代码组织最佳实践

将类型定义集中管理

将类型定义放在单独的文件中,便于管理和复用:

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

export type UserStatus = 'active' | 'inactive' | 'suspended';

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

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

对于相关的类型,使用命名空间进行组织:

typescript
// ✅ 推荐:使用命名空间
namespace UserTypes {
  export interface User {
    id: string;
    name: string;
  }

  export interface UserProfile extends User {
    bio: string;
    avatar: string;
  }
}

// 使用
const user: UserTypes.User = {
  id: '1',
  name: 'John'
};

工具类型最佳实践

创建自定义工具类型

创建自定义工具类型可以提高代码复用性:

typescript
// ✅ 推荐:创建自定义工具类型
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// 使用:使某些属性可选
interface User {
  id: string;
  name: string;
  email: string;
  age: number;
}

type CreateUserInput = Optional<User, 'id' | 'age'>;
// { name: string; email: string; id?: string; age?: number; }

注意事项

注意

  1. 不要过度使用类型:不是所有地方都需要明确的类型注解,让 TypeScript 的类型推断发挥作用。
  2. 保持类型简单:复杂的类型虽然强大,但会影响可读性和编译性能。
  3. 定期重构类型:随着项目发展,及时重构和改进类型定义。
  4. 使用严格模式:启用 TypeScript 的严格模式可以捕获更多潜在错误。

提示

最佳实践不是一成不变的,应该根据项目需求和团队约定进行调整。重要的是保持代码的一致性和可维护性。

相关链接

基于 VitePress 构建