Designing Tagged Shapes
Why loose object shapes do not narrow, and how to parse unknown JSON into strict tagged unions.
The tag only helps when each variant is exact. A loose object with optional fields still leaves TypeScript unsure.
If kind is just string, TypeScript cannot know that "circle" means radius exists.
type LooseShape = {
kind: string;
radius?: number;
width?: number;
height?: number;
};
function describeLoose(shape: LooseShape): string {
if (shape.kind === "circle") {
// Error: shape.radius is possibly undefined.
// @ts-expect-error loose shapes do not narrow optional fields
return `circle ${shape.radius.toFixed(1)}`;
}
return "unknown";
}Keep loose data at the edge. Check JSON once, then convert it into the strict union used by normal code.
type RawEvent = { kind?: string; userId?: unknown };
type DomainEvent = { kind: "user.created"; userId: string };
function parseEvent(raw: RawEvent): DomainEvent | null {
if (raw.kind === "user.created" && typeof raw.userId === "string") {
return { kind: "user.created", userId: raw.userId };
}
return null;
}In production
Do not spread optional fields across one loose mega-shape. Parse external data into a strict union at the boundary, then let the rest of the code rely on exact tags and required fields.
Enjoyed this? Get more software craft delivered to your inbox.