ยท Jake Worth

Union type from array in TypeScript


I often want the same string list in three places: runtime values, a state type, and function parameters. Duplicating a union by hand drifts out of sync:

component.tsx
const builderSteps = ['communications', 'estimates', 'procedures'];
type BuilderStep = 'communications' | 'estimates' | 'procedures'; // Better keep this up-to-date! ๐Ÿคž

With as const and an indexed access type, the array is the single source of truth:

component.tsx
const builderSteps = ['communications', 'estimates', 'procedures'] as const;
type BuilderStep = (typeof builderSteps)[number];
// Returns a union of 'communications' | 'estimates' | 'procedures'

This lets us type a slice of state, type a function that might receive that state, and build components all from the same JavaScript array.

component.tsx
import {useState} from 'react';
// `as const` array
const builderSteps = ['communications', 'estimates', 'procedures'] as const;
// Indexed access type
type BuilderStep = (typeof builderSteps)[number];
export const Component = () => {
// Use case #1: state
const [step, setStep] = useState<BuilderStep>('communications');
// Use case #2: function typing
const handleStepClick = (step: BuilderStep) => setStep(step);
// Use case #3: building UI
return (
<>
{builderSteps.map((step) => (
<button key={step} onClick={() => handleStepClick(step)}>
Set step: {step}
</button>
))}
</>
);
};

When we skip as const, TypeScript widens the array to string[]. Then (typeof builderSteps)[number] becomes string, and you lose the union. The const assertion freezes the elements as read-only literal types so the [number] lookup can turn the array into a union.