各位观众老爷,大家好!今天咱们不聊风花雪月,聊聊怎么用 TypeScript 里面的“代数数据类型”(Algebraic Data Types, ADTs)玩转“类型驱动开发”(Type-Driven Development, TDD)。别被这些高大上的名词吓跑,其实都是纸老虎,看完你就知道,它们能让你的代码更健壮、更易维护,而且,更不容易被老板骂!
咱们先来个热身,搞清楚几个概念:
1. 啥是代数数据类型 (ADTs)?
ADTs,简单来说,就是用组合(Sum type)和乘积(Product type)两种方式构建出来的数据类型。
-
Sum Type (联合类型): 想象一下,你有一个变量,它可以是A类型,也可以是B类型,还可以是C类型… 这就是Sum Type。TypeScript里面的
union
类型就实现了这个功能。 就像一个东西可能是猫,也可能是狗,也可能是鸟
。 -
Product Type (乘积类型): 这个更好理解,就是把几个类型“打包”在一起,形成一个新的类型。TypeScript 里面的
interface
和type
都可以实现这个。 就像一个东西既有颜色,又有大小,还有重量
。
2. 啥是类型驱动开发 (TDD)?
TDD 就是先定义好你的类型,然后围绕这些类型来写代码。与其先写代码再考虑类型,不如反过来,先用类型把你的需求框住,然后往里面填代码。 就像 先画好蓝图,再盖房子
。
3. TypeScript 和 ADTs 的关系?
TypeScript 提供了强大的类型系统,完美支持 ADTs,让我们可以用类型来表达复杂的业务逻辑。
OK,热身完毕,咱们进入正题。
一、Sum Type (联合类型) 的妙用
Sum Type 最常见的应用场景就是处理不同的状态。比如,一个异步请求可能有三种状态:Loading
(加载中)、Success
(成功)和 Error
(失败)。
type LoadingState = {
status: 'loading';
};
type SuccessState<T> = {
status: 'success';
data: T;
};
type ErrorState = {
status: 'error';
error: string;
};
type RequestState<T> = LoadingState | SuccessState<T> | ErrorState;
// 使用例子
const loading: RequestState<number> = { status: 'loading' };
const success: RequestState<number> = { status: 'success', data: 123 };
const error: RequestState<number> = { status: 'error', error: 'Something went wrong' };
function renderData<T>(state: RequestState<T>): string {
switch (state.status) {
case 'loading':
return 'Loading...';
case 'success':
return `Data: ${state.data}`;
case 'error':
return `Error: ${state.error}`;
default:
// TypeScript 会提示这里需要处理所有可能的 `status`,避免遗漏
const _exhaustiveCheck: never = state;
return _exhaustiveCheck; // 这行代码永远不会执行,但是能保证类型安全
}
}
console.log(renderData(loading)); // Output: Loading...
console.log(renderData(success)); // Output: Data: 123
console.log(renderData(error)); // Output: Error: Something went wrong
代码解释:
- 我们定义了三种状态的类型:
LoadingState
、SuccessState
和ErrorState
。 RequestState<T>
是一个 Sum Type,它可能是LoadingState
,也可能是SuccessState<T>
,也可能是ErrorState
。renderData
函数根据state.status
来渲染不同的内容。_exhaustiveCheck
是关键! 如果你在switch
语句中漏掉了任何一种status
,TypeScript 就会报错,告诉你需要处理所有可能的类型。这保证了类型安全,避免了运行时错误。
Sum Type 的优势:
- 类型安全: TypeScript 编译器会检查你是否处理了所有可能的类型。
- 可读性强: 代码清晰地表达了不同的状态。
- 易于维护: 当增加新的状态时,TypeScript 会提示你修改所有相关的代码。
真实案例:
假设你要开发一个电商网站,商品的展示方式有两种:List
(列表) 和 Grid
(网格)。
type ListView = {
type: 'list';
items: Product[];
}
type GridView = {
type: 'grid';
items: Product[];
columns: number;
}
type ProductView = ListView | GridView;
interface Product {
id: string;
name: string;
price: number;
}
function renderProductView(view: ProductView): string {
switch (view.type) {
case 'list':
return `<ul>${view.items.map(item => `<li>${item.name} - $${item.price}</li>`).join('')}</ul>`;
case 'grid':
// 假设有一个辅助函数可以将商品排列成网格
return `<div>Grid View with ${view.columns} columns</div>`; // 简化版
default:
const _exhaustiveCheck: never = view;
return _exhaustiveCheck;
}
}
const products: Product[] = [
{ id: '1', name: 'Laptop', price: 1200 },
{ id: '2', name: 'Mouse', price: 25 },
];
const listView: ListView = { type: 'list', items: products };
const gridView: GridView = { type: 'grid', items: products, columns: 3 };
console.log(renderProductView(listView));
console.log(renderProductView(gridView));
二、Product Type (乘积类型) 的精髓
Product Type 就是把几个类型“打包”在一起,形成一个新的类型。TypeScript 里面的 interface
和 type
都可以实现这个。
简单例子:
interface Point {
x: number;
y: number;
}
const myPoint: Point = {
x: 10,
y: 20,
};
console.log(myPoint.x, myPoint.y);
Product Type 的优势:
- 组织数据: 把相关的数据组织在一起,方便管理。
- 代码复用: 定义好的类型可以在多个地方使用。
- 类型约束: 确保数据的结构符合预期。
真实案例:
假设你要开发一个用户管理系统,需要存储用户的各种信息:姓名、年龄、地址、邮箱等等。
interface User {
id: string;
name: string;
age: number;
address: {
street: string;
city: string;
zipCode: string;
};
email: string;
phoneNumber?: string; // 可选属性
}
const myUser: User = {
id: '123',
name: '张三',
age: 30,
address: {
street: 'XX路',
city: '北京',
zipCode: '100000',
},
email: '[email protected]',
};
console.log(myUser.name, myUser.address.city);
三、Sum Type + Product Type = 无敌
真正的威力在于把 Sum Type 和 Product Type 结合起来使用。
案例:处理 API 响应
假设你要调用一个 API,API 可能会返回成功的数据,也可能会返回错误信息。
interface ApiResponseSuccess<T> {
status: 'success';
data: T;
}
interface ApiResponseError {
status: 'error';
message: string;
}
type ApiResponse<T> = ApiResponseSuccess<T> | ApiResponseError;
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
try {
const response = await fetch(url);
const data = await response.json() as T; // 假设 API 返回 JSON
return { status: 'success', data };
} catch (error: any) {
return { status: 'error', message: error.message };
}
}
// 使用例子
async function processData() {
interface User {
id: number;
name: string;
}
const response = await fetchData<User[]>('https://example.com/users');
if (response.status === 'success') {
console.log('Users:', response.data);
} else {
console.error('Error fetching users:', response.message);
}
}
processData();
代码解释:
ApiResponseSuccess<T>
和ApiResponseError
是 Product Type,分别表示成功和失败的响应。ApiResponse<T>
是一个 Sum Type,它可以是ApiResponseSuccess<T>
,也可以是ApiResponseError
。fetchData
函数返回一个Promise<ApiResponse<T>>
,表示异步请求的结果。- 在
processData
函数中,我们根据response.status
来处理不同的情况。
这种方式的优点:
- 类型安全: TypeScript 编译器会检查你是否处理了所有可能的响应状态。
- 清晰的错误处理: 你可以明确地知道 API 可能会返回哪些错误,并进行相应的处理。
- 代码可读性高: 代码清晰地表达了 API 响应的结构。
四、Discriminated Unions (可辨识联合)
上面例子里的 ApiResponse
就是一个 Discriminated Union。 Discriminated Union 是一种特殊的 Sum Type,它包含一个 discriminator
属性,用于区分不同的类型。
Discriminated Union 的特点:
- 每个类型都有一个共同的属性(discriminator),这个属性的值不同,用于区分不同的类型。
- TypeScript 可以根据 discriminator 的值来推断具体的类型。
Discriminated Union 的优势:
- 类型推断: TypeScript 可以自动推断类型,减少代码量。
- 代码简洁: 可以更简洁地处理不同的类型。
案例:处理不同类型的表单字段
假设你要开发一个动态表单,表单字段的类型可能有很多种:文本框、下拉框、复选框等等。
interface TextField {
type: 'text';
label: string;
placeholder?: string;
}
interface SelectField {
type: 'select';
label: string;
options: { value: string; label: string }[];
}
interface CheckboxField {
type: 'checkbox';
label: string;
defaultValue?: boolean;
}
type FormField = TextField | SelectField | CheckboxField;
function renderFormField(field: FormField): string {
switch (field.type) {
case 'text':
return `<input type="text" placeholder="${field.placeholder || ''}" />`;
case 'select':
return `<select>${field.options.map(option => `<option value="${option.value}">${option.label}</option>`).join('')}</select>`;
case 'checkbox':
return `<input type="checkbox" ${field.defaultValue ? 'checked' : ''} />`;
default:
const _exhaustiveCheck: never = field;
return _exhaustiveCheck;
}
}
const formFields: FormField[] = [
{ type: 'text', label: 'Name', placeholder: 'Enter your name' },
{ type: 'select', label: 'Country', options: [{ value: 'US', label: 'United States' }, { value: 'CA', label: 'Canada' }] },
{ type: 'checkbox', label: 'Agree to terms', defaultValue: true },
];
const formHtml = formFields.map(renderFormField).join('<br>');
console.log(formHtml);
代码解释:
TextField
、SelectField
和CheckboxField
都有一个共同的属性type
,它的值不同,用于区分不同的类型。FormField
是一个 Discriminated Union,它可以是TextField
,也可以是SelectField
,也可以是CheckboxField
。- 在
renderFormField
函数中,TypeScript 可以根据field.type
的值来推断具体的类型。
五、类型驱动开发 (TDD) 的实践
说了这么多,咱们来聊聊怎么把 ADTs 应用到类型驱动开发中。
步骤:
- 定义类型: 首先,根据你的需求,定义好你的类型。 思考你的数据有哪些不同的状态,需要哪些属性。
- 编写函数签名: 根据你的类型,编写函数的签名。 函数签名包括函数的参数类型和返回值类型。
- 编写测试用例: 根据你的类型和函数签名,编写测试用例。 测试用例应该覆盖所有可能的输入和输出。
- 实现函数: 根据测试用例,实现函数。 确保你的代码符合类型定义,并通过所有测试用例。
案例:实现一个简单的计算器
- 定义类型:
type Operator = 'add' | 'subtract' | 'multiply' | 'divide';
interface Calculation {
operator: Operator;
operand1: number;
operand2: number;
}
type CalculationResult =
| { status: 'success'; result: number }
| { status: 'error'; error: string };
- 编写函数签名:
function calculate(calculation: Calculation): CalculationResult {
// ... 待实现
}
- 编写测试用例:
// 单元测试框架,例如 Jest, Mocha 等,这里只是伪代码
test('add two numbers', () => {
const result = calculate({ operator: 'add', operand1: 1, operand2: 2 });
expect(result).toEqual({ status: 'success', result: 3 });
});
test('divide by zero', () => {
const result = calculate({ operator: 'divide', operand1: 1, operand2: 0 });
expect(result).toEqual({ status: 'error', error: 'Cannot divide by zero' });
});
// ... 其他测试用例
- 实现函数:
function calculate(calculation: Calculation): CalculationResult {
switch (calculation.operator) {
case 'add':
return { status: 'success', result: calculation.operand1 + calculation.operand2 };
case 'subtract':
return { status: 'success', result: calculation.operand1 - calculation.operand2 };
case 'multiply':
return { status: 'success', result: calculation.operand1 * calculation.operand2 };
case 'divide':
if (calculation.operand2 === 0) {
return { status: 'error', error: 'Cannot divide by zero' };
}
return { status: 'success', result: calculation.operand1 / calculation.operand2 };
default:
const _exhaustiveCheck: never = calculation.operator;
return _exhaustiveCheck;
}
}
TDD 的优势:
- 代码质量高: 测试用例驱动开发,确保代码的正确性。
- 易于维护: 类型定义和测试用例可以作为代码的文档,方便维护。
- 减少 Bug: 在开发过程中发现 Bug,避免在生产环境中出现 Bug。
六、总结
今天我们聊了 TypeScript 里面的 ADTs 和 TDD,希望大家对它们有了一个更清晰的认识。
核心要点:
- Sum Type (联合类型): 处理不同的状态。
- Product Type (乘积类型): 组织数据。
- Discriminated Union (可辨识联合): 简化类型推断。
- 类型驱动开发 (TDD): 先定义类型,再写代码。
ADTs + TDD = 代码质量的保障
希望大家在实际开发中多多使用 ADTs 和 TDD,写出更健壮、更易维护的代码!
最后,送给大家一句名言:
"Types are your friends, not your enemies."
希望大家喜欢今天的分享! 感谢各位观众老爷!