From harness-claude
Models mutually exclusive states using TypeScript discriminated unions with exhaustive narrowing for loading/success/error, API responses, domain events, and replacing boolean flags.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Model mutually exclusive states with discriminated unions and exhaustive narrowing
Provides TypeScript functional patterns for ADTs, discriminated unions, Result/Option types, branded types. Use for state machines, type-safe domain models, and error handling.
Provides examples of advanced TypeScript patterns: generics with constraints, conditional types, mapped types, discriminated unions, and type guards. Useful for complex typing scenarios.
Models variant types with Zod's z.union, z.discriminatedUnion, z.intersection, and type narrowing for polymorphic API payloads, tagged unions, and ADTs.
Share bugs, ideas, or general feedback.
Model mutually exclusive states with discriminated unions and exhaustive narrowing
type Result<T> =
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
| { status: 'loading' };
switch or if statements — TypeScript narrows the type automatically:function handleResult<T>(result: Result<T>) {
switch (result.status) {
case 'success':
console.log(result.data); // TypeScript knows data exists here
break;
case 'error':
console.log(result.error); // TypeScript knows error exists here
break;
case 'loading':
console.log('Loading...');
break;
}
}
never — catch unhandled cases at compile time:function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
function handleResult<T>(result: Result<T>): string {
switch (result.status) {
case 'success':
return 'OK';
case 'error':
return 'FAIL';
case 'loading':
return 'WAIT';
default:
return assertNever(result);
// If a new status is added, this line errors at compile time
}
}
// Bad: invalid states are possible (isLoading + error both true)
interface State {
isLoading: boolean;
data?: User;
error?: Error;
}
// Good: each state is explicitly defined
type State =
| { kind: 'idle' }
| { kind: 'loading' }
| { kind: 'success'; data: User }
| { kind: 'error'; error: Error };
type OrderEvent =
| { type: 'ORDER_PLACED'; orderId: string; items: Item[] }
| { type: 'PAYMENT_RECEIVED'; orderId: string; amount: number }
| { type: 'ORDER_SHIPPED'; orderId: string; trackingNumber: string }
| { type: 'ORDER_CANCELLED'; orderId: string; reason: string };
function processEvent(event: OrderEvent): void {
switch (event.type) {
case 'ORDER_PLACED':
// event.items is available
break;
case 'ORDER_SHIPPED':
// event.trackingNumber is available
break;
}
}
type ApiResponse<T> =
| { ok: true; data: T; status: number }
| { ok: false; error: string; status: number };
async function fetchUser(): Promise<ApiResponse<User>> {
// ...
}
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
in operator for narrowing when there is no explicit discriminant:type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ('swim' in animal) {
animal.swim(); // Narrowed to Fish
}
}
A discriminated union (also called a tagged union) is a union of types that share a common property with literal type values. TypeScript uses this property as a discriminant to narrow the type in conditional branches.
The discriminant property must be:
Exhaustive checking patterns:
switch with default: assertNever(x) — throws at runtime if an unhandled case is reachednever variable: const _exhaustive: never = x — compile-time only, no runtime overhead--noUncheckedIndexedAccess and strictNullChecks enhance exhaustiveness checkingPerformance: Discriminated unions have zero runtime overhead beyond the discriminant property. The narrowing happens entirely at compile time.
Common naming conventions for discriminants:
kind — for geometric shapes, node types, abstract syntax treestype — for events, actions, messagesstatus — for state machines, API responsestag — for algebraic data typesTrade-offs:
https://typescriptlang.org/docs/handbook/2/narrowing.html