Learn/TypeScript Deep Dives

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.

8 min read
TypeScriptTemplate literal typesMapped types

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:

ts
emit("user:updatd", payload); // typo, runtime bug

A typo in a magic string is a runtime bug waiting to happen.

Template literal types

Model the string as a type:

ts
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", {}); // Error

Now every call site gets autocomplete and typo protection.

Routes follow the same idea:

ts
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:

ts
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 User

Combine 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:

Rules of thumb:

  1. Name intermediate types
  2. Cap recursion depth
  3. Prefer finite unions over open-ended recursive parsers when a domain list is known
  4. If a type needs a paragraph of comments to explain, simplify the model

Bottom line

ts
- 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