Skip to content
Sarj Code Standards

prefer-discriminated-union

Preserve producer-proven tag/payload and Boolean state relationships in return types.

Why

A flat object with optional branch data loses the relationship between a tag and its payload. Callers cannot narrow to the required fields, and the type permits incomplete or contradictory combinations.

Fix

If the variants are mutually exclusive, represent each with a literal tag and its required payload. For predicates over one current state, consider returning that authoritative state instead of storing multiple derived flags. Preserve every valid state, independent dimension, shared field, payload value type and nullability. For a producer returning a subset of a broader wire contract, give the producer a precise domain return type after validation.

Examples

Before — flagged Two predicates over one immutable phase cannot both be true
src/projection.ts
type Phase = "ready" | "waiting" | "unknown";
declare function snapshot(): Phase;
function state(): { ready: boolean; waiting: boolean } {
const phase = snapshot();
return { ready: phase === "ready", waiting: phase === "waiting" };
}
After — preferred Return the phase while preserving the independent timeout dimension
src/projection.ts
type Phase = "ready" | "waiting" | "reconnecting" | "unknown";
declare function snapshot(): Phase;
function state(timeout: boolean): { phase: Phase; timeout: boolean } {
const phase = snapshot();
return { phase, timeout };
}
Before — flagged Preserve producer variant relationships
src/job.ts
function job(done: boolean): { phase: "queued" | "complete"; ticket?: string } {
return done ? { phase: "complete" } : { phase: "queued", ticket: "t" };
}
After — preferred Give each produced variant its payload
src/job.ts
function job(
done: boolean,
): { phase: "queued"; ticket: string } | { phase: "complete" } {
return done ? { phase: "complete" } : { phase: "queued", ticket: "t" };
}
Before — flagged A producer establishes the status-to-payload relationship
src/result.ts
function result(ok: boolean): { ok: boolean; data?: string; error?: string } {
return ok ? { ok: true, data: "value" } : { ok: false, error: "failed" };
}
After — preferred Use explicit result branches
src/result.ts
type Result = { ok: true; data: string } | { ok: false; error: string };
Before — flagged A complete dispatcher preserves each returned variant
src/job.ts
function job(command: string): { phase: "queued" | "complete"; ticket?: string } {
switch (command) {
case "start":
return { phase: "queued", ticket: "t" };
default:
return { phase: "complete" };
}
}
After — preferred An explicit dispatcher result supports payload narrowing
src/job.ts
function job(
command: string,
): { phase: "queued"; ticket: string } | { phase: "complete" } {
switch (command) {
case "start":
return { phase: "queued", ticket: "t" };
default:
return { phase: "complete" };
}
}
Before — flagged A producer returns retry, terminal and saved-partial combinations
src/failure.ts
function failure(
retry: boolean,
saved: boolean,
): { retriable: boolean; persisted: boolean } {
return saved
? { retriable: false, persisted: true }
: retry
? { retriable: true, persisted: false }
: { retriable: false, persisted: false };
}
After — preferred Independent observations retain all four combinations
src/observations.ts
function observations(a: boolean, b: boolean): { started: boolean; reported: boolean } {
return a
? b
? { started: true, reported: true }
: { started: true, reported: false }
: b
? { started: false, reported: true }
: { started: false, reported: false };
}

References