Discriminated union state machines help you manage different states clearly and safely in your program. They make it easy to know what state you are in and what actions you can do next.
Discriminated union state machines in Typescript
type State =
| { type: 'idle' }
| { type: 'loading' }
| { type: 'success'; data: string }
| { type: 'error'; message: string };
function handleState(state: State) {
switch (state.type) {
case 'idle':
// handle idle
break;
case 'loading':
// handle loading
break;
case 'success':
// handle success with state.data
break;
case 'error':
// handle error with state.message
break;
}
}The type property is the discriminant that tells TypeScript which state it is.
Each state can have its own extra properties to hold data related to that state.
type LightState =
| { type: 'red' }
| { type: 'yellow' }
| { type: 'green' };type AuthState =
| { type: 'loggedOut' }
| { type: 'loggingIn' }
| { type: 'loggedIn'; user: string };
function printState(state: AuthState) {
switch (state.type) {
case 'loggedOut':
console.log('User is logged out');
break;
case 'loggingIn':
console.log('User is logging in');
break;
case 'loggedIn':
console.log(`Welcome, ${state.user}`);
break;
}
}This program models a door that can be closed, opening, open, or closing. It moves through states in order and prints each state.
type DoorState =
| { type: 'closed' }
| { type: 'opening' }
| { type: 'open' }
| { type: 'closing' };
function nextState(state: DoorState): DoorState {
switch (state.type) {
case 'closed':
return { type: 'opening' };
case 'opening':
return { type: 'open' };
case 'open':
return { type: 'closing' };
case 'closing':
return { type: 'closed' };
}
}
let state: DoorState = { type: 'closed' };
console.log(`Initial state: ${state.type}`);
state = nextState(state);
console.log(`Next state: ${state.type}`);
state = nextState(state);
console.log(`Next state: ${state.type}`);
state = nextState(state);
console.log(`Next state: ${state.type}`);
state = nextState(state);
console.log(`Next state: ${state.type}`);Always use a unique type string for each state to help TypeScript distinguish them.
Using a switch statement on the discriminant helps TypeScript know which properties are available.
This pattern helps prevent bugs by making invalid states or transitions impossible to represent.
Discriminated unions let you define clear, safe states with a common type property.
Use them to build simple state machines that are easy to understand and maintain.
TypeScript helps catch mistakes by knowing exactly which state you are working with.
Practice
What is the main purpose of using a discriminated union in TypeScript state machines?
Solution
Step 1: Understand discriminated unions
Discriminated unions use a commontypeproperty to distinguish between different object shapes.Step 2: Identify the purpose in state machines
This common property allows TypeScript to safely narrow the state type and catch errors.Final Answer:
To clearly define different states with a commontypeproperty for safe type checking. -> Option BQuick Check:
Discriminated union = safe state typing [OK]
type property in states [OK]- Thinking discriminated unions allow any property names freely
- Assuming states can be added dynamically without type errors
- Confusing discriminated unions with UI generation
Which of the following is the correct syntax to define a discriminated union type for a state machine with states loading and success?
type State = ?Solution
Step 1: Identify the discriminant property
The discriminated union requires a common property namedtypeto distinguish states.Step 2: Match the correct state shapes
Loading state has onlytype: 'loading', success state hastype: 'success'and extradata.Final Answer:
{ type: 'loading' } | { type: 'success'; data: string } -> Option CQuick Check:
Commontypeproperty with correct fields = B [OK]
- Using different property names like 'status' or 'state'
- Mixing properties between states incorrectly
- Adding data to loading state incorrectly
Given the following TypeScript code, what will be the output?
type State =
| { type: 'idle' }
| { type: 'loading' }
| { type: 'success'; data: string };
function getMessage(state: State): string {
switch (state.type) {
case 'idle':
return 'Waiting...';
case 'loading':
return 'Loading...';
case 'success':
return `Data: ${state.data}`;
}
}
console.log(getMessage({ type: 'success', data: 'Hello' }));Solution
Step 1: Analyze the input state
The input state is{ type: 'success', data: 'Hello' }, matching the 'success' case.Step 2: Follow the switch case logic
For 'success', the function returns`Data: ${state.data}`, which becomes 'Data: Hello'.Final Answer:
Data: Hello -> Option DQuick Check:
State 'success' returns data string [OK]
type in switch to get correct output [OK]- Confusing 'success' with 'loading' or 'idle'
- Expecting an error accessing 'data' without checking type
- Ignoring template string interpolation
Identify the error in this discriminated union state machine code:
type State =
| { type: 'start' }
| { type: 'end'; result: number };
function process(state: State) {
if (state.type === 'start') {
console.log('Starting');
} else {
console.log('Result is ' + state.result);
}
}
process({ type: 'start' });Solution
Step 1: Check the state type conditions
The function checks ifstate.type === 'start'and logs 'Starting'.Step 2: Confirm safe access to 'result'
In the else branch,state.resultis accessed only ifstate.type !== 'start', so it must be 'end' withresult.Final Answer:
No error; code runs and prints 'Starting'. -> Option AQuick Check:
Type narrowing prevents invalid property access [OK]
- Assuming accessing 'result' on 'start' state causes error
- Expecting return type annotation is mandatory
- Thinking 'type' property should be optional
You want to extend a discriminated union state machine to handle an error state with an optional message. Which is the best way to define this state to keep type safety and allow missing message?
type State =
| { type: 'idle' }
| { type: 'loading' }
| { type: 'success'; data: string }
| ???Solution
Step 1: Understand optional properties in discriminated unions
Usingmessage?: stringmeans the property can be missing or a string, which fits optional message.Step 2: Compare other options
{ type: 'error'; message: string | undefined } requires the property but allows undefined explicitly, less idiomatic. { type: 'error'; message: string } requires message always. { type: 'error'; message: string | null } requires message but allows null, which is less type safe.Final Answer:
{ type: 'error'; message?: string } -> Option AQuick Check:
Optional property syntax keeps type safety and flexibility [OK]
- Making message required when it should be optional
- Using union with undefined instead of optional property
- Allowing null which can cause runtime issues
Ready to prove it?
Pass the test and this pattern moves unknown
