Status-Discriminated Unions
expect-status is designed for APIs that emit status-discriminated unions - response types where the HTTP status code is a discriminator field that determines the shape of the body.
What are status-discriminated unions?
Section titled “What are status-discriminated unions?”A status-discriminated union is a TypeScript type where the status field determines which body shape is possible:
type CreateOrgResponse = | { status: 201; body: { id: string; name: string } } | { status: 409; body: { message: string; organisationId: string } } | { status: 422; body: { message: string; field: string } };This pattern is common in modern OpenAPI codegens:
- ts-rest - emits status-discriminated unions by default
- orval - can be configured to emit this pattern
- hey-api - supports this pattern
- openapi-fetch (openapi-typescript) - supports this pattern
Why this pattern?
Section titled “Why this pattern?”Status-discriminated unions provide:
- Type safety - TypeScript can narrow the body type based on the status
- Explicit error handling - all possible error branches are visible in the type
- No runtime checks - the type system enforces correctness at compile time
How expect-status uses it
Section titled “How expect-status uses it”expect-status takes a status-discriminated union and a success status, then:
- Narrows the type to the matching branch
- Returns the body of that branch
- Throws an error if the status doesn’t match
type Response = | { status: 200; body: { id: string } } | { status: 404; body: { message: string } };
const body = await expectStatus(200, response);// ^? { id: string } - TypeScript knows this is the 200 branchCustom field names
Section titled “Custom field names”By default, expect-status reads from status and body fields. If your API uses different field names, configure them:
const expectStatus = createExpectStatus({ statusField: "statusCode", bodyField: "data",});
type Response = | { statusCode: 200; data: { id: string } } | { statusCode: 404; data: { message: string } };See Custom Field Names for more details.
Non-numeric discriminators
Section titled “Non-numeric discriminators”expect-status is intentionally focused on numeric HTTP-style status codes. This allows it to offer:
- Class-range matchers (
'4xx','5xx') - HTTP-aware exhaustive checking
- Status-to-class mapping utilities
For non-numeric discriminators (string tags like { tag: 'success' }, GraphQL __typename), use a general pattern-matching library like ts-pattern.
Next steps
Section titled “Next steps”- Flat Dispatch — handlers and messages in one flat object
- Status Specifiers — all the forms the first argument accepts
- Custom Envelope — use different field names