10 questions foundA template literal type uses the same backtick syntax as a runtime template literal, but with TYPES embedded inside `${...}` instead of values — it produces a new string literal type (or union of them) built from combining literal text with the possible values of the embedded type(s).
type Greeting = `Hello, ${string}!`;
const a: Greeting = 'Hello, Sam!'; // OK
// const b: Greeting = 'Hi, Sam!'; // Error: doesn't match the pattern
Real-world example Constraining a string parameter to follow a specific required format, like a greeting or a CSS custom property name.
Common follow-ups: What happens if you embed a UNION type (not just 'string') inside a template literal type?
Types & Interfaces
When you embed a union type inside `${...}`, TypeScript computes the CROSS PRODUCT of every combination, producing a new union of every possible resulting string literal — this happens automatically, without any extra syntax needed.
type Size = 'small' | 'medium' | 'large';
type ClassName = `btn-${Size}`;
// 'btn-small' | 'btn-medium' | 'btn-large'
Real-world example Generating a precise union of valid CSS class names or Tailwind utility classes from a smaller set of literal building blocks.
Common follow-ups: What happens if you combine TWO different union types inside the same template literal type?
Union & Intersection Types
These four built-in generic utility types transform a string literal type's CASING at the type level (mirroring their runtime string method equivalents), and are most commonly combined with template literal types to systematically derive new property/method names with consistent casing conventions.
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type HoverEvent = EventName<'hover'>; // 'onHover'
Real-world example Deriving consistent event-handler prop names (onClick, onHover) automatically from a base set of event name literals.
Common follow-ups: Do these intrinsic types have any runtime equivalent, or do they exist purely at the type level?
Mapped Types
Build a template literal type describing the exact expected character pattern using nested unions for each character position — while genuinely complex patterns (like arbitrary-length hex codes) hit practical limits, simpler fixed-length patterns work well and give real compile-time format validation.
type HexDigit = '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'a'|'b'|'c'|'d'|'e'|'f';
type HexColor = `#${HexDigit}${HexDigit}${HexDigit}${HexDigit}${HexDigit}${HexDigit}`;
const valid: HexColor = '#ff5733'; // OK
// const invalid: HexColor = '#zzz'; // Error: doesn't match pattern
Real-world example Enforcing that a configuration value or design-token color string is a syntactically valid 6-digit hex code at compile time.
Common follow-ups: Why does this specific approach become impractical for validating longer or more complex patterns, like full email address validation?
Union & Intersection Types
Within a mapped type's key-remapping 'as' clause, you can construct a NEW property name using a template literal type that references the original key (often combined with Capitalize<>), letting you systematically derive a full set of consistently-named methods or properties from an existing type's keys.
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void
};
interface Person { name: string; age: number; }
type PersonSetters = Setters<Person>;
// { setName: (value: string) => void; setAge: (value: number) => void }
Real-world example Auto-generating a complete, consistently-named setter interface from a plain data model interface without manual duplication.
Common follow-ups: Why does the key need to be written as `string & K` rather than just `K` inside the template literal here?
Mapped Types
Combine a conditional type with a template literal PATTERN on the 'extends' side, using 'infer' at the specific position you want to capture — TypeScript pattern-matches the string literal against the template shape and extracts the inferred substring as its own type.
type ExtractRouteParam<T extends string> =
T extends `${string}:${infer Param}` ? Param : never;
type Param1 = ExtractRouteParam<'/users/:id'>; // 'id'
type Param2 = ExtractRouteParam<'/posts/:postId'>; // 'postId'
Real-world example Building a fully type-safe router library that extracts route parameter names directly from a route path's literal string type.
Common follow-ups: How would you extend this to extract MULTIPLE route parameters from a path with several ':param' segments?
Conditional Types
Recursively walk the route string literal type using conditional types with 'infer', peeling off one `:param` segment at a time, and use a mapped type (via Record) to assemble the final object type mapping every discovered parameter name to 'string' — this is exactly the technique modern type-safe routing libraries use internally.
type ParseParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ParseParams<Rest>]: string }
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
type Params = ParseParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string }
Real-world example Building a fully type-safe routing library where navigating to a route automatically requires and types its exact set of URL parameters.
Common follow-ups: What real-world libraries (like React Router's typed routes, or tRPC) rely on exactly this kind of type-level string parsing technique?
Conditional Types
Embedding a `number` or `bigint` type inside a template literal type converts it to its STRING representation in the resulting type (matching how JavaScript's actual template literals coerce numbers to strings at runtime) — embedding a literal number type produces that number's specific string form, while embedding the general 'number' type produces a much broader (though still constrained) string pattern.
type Pixels<N extends number> = `${N}px`;
type Width = Pixels<100>; // '100px' -- a specific string literal
type AnyPixelValue = `${number}px`; // matches '0px', '42px', '3.5px', etc. -- any numeric string followed by 'px'
Real-world example Typing CSS-in-JS style dimension values that must be a valid number immediately followed by a unit suffix like 'px' or '%'.
Common follow-ups: Does embedding a 'boolean' type inside a template literal type behave similarly, producing 'true' | 'false' as string literals?
Primitive Types Arrays & Tuples
Build a template literal type combining number-embedding for each version segment with literal '.' separators, giving reasonable structural validation of the major.minor.patch shape — though note this can't validate that each segment stays within a specific numeric RANGE, since 'number' embedded in a template literal accepts any numeric string.
type SemVer = `${number}.${number}.${number}`;
const valid: SemVer = '1.4.2'; // OK
// const invalid: SemVer = '1.4'; // Error: missing the patch segment
Real-world example Enforcing that a package version string field follows the expected major.minor.patch structural shape before it's used elsewhere.
Common follow-ups: Why can't this technique alone prevent an invalid value like '99999.0.0' if there's a business rule against versions that high?
Primitive Types Arrays & Tuples
Recursively split the dot-separated path string literal type using 'infer' to peel off one segment at a time, checking at each step that the segment is actually a valid key of the CURRENT nested object type, and recursing into that property's type for the next segment — producing full compile-time validation of arbitrarily deep property paths.
type PathValue<T, Path extends string> =
Path extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? PathValue<T[Key], Rest>
: never
: Path extends keyof T
? T[Path]
: never;
interface State { user: { profile: { name: string } } }
type NameType = PathValue<State, 'user.profile.name'>; // string
type BadType = PathValue<State, 'user.profile.age'>; // never -- 'age' doesn't exist
Real-world example Building a fully type-safe deep-property-access utility that catches invalid dot-path typos at compile time, unlike lodash's untyped runtime-only get().
Common follow-ups: Why does this kind of deeply recursive type-level parsing sometimes hit the TypeScript compiler's recursion depth limits on very large object types?
Conditional Types