Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Everyday data transformations using functional patterns - arrays, objects, grouping, aggregation, and null-safe access
.claude/skills/fp-data-transforms/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-15 | ✗→✓ | ▲ Improved | — | — |
| case-13 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-18 | ✗→✓ | ▲ Improved | — | — |
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.
typescript// 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);
typescriptinterface 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);
typescriptinterface 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, })); };
typescriptinterface 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 needstypescript// Native is fine here const activeUserNames = users .filter(u => u.isActive) .map(u => u.name);
typescript// 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 datatypescript// 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 datatypescript// 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());
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +27 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
The publisher has shipped newer versions since this run, so these numbers describe v1, not the version currently listed.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.