Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
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.
When to Use
You need to transform arrays, objects, grouped data, or nested values in TypeScript.
The task involves reshaping API responses, null-safe access, aggregation, or normalization.
You want practical functional patterns for everyday data work instead of low-level loops.
Why functional is better here: The intent is immediately clear. map says "transform each element." The transformation logic (toDollars) is named and reusable. No index management, no manual array building.
Filter: Keep What Matches
The Task: Get all active users from a list.
Imperative Approach
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;
}
Why functional is better here: The predicate (isActive) is separated from the iteration logic. You can reuse, test, and compose predicates independently.
Reduce: Accumulate Into Something New
The Task: Calculate the total price of items in a cart.
Imperative Approach
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;
}
Functional Approach
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);
Honest assessment: For simple sums, the imperative loop is actually quite readable. The functional version shines when you need to compose the accumulation with other transformations, or when the reduction logic is complex enough to benefit from being named.
Chaining: Combine Operations
The Task: Get the names of all active premium users, sorted alphabetically.
Imperative Approach
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;
}
Why functional is better here: Each step in the chain has a single responsibility. You can read the transformation as a series of steps: "filter active, filter premium, get names, sort." Adding or removing a step is trivial.
Using fp-ts Array Module
fp-ts provides additional array utilities with better composition support:
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. Object Transformations
Objects need reshaping constantly: picking fields, omitting sensitive data, merging settings, and updating nested values.
Pick: Select Specific Fields
The Task: Extract only the public fields from a user object.
// 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',
]);
Honest assessment: For one-off omits, destructuring (the imperative approach) is perfectly fine and very readable. The functional omit utility pays off when you have many such transformations or need to compose them.
Why functional is better here: Spread syntax is concise and handles any number of keys. Later spreads override earlier ones, giving you natural "defaults with overrides" behavior.
// 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');
Honest assessment: The spread nesting is verbose but explicit. For deeply nested updates, consider using a library like immer or fp-ts lenses. The verbosity of the functional approach is the price of immutability.
3. Data Normalization
API responses rarely match the shape your app needs. Normalization transforms nested, denormalized data into flat, indexed structures.
API Response to App State
The Task: Transform a nested API response into a normalized state.
Why functional is better here: Each extraction is independent and testable. The createNormalizedCollection helper is reusable. Adding a new entity type means adding one new extraction function.
Transform API Response to UI-Ready Data
The Task: Convert API data to what your components need.
// 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
}
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: Count Occurrences
The Task: Count orders by status.
Imperative Approach
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;
}
Honest assessment: For simple access patterns, optional chaining (?.) is perfect. It's built into the language and very readable. Use fp-ts Option when you need to compose operations on potentially missing values.
When to Use Option Instead
Use fp-ts Option when:
You need to chain multiple operations on potentially missing values
You want to distinguish "missing" from other falsy values
You're building a pipeline of transformations
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')
);
Safe Array Access
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')
);
Safe Record/Dictionary Access
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: 'alice@example.com' },
'user-2': { name: 'Bob', email: 'bob@example.com' },
};
// 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(() => 'unknown@example.com')
);
Combining Multiple Optional Values
The Task: Get a user's display name, which requires both first and last name.
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. Real-World Examples
Example 1: Transform API Response to UI-Ready Data
Domain-specific operations: groupBy, countBy, sumBy for your data
Repeated patterns: You find yourself writing the same transformation many times
Team conventions: Establishing consistent patterns across the codebase
// Custom utility pays off when used repeatedly
const revenueByRegion = sumBy(
(sale: Sale) => sale.region,
(sale: Sale) => sale.amount
)(sales);
Performance Considerations
Chaining creates intermediate arrays: arr.filter().map() creates one array, then another
For hot paths, consider reduce: One pass through the data
Measure before optimizing: The readability cost of optimization is often not worth it
// 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());