Skip to content

第 4 章:FP 之道 —— 何时用 type

在前面的章节中,我们探讨了面向对象编程中如何使用 interface。现在,让我们转换视角,来看看在函数式编程(Functional Programming)中,何时更适合使用 type

代数数据类型(ADT):Result<T> = Success | Failure

函数式编程的一个重要概念是代数数据类型(Algebraic Data Types,ADT)。使用 type 可以很好地表达这类数据结构:

typescript
// 定义一个 Result 类型,表示操作可能成功也可能失败
type Result<T> = Success<T> | Failure;

interface Success<T> {
  kind: 'success';
  value: T;
}

interface Failure {
  kind: 'failure';
  error: Error;
}

// 使用示例
function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { kind: 'failure', error: new Error('Division by zero') };
  }
  return { kind: 'success', value: a / b };
}

// 模式匹配处理结果
function handleResult(result: Result<number>) {
  switch (result.kind) {
    case 'success':
      console.log(`Result: ${result.value}`);
      break;
    case 'failure':
      console.error(`Error: ${result.error.message}`);
      break;
  }
}

这种模式在函数式编程语言(如 Haskell、Rust)中非常常见,使用 type 能够完美地模拟这种行为。

组合优于继承:User = Name & Email & Timestamp

在函数式编程中,我们更倾向于通过组合而不是继承来构建复杂类型。type 与交叉类型(Intersection Types)结合使用,可以轻松实现这一点:

typescript
// 定义基本的类型片段
type Name = {
  firstName: string;
  lastName: string;
};

type Email = {
  email: string;
};

type Timestamp = {
  createdAt: Date;
  updatedAt: Date;
};

// 通过组合创建更复杂的类型
type User = Name & Email & Timestamp;

// 也可以选择性地组合
type PublicUserInfo = Name & Email;

这种方式比继承更加灵活,因为你可以自由地组合任何你想要的属性集合,而不需要考虑复杂的继承层次结构。

高阶类型:type Pick<T, K> = ...

TypeScript 内置的一些高阶类型都是使用 type 定义的,比如 Pick、Omit、Partial 等:

typescript
// Pick 的简化实现
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

// Omit 的简化实现
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// Partial 的简化实现
type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

// 使用示例
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// 只选择 name 和 email 属性
type UserProfile = MyPick<User, 'name' | 'email'>;

// 忽略 password 属性
type SafeUser = MyOmit<User, 'password'>;

// 所有属性都变为可选
type PartialUser = MyPartial<User>;

这些类型操作符体现了函数式编程的思想:通过组合简单的类型构造函数来创建复杂的类型。

适用场景:React props、状态管理、CLI 配置

在实际项目中,以下场景更适合使用 type

1. React 组件 Props

在 React 开发中,使用 type 定义组件 props 更加常见:

typescript
// 函数组件 props
type ButtonProps = {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary';
  onClick: () => void;
};

const Button: React.FC<ButtonProps> = ({ children, variant = 'primary', onClick }) => {
  // 组件实现
  return (
    <button className={`btn btn-${variant}`} onClick={onClick}>
      {children}
    </button>
  );
};

2. 状态管理

在状态管理中,使用 type 定义状态和动作:

typescript
// Redux 状态定义
type AppState = {
  user: User | null;
  loading: boolean;
  error: string | null;
};

// Redux 动作类型
type Action = 
  | { type: 'LOGIN_START' }
  | { type: 'LOGIN_SUCCESS', payload: User }
  | { type: 'LOGIN_FAILURE', payload: string };

3. CLI 配置

在构建命令行工具时,使用 type 定义配置选项:

typescript
type CliOptions = {
  input: string;
  output: string;
  verbose: boolean;
  format: 'json' | 'xml' | 'csv';
};

type CliCommand = {
  name: string;
  description: string;
  options: CliOptions;
  handler: (options: CliOptions) => Promise<void>;
};

小结

在本章中,我们探讨了在函数式编程中使用 type 的各种场景:

  1. 使用联合类型表达代数数据类型(ADT)
  2. 通过交叉类型实现组合优于继承
  3. 定义高阶类型操作符
  4. 在 React、状态管理和 CLI 工具中的实际应用

type 是函数式编程思想在 TypeScript 类型系统中的体现,它强调不可变性、组合性和类型安全。

下一章,我们将通过真实项目的案例,进一步探讨如何在实际开发中做出选择。


思考题: 你在项目中有使用过类似 Result 这样的 ADT 吗?你觉得这种方式相比传统的 try/catch 异常处理有什么优势和劣势?