Enforcing string literal types in React components
Leverage generics to create truly type-safe React components that prevent impossible states.
The Problem
Recently, I encountered this React component:
Coming from a functional programming background, I immediately spotted a critical issue: the component allows representation of illegal or impossible states. Specifically, we can pass an initialStep
value that’s outside the bounds of the steps
array:
First attempt: “meh” solution
My first attempt at solving this involved introducing a generic type for the steps, allowing us to reference the type of initialStep
:
However, this approach falls short because initialStep
is still inferred as string
:
We can force TypeScript to narrow the type by marking our steps array with as const
:
While this works, it requires users to remember to add as const
when using the component. I wanted to enforce this type safety from within the component itself.
Second attempt: “the” solution
Now we achieve the desired behavior without as const
and get editor autocompletion as a bonus! 🎉
The trick here lies in how we parameterize the step name. Instead of using a generic that extends the entire Step interface, we make the name property itself generic. This approach fundamentally changes how TypeScript infers the types:
- When we parameterize
U extends string
, TypeScript treats each string literal as a distinct type rather than widening it to string. - The type parameter
T extends Step<U>[]
creates the proper type relationship between the type ofinitialStep
andsteps
. - Finally,
T[number]['name']
forinitialStep
extracts the union of literal types.