Skip to main content

Template Literal Types in React Tutorial

Template literal types combine TypeScript's template literal syntax with type operations to generate new string types from existing ones. By using backticks and type placeholders, you can create exhaustive, type-safe APIs for styling, event handling, and configuration where invalid string values are caught at compile time. This enables React components to accept only valid variant combinations or className prefixes without runtime validation.

Understanding Template Literal Type Syntax

Template literal types mirror JavaScript template literals but work on types. You write backticks and ${T} to interpolate type variables. When combined with unions, TypeScript expands the union into all possible combinations:

// Simple template literal type
type ColorVariant = "red" | "blue" | "green";
type ButtonClass = `btn-${ColorVariant}`;
// Result: "btn-red" | "btn-blue" | "btn-green"

// Multiple unions expand into combinations
type Size = "sm" | "md" | "lg";
type Color = "primary" | "secondary";
type ButtonVariant = `${Color}-${Size}`;
// Result: "primary-sm" | "primary-md" | "primary-lg" | "secondary-sm" | ...

interface ButtonProps {
variant: ButtonVariant; // Only valid combinations allowed
children: React.ReactNode;
}

export const Button = ({ variant, children }: ButtonProps) => (
<button className={`btn ${variant}`}>{children}</button>
);

// ✓ Valid
<Button variant="primary-lg">Large Button</Button>;

// ✗ Error: "invalid-sm" is not a valid variant
<Button variant="invalid-sm">Invalid</Button>;

Template literal types prevent misspellings and invalid combinations at compile time. Unlike runtime validation, you catch errors during development, not in production.

Event Handler Type Safety

Template literal types excel at creating exhaustive event handler types. A component can define which events it emits using string templates:

// Define events as template literal types
type ButtonEvent = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<ButtonEvent>}`;
// Result: "onClick" | "onFocus" | "onBlur"

interface CustomButtonProps {
handlers: Partial<Record<EventHandler, () => void>>;
label: string;
}

export const CustomButton = ({ handlers, label }: CustomButtonProps) => (
<button
onClick={handlers.onClick}
onFocus={handlers.onFocus}
onBlur={handlers.onBlur}
>
{label}
</button>
);

// ✓ Valid: only valid event handlers
<CustomButton
label="Click"
handlers={{
onClick: () => console.log("clicked"),
onFocus: () => console.log("focused"),
}}
/>;

// ✗ Error: "onDrag" is not a valid ButtonEvent handler
<CustomButton
label="Click"
handlers={{ onDrag: () => {} }}
/>;

CSS-in-JS Class Name Safety

Template literal types are particularly powerful for CSS class naming conventions. You can enforce a strict naming scheme and prevent inconsistent class names:

// Define a strict BEM-style class naming scheme
type BlockName = "button" | "card" | "input";
type Modifier = "primary" | "secondary" | "disabled";

type BemClass = `${BlockName}` | `${BlockName}__${string}` | `${BlockName}--${Modifier}`;

interface StyledComponentProps {
baseClass: BlockName;
modifiers?: Modifier[];
children: React.ReactNode;
}

export const StyledComponent = ({
baseClass,
modifiers,
children,
}: StyledComponentProps) => {
const classes = [baseClass];
if (modifiers) {
modifiers.forEach((mod) => classes.push(`${baseClass}--${mod}`));
}
return <div className={classes.join(" ")}>{children}</div>;
};

// ✓ Type-safe and consistent CSS classes
<StyledComponent baseClass="button" modifiers={["primary"]}>
Submit
</StyledComponent>;

Advanced: Recursive Template Literal Types

For deeply nested configurations, template literal types can compose recursively. For example, a component that validates configuration paths:

interface ConfigNode {
[key: string]: string | number | ConfigNode;
}

// Generate all valid paths through a config tree
type PathsTo<T, Prefix extends string = ""> = T extends ConfigNode
? {
[K in keyof T]: T[K] extends ConfigNode
? PathsTo<T[K], `${Prefix}${string & K}.`>
: `${Prefix}${string & K}`;
}[keyof T]
: never;

const config = {
api: {
baseUrl: "https://api.example.com",
timeout: 5000,
},
theme: {
colors: {
primary: "#007bff",
secondary: "#6c757d",
},
},
};

type ValidConfigPaths = PathsTo<typeof config>;
// Result: "api.baseUrl" | "api.timeout" | "theme.colors.primary" | ...

interface GetConfigProps {
path: ValidConfigPaths;
}

export const GetConfig = ({ path }: GetConfigProps) => {
// path is exhaustively typed to valid config paths
return <div>Config: {path}</div>;
};

Template Literal Patterns Table

PatternUse CaseExample
`${Union}`Expand union into string literals`btn-${Color}`
`${A}${B}`Combine multiple unions`${Size}-${Color}`
`on${Capitalize<T>}`Generate event handler namesEvent type to handler
`${Block}__${Element}`BEM-style CSS namesEnforce naming conventions
Recursive compositionValidate nested pathsConfig tree validation

Key Takeaways

  • Template literal types generate exhaustive string types from unions, preventing invalid string values at compile time.
  • Combine with Capitalize, Uppercase, and other intrinsic types to transform string unions into handler names or CSS classes.
  • Template literal types excel at enforcing consistent naming conventions (BEM, atomic, etc.) without runtime overhead.
  • Recursive template literal types validate deeply nested paths or configurations exhaustively.
  • Template literal types have zero runtime cost: they are erased during compilation and exist only to provide type safety.

Frequently Asked Questions

How do template literal types affect bundle size?

They don't. Template literal types are purely compile-time constructs and are completely erased when TypeScript is transpiled to JavaScript. They have zero impact on bundle size or runtime performance.

Can template literal types extract parts of strings at runtime?

No. Template literal types only validate types. At runtime, you still need string manipulation functions if you need to parse or extract parts of strings. Template literals help you avoid invalid values upfront.

What happens if my union is too large?

Very large unions (hundreds of combinations) can slow TypeScript compilation. For example, ${A}${B}${C}${D}${E} where each is a 10-item union creates 100,000 combinations. Keep unions reasonably sized (≤20 items per union) for fast development.

Can I use regex or constraints in template literal types?

No. Template literal types don't support regex matching or custom validation logic. They work with fixed unions only. For pattern matching, use conditional types instead.

Further Reading