Unlocking the Power of TypeScript Enums and Unions
The Lure of Enums
Enums are essentially sets of named constants that can take either numeric or string forms. At first, I used enums everywhere, from Redux actions to finite state machines, to avoid pesky string typo errors.
The Limitations of Enums
Take, for instance, an AuthenticationStates
enum that models an authentication workflow:
enum AuthenticationStates {
UNAUTHENTICATED,
AUTHENTICATING,
AUTHENTICATED,
ERROR
}
While enums ensure that a user can’t be in multiple contradictory states at once, they fall short when it comes to handling additional data, such as error objects.
Enter Discriminated Unions
That’s when I discovered discriminated unions, also known as algebraic data types. These powerful constructs allow us to create unions that can be composed of many types, not just primitive ones.
type AuthenticationState =
| { type: 'UNAUTHENTICATED' }
| { type: 'AUTHENTICATING' }
| { type: 'AUTHENTICATED', authToken: string }
| { type: 'ERROR', error: Error }
By introducing a discriminator field (type
in this example), we can create a union that only requires the same kind field as each element in the union.
Type Narrowing and Compiler Enforcement
The true magic happens when TypeScript type narrows on a discriminator of a union. This means that only the appropriate data is available on each type, ensuring that we can’t access an authToken
if we’re not in the AUTHENTICATED
state.
function handleAuthState(state: AuthenticationState) {
switch (state.type) {
case 'AUTHENTICATED':
console.log(state.authToken); // okay
break;
default:
console.log(state.authToken); // error: Property 'authToken' does not exist on type '{ type: "UNAUTHENTICATED" } | { type: "AUTHENTICATING" } | { type: "ERROR"; error: Error; }'.
}
}
The compiler enforces this correctness, making it impossible to access incorrect data.
Booleans Don’t Cut It
Attempts to model state with Booleans inevitably lead to a mess of tangled logic and contradicting variables. Algebraic data types, on the other hand, provide a elegant solution to this problem.
Unions: The Ultimate Executable Documentation
Discriminated unions serve as beautiful, executable documentation that keeps developers on the right track. They’ve been around in functional languages like Haskell, and it’s exciting to see TypeScript bring them to the frontend development community.