Skip to content

第 5 章:真实项目中的选择 —— 我们是如何决定的

在前几章中,我们分别从面向对象和函数式编程的角度探讨了 interfacetype 的使用场景。现在,让我们通过具体的项目实例,看看在真实开发中应该如何做出选择。

CLI 工具:用 type CliCommand

在构建命令行工具时,我们通常会定义命令的结构。由于命令对象通常是数据载体,不会被类实现,因此更适合使用 type

typescript
type CliCommand = {
  name: string;
  description: string;
  aliases?: string[];
  builder?: (yargs: Argv) => Argv;
  handler: (argv: any) => Promise<void>;
};

// 具体命令实现
const buildCommand: CliCommand = {
  name: 'build',
  description: 'Build the project',
  builder: (yargs) => yargs.option('watch', {
    alias: 'w',
    type: 'boolean',
    description: 'Watch for changes'
  }),
  handler: async (argv) => {
    // 构建逻辑
  }
};

这里使用 type 更合适,因为我们不需要定义契约让类去实现,而只是描述数据结构。

React 组件:用 type Props

在 React 开发中,组件的 Props 和 State 通常使用 type 来定义:

typescript
import React, { useState } from 'react';

type UserCardProps = {
  user: {
    id: number;
    name: string;
    email: string;
  };
  actions?: {
    onEdit?: (id: number) => void;
    onDelete?: (id: number) => void;
  };
};

type UserCardState = {
  expanded: boolean;
};

const UserCard: React.FC<UserCardProps> = ({ user, actions }) => {
  const [state, setState] = useState<UserCardState>({ expanded: false });
  
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      {state.expanded && <p>{user.email}</p>}
      <button onClick={() => setState({ expanded: !state.expanded })}>
        Toggle
      </button>
    </div>
  );
};

React 社区普遍使用 type 来定义组件的 Props 和 State,这已经成为一种约定俗成的做法。

Node.js 服务:用 interface UserService

在构建后端服务时,特别是使用依赖注入或面向对象架构时,更适合使用 interface 来定义服务契约:

typescript
interface UserService {
  findById(id: number): Promise<User>;
  findByEmail(email: string): Promise<User | null>;
  create(userData: CreateUserDto): Promise<User>;
  update(id: number, userData: UpdateUserDto): Promise<User>;
}

interface UserRepository {
  findById(id: number): Promise<User>;
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<User>;
}

class DatabaseUserService implements UserService {
  constructor(private readonly userRepository: UserRepository) {}
  
  async findById(id: number): Promise<User> {
    return this.userRepository.findById(id);
  }
  
  async findByEmail(email: string): Promise<User | null> {
    return this.userRepository.findByEmail(email);
  }
  
  async create(userData: CreateUserDto): Promise<User> {
    const user = new User();
    user.name = userData.name;
    user.email = userData.email;
    return this.userRepository.save(user);
  }
  
  async update(id: number, userData: UpdateUserDto): Promise<User> {
    const user = await this.userRepository.findById(id);
    user.name = userData.name;
    user.email = userData.email;
    return this.userRepository.save(user);
  }
}

在这种场景下,interface 提供了清晰的服务契约,便于测试替身(Test Double)的实现和依赖注入。

领域模型:混合使用,interface User + type Events

在复杂的领域驱动设计(DDD)中,我们往往会混合使用 interfacetype

typescript
// 领域实体使用 interface,因为它可能有多种实现
interface User {
  id: number;
  name: string;
  email: string;
  validate(): boolean;
}

// 领域事件使用 type,因为它是数据载体
type UserCreatedEvent = {
  type: 'UserCreated';
  userId: number;
  timestamp: Date;
};

type UserUpdatedEvent = {
  type: 'UserUpdated';
  userId: number;
  changes: Partial<User>;
  timestamp: Date;
};

type UserEvent = UserCreatedEvent | UserUpdatedEvent;

// 值对象使用 type,因为它们通常是不可变的数据结构
type Email = string & { readonly brand: unique symbol };

function createEmail(email: string): Email {
  if (!email.includes('@')) {
    throw new Error('Invalid email');
  }
  return email as Email;
}

这种混合使用的方式充分发挥了两种类型定义的优势:

  • interface 用于定义可能有多种实现的实体契约
  • type 用于定义数据载体和不可变的值对象

小结

在本章中,我们通过具体的项目实例展示了如何在实际开发中做出选择:

  1. CLI 工具使用 type 定义命令结构
  2. React 组件使用 type 定义 Props 和 State
  3. Node.js 服务使用 interface 定义服务契约
  4. 复杂领域模型中混合使用两者

关键是要根据具体的使用场景和设计意图来选择,而不是盲目遵循某个固定的规则。

下一章,我们将探讨现代 TypeScript 发展趋势,看看 type 是如何逐渐成为主流的。


思考题: 在你的项目中,有没有遇到过难以决定使用 interface 还是 type 的情况?最终是如何解决的?