关于 Practical Data Transformations
为 TypeScript 开发者讲解日常数据转换,并以命令式与函数式写法对照展示数组、对象、数据归一化、分组聚合和空值安全访问。
主要功能
- 01转换、筛选与归约数组
- 02选择、合并及不可变更新对象
- 03将 API 响应归一化为应用状态
- 04按键分组并聚合数据
- 05安全访问嵌套值
- 06比较命令式与函数式方案
适用场景
- 01筛选并排序符合条件的用户
- 02将 API 数据整理为 UI 所需格式
- 03按客户或状态汇总订单
- 04合并配置并更新嵌套状态
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ whatiskadudoing/ fp-ts-skills --skill practical-data-transformations安装后在 Claude Code 中输入/practical-data-transformations确认已加载。
详细介绍
实用数据转换
本技能涵盖日常数据转换工作:处理数组、重塑对象、规范化 API 响应、对数据分组,以及安全地访问嵌套值。每个部分都会先展示命令式方法,再展示函数式等效方法,并如实评估各自适用的场景。
目录
1. 数组操作
数组操作是数据转换的基础。让我们用表达力更强、可链式调用的操作来替代冗长的循环。
Map:转换每个元素
任务:将价格数组从美分转换为美元。
命令式方法
const pricesInCents = [999, 1499, 2999, 4999];
function convertToDollars(prices: number[]): number[] {
const result: number[] = [];
for (let i = 0; i < prices.length; i++) {
result.push(prices[i] / 100);
}
return result;
}
const dollars = convertToDollars(pricesInCents);
// [9.99, 14.99, 29.99, 49.99]
函数式方法
const pricesInCents = [999, 1499, 2999, 4999];
const toDollars = (cents: number): number => cents / 100;
const dollars = pricesInCents.map(toDollars);
// [9.99, 14.99, 29.99, 49.99]
函数式方法为何更好:意图一目了然。map 表示“转换每个元素”。转换逻辑(toDollars)有明确命名,且可重复使用。无需管理索引,也不必手动构建数组。
Filter:保留匹配项
任务:从列表中获取所有活跃用户。
命令式方法
interface User {
id: string;
name: string;
isActive: boolean;
}
function getActiveUsers(users: User[]): User[] {
const result: User[] = [];
for (const user of users) {
if (user.isActive) {
result.push(user);
}
}
return result;
}
函数式方法
const isActive = (user: User): boolean => user.isActive;
const activeUsers = users.filter(isActive);
// Or inline for simple predicates
const activeUsers = users.filter(user => user.isActive);
函数式方法为何更好:谓词(isActive)与迭代逻辑分离。你可以独立复用、测试和组合谓词。
Reduce:累积生成新值
任务:计算购物车中商品的总价。
命令式方法
interface CartItem {
name: string;
price: number;
quantity: number;
}
function calculateTotal(items: CartItem[]): number {
let total = 0;
for (const item of items) {
total += item.price * item.quantity;
}
return total;
}
函数式方法
const calculateTotal = (items: CartItem[]): number =>
items.reduce(
(total, item) => total + item.price * item.quantity,
0
);
// Or break out the line total calculation
const lineTotal = (item: CartItem): number => item.price * item.quantity;
const calculateTotal = (items: CartItem[]): number =>
items.map(lineTotal).reduce((a, b) => a + b, 0);
客观评价:对于简单求和,命令式循环实际上相当易读。当你需要将累积与其他转换组合,或归约逻辑足够复杂、适合单独命名时,函数式方法才更有优势。
链式调用:组合操作
任务:获取所有活跃高级用户的姓名,并按字母顺序排序。
命令式方法
interface User {
id: string;
name: string;
isActive: boolean;
tier: 'free' | 'premium';
}
function getActivePremiumNames(users: User[]): string[] {
const result: string[] = [];
for (const user of users) {
if (user.isActive && user.tier === 'premium') {
result.push(user.name);
}
}
result.sort((a, b) => a.localeCompare(b));
return result;
}
函数式方法
const getActivePremiumNames = (users: User[]): string[] =>
users
.filter(user => user.isActive)
.filter(user => user.tier === 'premium')
.map(user => user.name)
.sort((a, b) => a.localeCompare(b));
// Or with named predicates for reuse
const isActive = (user: User): boolean => user.isActive;
const isPremium = (user: User): boolean => user.tier === 'premium';
const getName = (user: User): string => user.name;
const alphabetically = (a: string, b: string): number => a.localeCompare(b);
const getActivePremiumNames = (users: User[]): string[] =>
users
.filter(isActive)
.filter(isPremium)
.map(getName)
.sort(alphabetically);
函数式方法为何更好:链中的每一步都只负责一项工作。你可以将转换读作一系列步骤:“筛选活跃用户、筛选高级用户、获取姓名、排序。”添加或删除步骤都很简单。
使用 fp-ts Array 模块
fp-ts 提供了更多数组实用工具,支持更好的组合:
import * as A from 'fp-ts/Array';
import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';
// Safe head (first element)
const first = pipe(
[1, 2, 3],
A.head
); // Some(1)
const firstOfEmpty = pipe(
[] as number[],
A.head
); // None
// Safe lookup by index
const third = pipe(
['a', 'b', 'c', 'd'],
A.lookup(2)
); // Some('c')
// Find with predicate
const found = pipe(
users,
A.findFirst(user => user.id === 'abc123')
); // Option<User>
// Partition into two groups
const [inactive, active] = pipe(
users,
A.partition(user => user.isActive)
);
// Take first N elements
const topThree = pipe(
sortedScores,
A.takeLeft(3)
);
// Unique values
const uniqueTags = pipe(
allTags,
A.uniq({ equals: (a, b) => a === b })
);
2. 对象转换
对象经常需要重塑:选取字段、移除敏感数据、合并设置,以及更新嵌套值。
Pick:选取特定字段
任务:仅提取用户对象中的公开字段。
命令式方法
interface User {
id: string;
name: string;
email: string;
passwordHash: string;
internalNotes: string;
}
function getPublicUser(user: User): { id: string; name: string; email: string } {
return {
id: user.id,
name: user.name,
email: user.email,
};
}
函数式方法
// Generic pick utility
const pick = <T extends object, K extends keyof T>(
keys: K[]
) => (obj: T): Pick<T, K> =>
keys.reduce(
(result, key) => {
result[key] = obj[key];
return result;
},
{} as Pick<T, K>
);
const getPublicUser = pick<User, 'id' | 'name' | 'email'>(['id', 'name', 'email']);
const publicUser = getPublicUser(user);
函数式方法为何更好:pick 实用工具可在整个代码库中复用。类型安全可以确保你只能选取存在的键。
Omit:移除特定字段
任务:记录日志前移除敏感字段。
命令式方法
function sanitizeForLogging(user: User): Omit<User, 'passwordHash' | 'internalNotes'> {
const { passwordHash, internalNotes, ...safe } = user;
return safe;
}
函数式方法
// Generic omit utility
const omit = <T extends object, K extends keyof T>(
keys: K[]
) => (obj: T): Omit<T, K> => {
const result = { ...obj };
for (const key of keys) {
delete result[key];
}
return result as Omit<T, K>;
};
const sanitizeForLogging = omit<User, 'passwordHash' | 'internalNotes'>([
'passwordHash',
'internalNotes',
]);
客观评价:对于偶尔一次的字段移除,解构(命令式方法)完全够用,而且很易读。若需要执行许多此类转换,或需要将它们组合起来,函数式 omit 实用工具会更有价值。
Merge:合并对象
任务:将用户设置与默认设置合并。
命令式方法
interface Settings {
theme: 'light' | 'dark';
fontSize: number;
notifications: boolean;
language: string;
}
function mergeSettings(
defaults: Settings,
userSettings: Partial<Settings>
): Settings {
return {
theme: userSettings.theme !== undefined ? userSettings.theme : defaults.theme,
fontSize: userSettings.fontSize !== undefined ? userSettings.fontSize : defaults.fontSize,
notifications: userSettings.notifications !== undefined
? userSettings.notifications
: defaults.notifications,
language: userSettings.language !== undefined ? userSettings.language : defaults.language,
};
}
函数式方法
const mergeSettings = (
defaults: Settings,
userSettings: Partial<Settings>
): Settings => ({
...defaults,
...userSettings,
});
// Usage
const defaults: Settings = {
theme: 'light',
fontSize: 14,
notifications: true,
language: 'en',
};
const userPrefs: Partial<Settings> = {
theme: 'dark',
fontSize: 16,
};
const finalSettings = mergeSettings(defaults, userPrefs);
// { theme: 'dark', fontSize: 16, notifications: true, language: 'en' }
函数式方法为何更好:扩展语法简洁,并且可以处理任意数量的键。后面的扩展会覆盖前面的值,因此可自然实现“默认值加覆盖项”的行为。
Deep Merge:合并嵌套对象
任务:合并嵌套的配置对象。
命令式方法
interface Config {
api: {
baseUrl: string;
timeout: number;
retries: number;
};
ui: {
theme: string;
animations: boolean;
};
}
function deepMerge(
target: Config,
source: Partial<Config>
): Config {
const result = { ...target };
if (source.api) {
result.api = { ...target.api, ...source.api };
}
if (source.ui) {
result.ui = { ...target.ui, ...source.ui };
}
return result;
}
函数式方法
// Generic deep merge for one level of nesting
const deepMerge = <T extends Record<string, object>>(
target: T,
source: { [K in keyof T]?: Partial<T[K]> }
): T => {
const result = { ...target };
for (const key of Object.keys(source) as Array<keyof T>) {
if (source[key] !== undefined) {
result[key] = { ...target[key], ...source[key] };
}
}
return result;
};
// Usage
const defaultConfig: Config = {
api: { baseUrl: 'https://api.example.com', timeout: 5000, retries: 3 },
ui: { theme: 'light', animations: true },
};
const customConfig = deepMerge(defaultConfig, {
api: { timeout: 10000 },
ui: { theme: 'dark' },
});
// api.baseUrl preserved, api.timeout overridden
// ui.theme overridden, ui.animations preserved
不可变更新:更改嵌套值
任务:在不进行修改的情况下更新深层嵌套值。
命令式(会修改数据)方法
interface State {
user: {
profile: {
settings: {
theme: string;
};
};
};
}
function updateTheme(state: State, newTheme: string): void {
state.user.profile.settings.theme = newTheme; // Mutation!
}
函数式(不可变)方法
// Manual spread nesting
const updateTheme = (state: State, newTheme: string): State => ({
...state,
user: {
...state.user,
profile: {
...state.user.profile,
settings: {
...state.user.profile.settings,
theme: newTheme,
},
},
},
});
// With a lens-like helper
const updatePath = <T, V>(
obj: T,
path: string[],
value: V
): T => {
if (path.length === 0) return value as unknown as T;
const [head, ...rest] = path;
return {
...obj,
[head]: updatePath((obj as Record<string, unknown>)[head], rest, value),
} as T;
};
const newState = updatePath(state, ['user', 'profile', 'settings', 'theme'], 'dark');
坦诚评价:嵌套使用展开语法会比较冗长,但表达明确。对于深层嵌套更新,可以考虑使用 immer 或 fp-ts lens 等库。函数式方法的冗长是保持不可变性的代价。
3. 数据规范化
API 响应很少符合应用所需的数据结构。规范化会将嵌套的非规范化数据转换为扁平的索引结构。
将 API 响应转换为应用状态
任务:将嵌套的 API 响应转换为规范化状态。
API 响应(实际获取的数据)
interface ApiResponse {
orders: Array<{
id: string;
customerId: string;
customerName: string;
customerEmail: string;
items: Array<{
productId: string;
productName: string;
quantity: number;
price: number;
}>;
total: number;
status: string;
}>;
}
应用状态(所需的数据)
interface NormalizedState {
orders: {
byId: Record<string, Order>;
allIds: string[];
};
customers: {
byId: Record<string, Customer>;
allIds: string[];
};
products: {
byId: Record<string, Product>;
allIds: string[];
};
}
interface Order {
id: string;
customerId: string;
itemIds: string[];
total: number;
status: string;
}
interface Customer {
id: string;
name: string;
email: string;
}
interface Product {
id: string;
name: string;
price: number;
}
命令式方法
function normalizeApiResponse(response: ApiResponse): NormalizedState {
const state: NormalizedState = {
orders: { byId: {}, allIds: [] },
customers: { byId: {}, allIds: [] },
products: { byId: {}, allIds: [] },
};
for (const order of response.orders) {
// Extract customer
if (!state.customers.byId[order.customerId]) {
state.customers.byId[order.customerId] = {
id: order.customerId,
name: order.customerName,
email: order.customerEmail,
};
state.customers.allIds.push(order.customerId);
}
// Extract products and build item IDs
const itemIds: string[] = [];
for (const item of order.items) {
if (!state.products.byId[item.productId]) {
state.products.byId[item.productId] = {
id: item.productId,
name: item.productName,
price: item.price,
};
state.products.allIds.push(item.productId);
}
itemIds.push(item.productId);
}
// Add normalized order
state.orders.byId[order.id] = {
id: order.id,
customerId: order.customerId,
itemIds,
total: order.total,
status: order.status,
};
state.orders.allIds.push(order.id);
}
return state;
}
函数式方法
import { pipe } from 'fp-ts/function';
import * as A from 'fp-ts/Array';
import * as R from 'fp-ts/Record';
// Helper to create normalized collection
interface NormalizedCollection<T extends { id: string }> {
byId: Record<string, T>;
allIds: string[];
}
const createNormalizedCollection = <T extends { id: string }>(
items: T[]
): NormalizedCollection<T> => ({
byId: pipe(
items,
A.reduce({} as Record<string, T>, (acc, item) => ({
...acc,
[item.id]: item,
}))
),
allIds: items.map(item => item.id),
});
// Extract entities
const extractCustomers = (orders: ApiResponse['orders']): Customer[] =>
pipe(
orders,
A.map(order => ({
id: order.customerId,
name: order.customerName,
email: order.customerEmail,
})),
A.uniq({ equals: (a, b) => a.id === b.id })
);
const extractProducts = (orders: ApiResponse['orders']): Product[] =>
pipe(
orders,
A.flatMap(order => order.items),
A.map(item => ({
id: item.productId,
name: item.productName,
price: item.price,
})),
A.uniq({ equals: (a, b) => a.id === b.id })
);
const extractOrders = (orders: ApiResponse['orders']): Order[] =>
orders.map(order => ({
id: order.id,
customerId: order.customerId,
itemIds: order.items.map(item => item.productId),
total: order.total,
status: order.status,
}));
// Compose into final normalization
const normalizeApiResponse = (response: ApiResponse): NormalizedState => ({
orders: createNormalizedCollection(extractOrders(response.orders)),
customers: createNormalizedCollection(extractCustomers(response.orders)),
products: createNormalizedCollection(extractProducts(response.orders)),
});
函数式方法在此处更好的原因:每项提取操作彼此独立,且可单独测试。createNormalizedCollection 辅助函数可以重复使用。添加新的实体类型时,只需添加一个新的提取函数。
将 API 响应转换为可直接用于 UI 的数据
任务:将 API 数据转换为组件所需的数据。
// API gives you this
interface ApiUser {
user_id: string;
first_name: string;
last_name: string;
email_address: string;
created_at: string; // ISO string
avatar_url: string | null;
}
// Components need this
interface DisplayUser {
id: string;
fullName: string;
email: string;
memberSince: string; // "Jan 2024"
avatarUrl: string; // With fallback
}
函数式方法
const formatDate = (isoString: string): string => {
const date = new Date(isoString);
return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
};
const DEFAULT_AVATAR = 'https://example.com/default-avatar.png';
const toDisplayUser = (apiUser: ApiUser): DisplayUser => ({
id: apiUser.user_id,
fullName: `${apiUser.first_name} ${apiUser.last_name}`,
email: apiUser.email_address,
memberSince: formatDate(apiUser.created_at),
avatarUrl: apiUser.avatar_url ?? DEFAULT_AVATAR,
});
// Transform array of users
const toDisplayUsers = (apiUsers: ApiUser[]): DisplayUser[] =>
apiUsers.map(toDisplayUser);
4. 分组与聚合
数据分组与聚合对于报告、仪表板和分析至关重要。
GroupBy:按键组织数据
任务:按客户对订单进行分组。
命令式方法
interface Order {
id: string;
customerId: string;
total: number;
date: string;
}
function groupByCustomer(orders: Order[]): Record<string, Order[]> {
const result: Record<string, Order[]> = {};
for (const order of orders) {
if (!result[order.customerId]) {
result[order.customerId] = [];
}
result[order.customerId].push(order);
}
return result;
}
函数式方法
// Generic groupBy utility
const groupBy = <T, K extends string | number>(
getKey: (item: T) => K
) => (items: T[]): Record<K, T[]> =>
items.reduce(
(groups, item) => {
const key = getKey(item);
return {
...groups,
[key]: [...(groups[key] || []), item],
};
},
{} as Record<K, T[]>
);
// Usage
const groupByCustomer = groupBy<Order, string>(order => order.customerId);
const ordersByCustomer = groupByCustomer(orders);
// Or inline
const ordersByStatus = groupBy((order: Order) => order.status)(orders);
使用 fp-ts NonEmptyArray.groupBy:
import * as NEA from 'fp-ts/NonEmptyArray';
import { pipe } from 'fp-ts/function';
// NEA.groupBy guarantees non-empty arrays in result
const ordersByCustomer = pipe(
orders as NEA.NonEmptyArray<Order>, // Must be non-empty
NEA.groupBy(order => order.customerId)
); // Record<string, NonEmptyArray<Order>>
CountBy:统计出现次数
任务:按状态统计订单数量。
命令式方法
function countByStatus(orders: Order[]): Record<string, number> {
const counts: Record<string, number> = {};
for (const order of orders) {
counts[order.status] = (counts[order.status] || 0) + 1;
}
return counts;
}
函数式方法
// Generic countBy utility
const countBy = <T, K extends string>(
getKey: (item: T) => K
) => (items: T[]): Record<K, number> =>
items.reduce(
(counts, item) => {
const key = getKey(item);
return {
...counts,
[key]: (counts[key] || 0) + 1,
};
},
{} as Record<K, number>
);
// Usage
const orderCountByStatus = countBy((order: Order) => order.status)(orders);
// { pending: 5, shipped: 12, delivered: 8 }
SumBy:聚合数值
任务:计算每个产品类别的总收入。
命令式方法
interface Sale {
productId: string;
category: string;
amount: number;
}
function sumByCategory(sales: Sale[]): Record<string, number> {
const totals: Record<string, number> = {};
for (const sale of sales) {
totals[sale.category] = (totals[sale.category] || 0) + sale.amount;
}
return totals;
}
函数式方法
// Generic sumBy utility
const sumBy = <T, K extends string>(
getKey: (item: T) => K,
getValue: (item: T) => number
) => (items: T[]): Record<K, number> =>
items.reduce(
(totals, item) => {
const key = getKey(item);
return {
...totals,
[key]: (totals[key] || 0) + getValue(item),
};
},
{} as Record<K, number>
);
// Usage
const revenueByCategory = sumBy(
(sale: Sale) => sale.category,
(sale: Sale) => sale.amount
)(sales);
// { electronics: 15000, clothing: 8500, books: 3200 }
复杂聚合示例
任务:根据包含数量和单价的订单项计算总额。
interface LineItem {
productId: string;
productName: string;
quantity: number;
unitPrice: number;
}
interface Invoice {
id: string;
lineItems: LineItem[];
taxRate: number;
}
函数式方法
const lineTotal = (item: LineItem): number =>
item.quantity * item.unitPrice;
const subtotal = (items: LineItem[]): number =>
items.reduce((sum, item) => sum + lineTotal(item), 0);
const calculateTax = (amount: number, rate: number): number =>
amount * rate;
const calculateInvoiceTotal = (invoice: Invoice): {
subtotal: number;
tax: number;
total: number;
} => {
const sub = subtotal(invoice.lineItems);
const tax = calculateTax(sub, invoice.taxRate);
return {
subtotal: sub,
tax,
total: sub + tax,
};
};
// With fp-ts pipe for clarity
import { pipe } from 'fp-ts/function';
const calculateInvoiceTotal = (invoice: Invoice) => {
const sub = pipe(
invoice.lineItems,
A.map(lineTotal),
A.reduce(0, (a, b) => a + b)
);
return {
subtotal: sub,
tax: sub * invoice.taxRate,
total: sub * (1 + invoice.taxRate),
};
};
5. 空值安全访问
别再写 if (x && x.y && x.y.z) 了。安全地访问嵌套结构,避免运行时错误。
问题
interface Config {
database?: {
connection?: {
host?: string;
port?: number;
};
pool?: {
max?: number;
};
};
features?: {
experimental?: {
enabled?: boolean;
};
};
}
命令式(冗长)方法
function getDatabaseHost(config: Config): string {
if (
config.database &&
config.database.connection &&
config.database.connection.host
) {
return config.database.connection.host;
}
return 'localhost';
}
可选链(现代 TypeScript)
const getDatabaseHost = (config: Config): string =>
config.database?.connection?.host ?? 'localhost';
坦诚评价:对于简单的访问模式,可选链(?.)非常合适。它是语言内置功能,可读性也很强。当你需要组合对可能缺失的值执行的操作时,使用 fp-ts Option。
何时改用 Option
在以下情况下使用 fp-ts Option:
- 需要对可能缺失的值进行多次链式操作
- 希望区分“缺失”和其他假值
- 正在构建转换流水线
import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';
// Safe property access that returns Option
const prop = <T, K extends keyof T>(key: K) =>
(obj: T | null | undefined): O.Option<T[K]> =>
obj != null && key in obj
? O.some(obj[key] as T[K])
: O.none;
// Chain accesses with flatMap
const getDatabaseHost = (config: Config): O.Option<string> =>
pipe(
O.some(config),
O.flatMap(prop('database')),
O.flatMap(prop('connection')),
O.flatMap(prop('host'))
);
// Extract with default
const host = pipe(
getDatabaseHost(config),
O.getOrElse(() => 'localhost')
);
安全访问数组
import * as A from 'fp-ts/Array';
import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';
// Imperative: throws if array is empty
const first = items[0]; // Could be undefined!
// Safe: returns Option
const first = A.head(items); // Option<Item>
// Get first item's name, or default
const firstName = pipe(
items,
A.head,
O.map(item => item.name),
O.getOrElse(() => 'No items')
);
// Safe lookup by index
const third = pipe(
items,
A.lookup(2),
O.map(item => item.name),
O.getOrElse(() => 'Not found')
);
安全访问记录/字典
import * as R from 'fp-ts/Record';
import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';
const users: Record<string, User> = {
'user-1': { name: 'Alice', email: '[email protected]' },
'user-2': { name: 'Bob', email: '[email protected]' },
};
// Imperative: could be undefined
const user = users['user-3']; // User | undefined
// Safe: returns Option
const user = R.lookup('user-3')(users); // Option<User>
// Get user email or default
const email = pipe(
users,
R.lookup('user-3'),
O.map(u => u.email),
O.getOrElse(() => '[email protected]')
);
组合多个可选值
任务:获取用户的显示名称,这需要同时具备名和姓。
interface Profile {
firstName?: string;
lastName?: string;
nickname?: string;
}
// Imperative
function getDisplayName(profile: Profile): string {
if (profile.firstName && profile.lastName) {
return `${profile.firstName} ${profile.lastName}`;
}
if (profile.nickname) {
return profile.nickname;
}
return 'Anonymous';
}
// Functional with Option
import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';
const getDisplayName = (profile: Profile): string =>
pipe(
// Try full name first
O.Do,
O.bind('first', () => O.fromNullable(profile.firstName)),
O.bind('last', () => O.fromNullable(profile.lastName)),
O.map(({ first, last }) => `${first} ${last}`),
// Fall back to nickname
O.alt(() => O.fromNullable(profile.nickname)),
// Finally, default to Anonymous
O.getOrElse(() => 'Anonymous')
);
6. 实际示例
示例 1:将 API 响应转换为可直接用于 UI 的数据
// API response
interface ApiOrder {
order_id: string;
customer: {
id: string;
full_name: string;
};
line_items: Array<{
product_id: string;
product_name: string;
qty: number;
unit_price: number;
}>;
order_date: string;
status: 'pending' | 'processing' | 'shipped' | 'delivered';
}
// What the UI needs
interface OrderSummary {
id: string;
customerName: string;
itemCount: number;
total: number;
formattedTotal: string;
date: string;
statusLabel: string;
statusColor: string;
}
// Transformation
const STATUS_CONFIG: Record<string, { label: string; color: string }> = {
pending: { label: 'Pending', color: 'yellow' },
processing: { label: 'Processing', color: 'blue' },
shipped: { label: 'Shipped', color: 'purple' },
delivered: { label: 'Delivered', color: 'green' },
};
const formatCurrency = (cents: number): string =>
`$${(cents / 100).toFixed(2)}`;
const formatDate = (iso: string): string =>
new Date(iso).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
const toOrderSummary = (order: ApiOrder): OrderSummary => {
const total = order.line_items.reduce(
(sum, item) => sum + item.qty * item.unit_price,
0
);
const status = STATUS_CONFIG[order.status] ?? STATUS_CONFIG.pending;
return {
id: order.order_id,
customerName: order.customer.full_name,
itemCount: order.line_items.reduce((sum, item) => sum + item.qty, 0),
total,
formattedTotal: formatCurrency(total),
date: formatDate(order.order_date),
statusLabel: status.label,
statusColor: status.color,
};
};
// Transform all orders
const toOrderSummaries = (orders: ApiOrder[]): OrderSummary[] =>
orders.map(toOrderSummary);
示例 2:将用户设置与默认值合并
interface AppSettings {
theme: {
mode: 'light' | 'dark' | 'system';
primaryColor: string;
fontSize: 'small' | 'medium' | 'large';
};
notifications: {
email: boolean;
push: boolean;
sms: boolean;
frequency: 'immediate' | 'daily' | 'weekly';
};
privacy: {
showProfile: boolean;
showActivity: boolean;
allowAnalytics: boolean;
};
}
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
const DEFAULT_SETTINGS: AppSettings = {
theme: {
mode: 'system',
primaryColor: '#007bff',
fontSize: 'medium',
},
notifications: {
email: true,
push: true,
sms: false,
frequency: 'immediate',
},
privacy: {
showProfile: true,
showActivity: true,
allowAnalytics: true,
},
};
const deepMergeSettings = (
defaults: AppSettings,
user: DeepPartial<AppSettings>
): AppSettings => ({
theme: { ...defaults.theme, ...user.theme },
notifications: { ...defaults.notifications, ...user.notifications },
privacy: { ...defaults.privacy, ...user.privacy },
});
// Usage
const userPreferences: DeepPartial<AppSettings> = {
theme: { mode: 'dark' },
notifications: { sms: true, frequency: 'daily' },
};
const finalSettings = deepMergeSettings(DEFAULT_SETTINGS, userPreferences);
示例 3:按客户对订单分组并计算总额
interface Order {
id: string;
customerId: string;
customerName: string;
items: Array<{ name: string; price: number; quantity: number }>;
date: string;
}
interface CustomerOrderSummary {
customerId: string;
customerName: string;
orderCount: number;
totalSpent: number;
orders: Order[];
}
const calculateOrderTotal = (order: Order): number =>
order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const groupOrdersByCustomer = (orders: Order[]): CustomerOrderSummary[] => {
const grouped = groupBy((order: Order) => order.customerId)(orders);
return Object.entries(grouped).map(([customerId, customerOrders]) => ({
customerId,
customerName: customerOrders[0].customerName,
orderCount: customerOrders.length,
totalSpent: customerOrders.reduce(
(sum, order) => sum + calculateOrderTotal(order),
0
),
orders: customerOrders,
}));
};
示例 4:安全访问深层嵌套配置
interface AppConfig {
services?: {
api?: {
endpoints?: {
users?: string;
orders?: string;
products?: string;
};
auth?: {
type?: 'bearer' | 'basic' | 'oauth';
token?: string;
};
};
database?: {
primary?: {
host?: string;
port?: number;
name?: string;
};
};
};
}
import * as O from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';
// Create a type-safe config accessor
const getConfigValue = <T>(
config: AppConfig,
path: (config: AppConfig) => T | undefined,
defaultValue: T
): T => path(config) ?? defaultValue;
// Usage with optional chaining (simplest)
const apiUsersEndpoint = getConfigValue(
config,
c => c.services?.api?.endpoints?.users,
'/api/users'
);
// For more complex scenarios, use Option
const getEndpoint = (config: AppConfig, name: 'users' | 'orders' | 'products'): string =>
pipe(
O.fromNullable(config.services),
O.flatMap(s => O.fromNullable(s.api)),
O.flatMap(a => O.fromNullable(a.endpoints)),
O.flatMap(e => O.fromNullable(e[name])),
O.getOrElse(() => `/api/${name}`)
);
// Reusable pattern for multiple values
const getDbConfig = (config: AppConfig) => ({
host: config.services?.database?.primary?.host ?? 'localhost',
port: config.services?.database?.primary?.port ?? 5432,
name: config.services?.database?.primary?.name ?? 'app',
});
7. 如何选择
适合使用原生方法的情况:
- 简单转换:
.map()、.filter()、.reduce()完全够用 - 无需组合:只需进行一次性转换
- 团队熟悉:所有人都了解原生方法
- 可选链已足够:
obj?.prop?.value ?? default能满足你的空值安全需求
// Native is fine here
const activeUserNames = users
.filter(u => u.isActive)
.map(u => u.name);
适合使用 fp-ts 的情况:
- 链式执行可能失败的操作:多个步骤中的每一步都可能返回空值
- 组合转换:构建可重复使用的转换流水线
- 类型安全的错误处理:希望编译器跟踪潜在的失败
- 复杂数据流水线:包含许多适合显式组合的步骤
// fp-ts shines here
const result = pipe(
users,
A.findFirst(u => u.id === userId),
O.flatMap(u => O.fromNullable(u.profile)),
O.flatMap(p => O.fromNullable(p.settings)),
O.map(s => s.theme),
O.getOrElse(() => 'default')
);
在以下情况下使用自定义工具:
- 特定领域的操作:针对你的数据使用
groupBy、countBy、sumBy - 重复模式:你发现自己多次编写相同的转换
- 团队约定:在代码库中建立一致的模式
// Custom utility pays off when used repeatedly
const revenueByRegion = sumBy(
(sale: Sale) => sale.region,
(sale: Sale) => sale.amount
)(sales);
性能注意事项
- 链式调用会创建中间数组:
arr.filter().map()会先创建一个数组,再创建另一个 - 对于热点路径,可考虑使用
reduce:只遍历一次数据 - 优化前先进行测量:优化带来的可读性损失往往得不偿失
// If performance matters (and you've measured!)
const result = items.reduce((acc, item) => {
if (item.isActive) {
acc.push(item.name.toUpperCase());
}
return acc;
}, [] as string[]);
// vs the more readable (but 2-pass) version
const result = items
.filter(item => item.isActive)
.map(item => item.name.toUpperCase());
总结
| 任务 | 命令式 | 函数式 | 建议 |
|---|---|---|---|
| 转换数组元素 | 使用 for 循环和 push | .map() | 使用 map |
| 筛选数组 | 使用带条件的 for 循环 | .filter() | 使用 filter |
| 累积值 | 使用带累加器的 for 循环 | .reduce() | 复杂情况使用 reduce,简单情况使用循环 |
| 按键分组 | 使用 for 循环和对象 | groupBy 工具 | 创建可复用工具 |
| 选取对象字段 | 手动复制属性 | pick 工具 | 一次性操作使用展开语法,重复操作使用工具 |
| 合并对象 | 逐个处理属性 | 展开语法 | 使用展开语法 |
| 深度合并 | 嵌套条件语句 | 递归工具 | 使用工具或库 |
| 空值安全访问 | if (x && x.y) | ?. 或 Option | 简单情况使用 ?.,组合操作使用 Option |
| 规范化 API 数据 | 嵌套循环 | 提取函数 | 拆分为可组合的函数 |
以下情况更适合使用函数式方法:
- 需要组合操作
- 希望使用可复用的转换
- 重视显式的数据流,而非隐式状态
- 缺失值的类型安全很重要
以下情况可以使用命令式方法:
- 转换只需执行一次
- 逻辑简单且线性
- 性能至关重要,且你已进行测量
- 团队更习惯使用这种方法