Files
skills/docs/api-stack/api/api-plugin-campaigns/src/routes/shared/strip-undefined.md
2026-05-29 08:20:56 +01:00

2.5 KiB

strip-undefined

Purpose

stripUndefined is a route-layer adapter that bridges Zod's optional() output (T | undefined) with the exactOptionalPropertyTypes: true repository input types declared in @suaveplan/campaigns and the local CampaignSuppressionStore. When a route handler validates a request body through a Zod schema, optional fields surface as either the declared type or undefined; passing the parsed object straight to the repository fails the exactOptional check at compile time. This helper produces a shallow clone with every undefined-valued key removed so the downstream type narrows.

The helper is intentionally minimal — no recursion, no null handling, no key-name filtering — because every concrete usage in the routes layer passes a Zod-parsed object whose nesting has already been normalised. Recursion would obscure the cost model and tempt callers to use it as a deep-clean utility, which is out of scope.

Features

  • Shallow-only filter; nested objects are forwarded by reference.
  • null values are preserved (only === undefined triggers removal).
  • Returns a fresh object reference every call (never mutates input).
  • Generic-typed return narrows T[K] to Exclude<T[K], undefined>.

Usage: Basic

import { stripUndefined } from "@suaveplan/api-plugin-campaigns";

const body = { name: "x", templateId: undefined } as const;
const clean = stripUndefined(body);
// clean: { name: "x" } — `templateId` removed.

Usage: Advanced

import { stripUndefined } from "@suaveplan/api-plugin-campaigns";

const parsed = CreateCampaignSchema.parse(input);
await repo.create({
    id: idGen(),
    tenantId,
    ...stripUndefined(parsed),
    status: "draft",
    stats: EMPTY_STATS,
    createdAt: now,
    updatedAt: now,
});

API Reference

  • stripUndefined<T extends Record<string, unknown>>(input: T): { [K in keyof T]: Exclude<T[K], undefined> } — shallow copy with undefined-valued keys removed.

Implementation Notes

The implementation iterates Object.entries(input) once and writes into a fresh Record<string, unknown> accumulator. The output is then cast to the narrowed mapped type — a single assertion confined to the return statement. The accumulator is not pre-sized because the helper is hot-path-cheap (single object scan) and JavaScript engines do not benefit from new Object() capacity hints. The forbidden Object.assign path (which would forward undefined properties when iterating Object.keys) is intentionally avoided.