TypeScript / Intermediate / Lesson 9
Conditional and template literal types
Compute useful type relationships without pretending the checker executes data transformations or proves unlimited recursive models.
On this page
Lesson goals
- Read conditional types, infer, and distributive behavior.
- Build finite string relationships with template literal types.
- Keep type computations separate from runtime parsing and generation.
A conditional type branches on assignability
T extends U ? A : B selects a type according to assignability, not a runtime if statement over a value. It can express relationships that would otherwise need many repeated overloads. A conditional type on a naked type parameter distributes across a union, evaluating each alternative separately. Wrapping the checked sides in tuples can suppress that distribution when the question concerns the union as a whole. The distinction affects results involving unions and never, so write small compile-time examples before embedding the expression into a public library. A concise type expression can still encode the wrong relationship.
Infer a component type from a matched shape
infer introduces a type variable within the pattern a conditional type tests. The first example extracts an element from a readonly array type and otherwise keeps the original type. Its number variable is checked using that computed result, but the emitted JavaScript still only creates the literal value seven. No runtime array is inspected by ElementOf. If a function must inspect external arrays, write real checks and return a validated result. Type-level extraction is useful for relating APIs already described by trustworthy static types, not for converting arbitrary data into those types.
export {};
type ElementOf<T> = T extends readonly (infer Item)[] ? Item : T;
const value: ElementOf<readonly number[]> = 7;
const label: ElementOf<string> = "map";
console.log(value, label);
7 map
ElementOf yields number and string respectively. The runtime prints the actual assigned values.
Generate finite names in the checker
Template literal types combine literal components into string types. The second example permits only event names based on the known field union. The runtime event string exists because code assigns it; the type does not register listeners or send messages. Combining multiple large unions can multiply the number of alternatives, making the result expensive and diagnostics noisy. Use a finite domain where the relationship helps callers, and generate large stable type declarations ahead of time when that is clearer than asking every consumer to recompute a huge cross product. Validate external event names before dispatch.
export {};
type Field = "count" | "title";
type EventName = `${Field}Changed`;
function record(name: EventName): string { return `event:${name}`; }
console.log(record("countChanged"));
console.log(record("titleChanged"));
event:countChanged
event:titleChanged
Both names belong to the computed union. The function formats a string and performs no event-system side effect.
Bound the complexity of a type model
Recursive conditional types can express nested relationships but are subject to compiler resource and instantiation limits. Do not promise an exact universal recursion depth, because practical limits depend on the expression and compiler version. never behaves as an empty union in many distributive expressions, which can surprise code expecting an ordinary false branch. unknown and any also interact differently with conditionals. Test boundary types explicitly, including empty tuples, optional properties, unions, never, and broad string. Prefer a documented finite set of supported inputs to a clever generic that appears to accept everything but becomes unusable for realistic models.
Check that runtime behavior matches the advertised relation
A conditional return type can be harder to implement than a conditional parameter type because control-flow analysis may not prove a generic branch corresponds to the computed result. Do not solve that gap with repeated assertions that hide an unsupported promise. Consider overloads with a sound implementation, separate named functions, or an honest union result when callers do not need exact correlation. Keep type tests alongside runtime tests so a future refactor cannot preserve one while breaking the other. Type-level expressiveness is valuable when it improves the consumer contract, not when it merely demonstrates language complexity.
Common errors and fixes
A type-level name is expected to create an event at runtime.
- Why it happens
- A checker computation was confused with executable code.
- How to fix it
- Implement registration or dispatch explicitly.
- Prevent it next time
- Document what remains after type erasure.
A conditional behaves unexpectedly for a union.
- Why it happens
- Distribution was not considered.
- How to fix it
- Test each alternative or suppress distribution deliberately.
- Prevent it next time
- Include never and broad types in type tests.
The checker reports excessive instantiation depth.
- Why it happens
- Recursive type work exceeds practical compiler limits.
- How to fix it
- Simplify or bound the model instead of suppressing the error.
- Prevent it next time
- Measure consumer compile cost before publishing complex types.
Check your understanding
1. Compute ElementOf<string | number[]> and explain which feature makes the result. Does that computation inspect any runtime value?
Show worked solution
The naked T conditional distributes: the string alternative remains string and the array alternative yields number, producing string | number. No runtime value is inspected and no conversion occurs. A function receiving unknown input still needs executable checks before using either interpretation. Add compile-time assignments for both accepted result alternatives.
2. Extend Field with "enabled". State the new accepted event name and one runtime responsibility that does not change.
Show worked solution
enabledChanged becomes part of EventName. The type update does not create a handler, validate an incoming payload, or authorize the event source. The runtime must still register or route the event and check its actual data. Test both the new compile-time accepted name and the implementation branch that handles it.
Key takeaways
- Conditional types compute assignability relationships.
- Template literal types generate types, not runtime services.
- Bound complexity and test the implementation contract too.