This skill covers the data transformations you do every day: working with arrays, reshaping objects, normalizing API responses, grouping data, and safely accessing nested values. Each section shows the imperative approach first, then the functional equivalent, with honest assessments of when each approach shines.
Read the detailed guide before executing this skill. It retains the complete procedure and reference material. Treat its safety, prerequisites, and validation requirements as mandatory. For focused work, load the relevant sections; for end-to-end work, read the guide completely.
// 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; // integer cents, validated at the API boundary
}>;
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);
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,
},
};
// Input must be validated and omit undefined fields before spread merging.
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);
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,
}));
};
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',
});
.map(), .filter(), .reduce() are perfectly goodobj?.prop?.value ?? default handles your null-safety needs// Native is fine here
const activeUserNames = users
.filter(u => u.isActive)
.map(u => u.name);
// 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 for your data// Custom utility pays off when used repeatedly
const revenueByRegion = sumBy(
(sale: Sale) => sale.region,
(sale: Sale) => sale.amount
)(sales);
arr.filter().map() creates one array, then anotherreduce: One pass through the data// 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());