Skip to main content

mapEach in practice

APIs commonly return arrays whose items repeat the same transport problems: private fields, snake-case keys, missing defaults, and inconsistent primitive values. Writing .map(...) around that shaping logic at every call site duplicates both runtime work and TypeScript types.

mapEach lifts one reusable item transform into a collection transform. Each item follows the same public ShapeWire pipeline, and the inferred item output becomes the array's output type.

Transforming API records

Consider a support-ticket response:

type ApiTicket = {
ticket_id: string;
subject_line: string;
priority: string | null;
is_open: string;
internal_note: string;
};

const rawTickets: ApiTicket[] = [
{
ticket_id: 'T-100',
subject_line: 'Cannot sign in',
priority: null,
is_open: 'yes',
internal_note: 'reset attempted',
},
{
ticket_id: 'T-101',
subject_line: 'Update billing address',
priority: 'high',
is_open: 'no',
internal_note: 'verified by phone',
},
];

Define the item model once and apply it to the complete list:

const toTicket = pipe(
omit(['internal_note']),
rename({
ticket_id: 'id',
subject_line: 'subject',
is_open: 'isOpen',
}),
defaults({priority: 'normal' as const}),
normalize({isOpen: 'boolean'}),
pick(['id', 'subject', 'priority', 'isOpen']),
);

const toTickets = mapEach(toTicket);
const tickets = toTickets(rawTickets);

Output:

[
{
id: 'T-100',
subject: 'Cannot sign in',
priority: 'normal',
isOpen: true,
},
{
id: 'T-101',
subject: 'Update billing address',
priority: 'high',
isOpen: false,
},
]

The inferred result is:

Array<{
id: string;
subject: string;
readonly priority: string;
readonly isOpen: boolean | null;
}>

Paginated collections

When the collection is one field of a larger response, transform the page first and use at to apply mapEach at the renamed collection path:

const rawOrderPage = {
page_no: '1',
total_count: '2',
results: [
{
order_id: 'O-700',
placed_at: '2026-07-10',
total: '49.90',
status: null,
internal_reference: 'warehouse-4',
},
{
order_id: 'O-701',
placed_at: 'invalid',
total: '18.00',
status: 'shipped',
internal_reference: 'warehouse-2',
},
],
};
const toOrder = pipe(
omit(['internal_reference']),
rename({order_id: 'id', placed_at: 'placedAt'}),
defaults({status: 'pending' as const}),
normalize({placedAt: 'isoDate', total: 'number'}),
);

const toOrderPage = pipe(
rename({page_no: 'page', total_count: 'total', results: 'items'}),
normalize({page: 'number', total: 'number'}),
at('items', mapEach(toOrder)),
);

const orderPage = toOrderPage(rawOrderPage);

Output:

{
page: 1,
total: 2,
items: [
{
id: 'O-700',
placedAt: '2026-07-10T00:00:00.000Z',
total: 49.9,
status: 'pending',
},
{
id: 'O-701',
placedAt: null,
total: 18,
status: 'shipped',
},
],
}

Boundaries

  • null and undefined collections produce [].
  • Other non-array inputs are outside the public contract and throw TypeError at runtime.
  • The result array is new; item immutability depends on the item transform.
  • Item errors propagate and stop mapping.
  • Sparse input holes remain sparse holes.

See the mapEach API reference for the core contract and Failure behavior for invalid inputs.