Transform
Transform response data with full type inference
The Transform plugin enables per-request transformation of response data with full type inference.
Installation
npm install @spoosh/plugin-transformSetup
import { Spoosh } from "@spoosh/core";
import { transformPlugin } from "@spoosh/plugin-transform";
const spoosh = new Spoosh<ApiSchema, Error>("/api").use([transformPlugin()]);Usage
Response Transforms (useRead)
Response transforms produce a separate transformedData field in meta while preserving the original data:
const { data, meta } = useRead((api) => api("posts").GET(), {
transform: (posts) => ({
count: posts.length,
hasMore: posts.length >= 10,
ids: posts.map((p) => p.id),
}),
});
// data = Post[] (original response, preserved)
// meta.transformedData = { count: number, hasMore: boolean, ids: number[] } | undefinedThe meta.transformedData type is automatically inferred from your transformer's return type.
Response Transforms (useWrite)
With hook-level options, response transforms for useWrite provide full type inference for meta.transformedData:
const { trigger, data, meta } = useWrite((api) => api("posts").POST(), {
transform: (post) => ({
success: true,
postId: post.id,
createdAt: new Date(post.timestamp),
}),
});
await trigger({ body: { title: "New Post" } });
// After trigger completes:
// data = Post (original response, preserved)
// meta.transformedData = { success: boolean, postId: number, createdAt: Date } | undefinedBy passing transform as a hook-level option (second argument to useWrite), TypeScript can properly infer the transformedData type.
Response Transform Examples
Transform and Analyze Data
Transform and analyze response data:
const { data, meta } = useRead((api) => api("analytics").GET(), {
transform: (analytics) => ({
totalViews: analytics.reduce((sum, a) => sum + a.views, 0),
averageEngagement:
analytics.reduce((sum, a) => sum + a.engagement, 0) / analytics.length,
topPerformer: analytics.sort((a, b) => b.views - a.views)[0],
}),
});
// data = original analytics array
// meta.transformedData = computed summary with totals and top performerAsync Transforms
All transform functions support async operations:
const { data, meta } = useRead((api) => api("posts").GET(), {
transform: async (posts) => {
const enriched = await enrichPostsWithMetadata(posts);
return {
count: enriched.length,
titles: enriched.map((p) => p.title),
};
},
});Removing Data
Return undefined to remove the data entirely:
const { data, meta } = useRead((api) => api("posts").GET(), {
transform: (posts) => {
if (posts.length === 0) return undefined;
return {
count: posts.length,
titles: posts.map((p) => p.title),
};
},
});Type Inference
The transform plugin provides full type inference:
// Response type is inferred from endpoint (useRead)
const { data, meta } = useRead((api) => api("users").GET(), {
transform: (users) => {
// 'users' is typed as User[]
return {
activeCount: users.filter((u) => u.active).length,
admins: users.filter((u) => u.role === "admin"),
};
},
});
// meta.transformedData is typed as:
// { activeCount: number; admins: User[] } | undefinedType Inference with Hook-Level Options
By passing transform as a hook-level option, meta.transformedData is properly typed:
const { trigger, meta } = useWrite((api) => api("posts").POST(), {
transform: (post) => ({
success: true,
postId: post.id,
}),
});
await trigger({ body: { title: "New Post" } });
// meta.transformedData is properly typed as:
// { success: boolean; postId: number } | undefined
console.log(meta.transformedData?.success); // ✓ Works with full type safetyUse Cases
Compute Derived Data
const { data, meta } = useRead((api) => api("orders").GET(), {
transform: (orders) => ({
total: orders.reduce((sum, o) => sum + o.amount, 0),
averageOrder: orders.reduce((sum, o) => sum + o.amount, 0) / orders.length,
pendingCount: orders.filter((o) => o.status === "pending").length,
}),
});Extract Specific Fields
const { data, meta } = useRead((api) => api("posts").GET(), {
transform: (posts) => ({
ids: posts.map((p) => p.id),
titles: posts.map((p) => p.title),
publishedCount: posts.filter((p) => p.published).length,
}),
});Enrich Response Data
const { data, meta } = useRead((api) => api("users").GET(), {
transform: (users) => ({
totalUsers: users.length,
activeUsers: users.filter((u) => u.active),
usersByRole: users.reduce(
(acc, u) => {
acc[u.role] = (acc[u.role] || 0) + 1;
return acc;
},
{} as Record<string, number>
),
}),
});