第 4 章:FP 之道 —— 何时用 type
在前面的章节中,我们探讨了面向对象编程中如何使用 interface。现在,让我们转换视角,来看看在函数式编程(Functional Programming)中,何时更适合使用 type。
代数数据类型(ADT):Result<T> = Success | Failure
函数式编程的一个重要概念是代数数据类型(Algebraic Data Types,ADT)。使用 type 可以很好地表达这类数据结构:
// 定义一个 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)结合使用,可以轻松实现这一点:
// 定义基本的类型片段
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 等:
// 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 更加常见:
// 函数组件 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 定义状态和动作:
// 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 定义配置选项:
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 的各种场景:
- 使用联合类型表达代数数据类型(ADT)
- 通过交叉类型实现组合优于继承
- 定义高阶类型操作符
- 在 React、状态管理和 CLI 工具中的实际应用
type 是函数式编程思想在 TypeScript 类型系统中的体现,它强调不可变性、组合性和类型安全。
下一章,我们将通过真实项目的案例,进一步探讨如何在实际开发中做出选择。
思考题: 你在项目中有使用过类似 Result 这样的 ADT 吗?你觉得这种方式相比传统的 try/catch 异常处理有什么优势和劣势?