TypeScript Deep Dives · Part 4
Make TypeScript check your strings: template literal & mapped types
Event names, routes, and keys typed as `string` let typos ship. Learn how template literal types and mapped types turn magic strings into compile-time-checked unions, without wrecking build times.
Stringly-typed is untyped
Your codebase is full of strings TypeScript never really checks:
- Event names:
"user:updated" - API routes:
"GET /users/:id" - Permission keys, feature flags, query params
If they are typed as string, this compiles:
emit("user:updatd", payload); // typo, runtime bugA typo in a magic string is a runtime bug waiting to happen.
Template literal types
Model the string as a type:
type Entity = "user" | "order";
type Action = "created" | "updated" | "deleted";
type EventName = `${Entity}:${Action}`;
// "user:created" | "user:updated" | ... | "order:deleted"
function emit(event: EventName, payload: unknown) {
// ...
}
emit("user:updated", {}); // OK
emit("user:updatd", {}); // ErrorNow every call site gets autocomplete and typo protection.
Routes follow the same idea:
type Method = "GET" | "POST";
type Route = `${Method} /${string}`;You can tighten Path further with unions of known segments when your surface is finite.
Mapped types: one source, many shapes
Mapped types transform an object type without duplicating keys by hand:
type User = {
id: string;
email: string;
};
type Handlers = {
[K in keyof User as `on${Capitalize<K>}Change`]: (value: User[K]) => void;
};
// onIdChange, onEmailChange, always in sync with UserCombine keyof, indexed access, and mapped types to derive:
- Event maps from entities
- Form handlers from models
- Permission sets from role tables
Generate the string unions from the data, never hand-maintain both.
Know when to stop
Advanced types are powerful. They can also:
- Slow
tsc(see TypeScript 7 / build performance) - Become unreadable when recursion and conditionals nest deeply
Rules of thumb:
- Name intermediate types
- Cap recursion depth
- Prefer finite unions over open-ended recursive parsers when a domain list is known
- If a type needs a paragraph of comments to explain, simplify the model
Bottom line
- type Event = string;
+ type Event = `${Entity}:${Action}`;Routes, events, keys, checked and autocompleted. That is how TypeScript earns its keep on “just strings.”
FAQ
What are TypeScript template literal types?+
Template literal types build string types from other types, similar to JavaScript template strings. For example `` `${Entity}:${Action}` `` creates a union of valid event-name strings the compiler can check.
How do mapped types help with stringly-typed APIs?+
Mapped types transform one object type into another, for example turning a data model into an event map or a set of handlers, so keys stay derived from a single source of truth.
Can advanced string types slow down `tsc`?+
Yes. Deep recursive conditionals and huge generated unions can hurt compile times. Name intermediate types, avoid unnecessary recursion, and keep unions bounded, see our TypeScript performance notes.
TypeScript workshops for teams
We map your domain’s routes, events, and keys into checked types on your real codebase.
View workshops →Keep learning
TypeScript generics that stop fighting you
Good generics constrain and infer. Callers pass values and get precise types back, with no `<T>` by hand.
End-to-end TypeScript type safety without codegen
One schema, inferred everywhere: database → API → UI. A mismatch fails CI, not production.
TypeScript `satisfies` vs `as`: stop silencing the compiler
`as` isn’t a conversion, it’s a promise. `satisfies` checks the value and keeps the exact inferred shape.