第 5 章:真实项目中的选择 —— 我们是如何决定的
在前几章中,我们分别从面向对象和函数式编程的角度探讨了 interface 和 type 的使用场景。现在,让我们通过具体的项目实例,看看在真实开发中应该如何做出选择。
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)中,我们往往会混合使用 interface 和 type:
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用于定义数据载体和不可变的值对象
小结
在本章中,我们通过具体的项目实例展示了如何在实际开发中做出选择:
- CLI 工具使用
type定义命令结构 - React 组件使用
type定义 Props 和 State - Node.js 服务使用
interface定义服务契约 - 复杂领域模型中混合使用两者
关键是要根据具体的使用场景和设计意图来选择,而不是盲目遵循某个固定的规则。
下一章,我们将探讨现代 TypeScript 发展趋势,看看 type 是如何逐渐成为主流的。
思考题: 在你的项目中,有没有遇到过难以决定使用 interface 还是 type 的情况?最终是如何解决的?