Type-Safe State Machine in TypeScript
Building state machines is a common pattern in software development, particularly for managing complex workflows or user interfaces. This challenge asks you to create a type-safe state machine in TypeScript, ensuring that state transitions are valid and preventing runtime errors related to incorrect state changes. This will involve defining states and transitions with strong typing, making your code more robust and maintainable.
Problem Description
You are tasked with creating a generic StateMachine class in TypeScript that enforces type safety for states and transitions. The state machine should be configurable with a set of states and a set of valid transitions between those states. The class should provide methods for transitioning between states, and it should throw an error if an invalid transition is attempted.
Key Requirements:
- Type-Safe States: The state machine must use a union type to define the possible states.
- Type-Safe Transitions: Transitions between states must be defined with a type that specifies the source and destination states.
- Error Handling: Attempting an invalid transition should result in a clear error message.
- Current State Tracking: The state machine must maintain and expose the current state.
- Generic: The state machine should be generic, allowing it to be used with different state types.
Expected Behavior:
- The
StateMachineclass should be instantiated with an initial state and a set of valid transitions. - The
transition(newState: T)method should update the current state tonewStateif the transition is valid. - If the transition is invalid (i.e., there's no defined transition from the current state to
newState), thetransitionmethod should throw an error with a descriptive message. - The
getCurrentState()method should return the current state of the state machine.
Edge Cases to Consider:
- What happens if the initial state is not a valid state? (Consider throwing an error during instantiation).
- What happens if the transition function is not defined for a particular state?
- How to handle transitions that lead back to the same state? (Should be allowed by default, but consider if this needs to be configurable).
Examples
Example 1:
// Define states
type TrafficLightState = 'red' | 'yellow' | 'green';
// Define transitions
type TrafficLightTransition =
| { from: 'red'; to: 'green' }
| { from: 'green'; to: 'yellow' }
| { from: 'yellow'; to: 'red' };
// Usage
const trafficLight = new StateMachine<TrafficLightState>(
'red',
[
{ from: 'red', to: 'green' },
{ from: 'green', to: 'yellow' },
{ from: 'yellow', to: 'red' },
]
);
console.log(trafficLight.getCurrentState()); // Output: red
trafficLight.transition('green');
console.log(trafficLight.getCurrentState()); // Output: green
try {
trafficLight.transition('red'); // Invalid transition
} catch (error) {
console.error(error.message); // Output: Invalid transition from green to red
}
Explanation: This example demonstrates a simple traffic light state machine. The states are 'red', 'yellow', and 'green'. The transitions are defined as objects specifying the source and destination states. The code shows a valid transition and then attempts an invalid transition, which results in an error.
Example 2:
// Define states
type OrderState = 'pending' | 'processing' | 'shipped' | 'delivered';
// Define transitions
type OrderTransition =
| { from: 'pending'; to: 'processing' }
| { from: 'processing'; to: 'shipped' }
| { from: 'shipped'; to: 'delivered' };
// Usage
const orderMachine = new StateMachine<OrderState>(
'pending',
[
{ from: 'pending', to: 'processing' },
{ from: 'processing', to: 'shipped' },
{ from: 'shipped', to: 'delivered' },
]
);
console.log(orderMachine.getCurrentState()); // Output: pending
orderMachine.transition('processing');
console.log(orderMachine.getCurrentState()); // Output: processing
try {
orderMachine.transition('pending'); // Invalid transition
} catch (error) {
console.error(error.message); // Output: Invalid transition from processing to pending
}
Explanation: This example showcases an order processing state machine. The states represent different stages of an order. The transitions are defined to reflect the typical flow of an order.
Constraints
- Time Limit: The solution should be completed within 60 minutes.
- Code Quality: The code should be well-structured, readable, and follow TypeScript best practices.
- Error Messages: Error messages should be clear and informative, indicating the current state and the attempted destination state.
- No External Libraries: You are not allowed to use any external libraries for this challenge. The solution must be implemented using only built-in TypeScript features.
- Transition Definition: The transition definition must be an array of objects, each object having
fromandtoproperties.
Notes
- Consider using a type guard to ensure that the
newStateis a valid state. - Think about how to make the
StateMachineclass more flexible, perhaps by allowing for configurable error handling. - The
transitionmethod should not allow transitions to states that are not explicitly defined in the transition list. - Focus on type safety and clear error handling. A working, but not type-safe, solution will not be considered complete.
- The
fromandtoproperties in the transition definition should match the state type.