Utility and Advanced Types · Lesson 16 of 17
Mapped, Conditional and Template Literal Types
The utility types from the last lesson aren't magic – they're written in TypeScript itself using mapped types and conditional types. Learning these lets you read library types (React, Prisma, tRPC) and write your own when the built-ins aren't enough. This is the pro end of TypeScript, so focus on the patterns rather than memorising syntax.
In this lesson
- Mapped types and key remapping
- Conditional types and
infer - Template literal types
as constandsatisfies
Mapped types
A mapped type loops over keys: { [K in keyof T]: … }. This is how Partial is defined:
type MyPartial<T> = { [K in keyof T]?: T[K] }; // add ?
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // remove readonly
type Flags<T> = { [K in keyof T]: boolean }; // change every value type
interface Settings {
readonly darkMode: boolean;
fontSize: number;
language: string;
}
const touched: Flags<Settings> = { darkMode: true, fontSize: false, language: false };
const partial: MyPartial<Settings> = { fontSize: 18 };
const editable: Mutable<Settings> = { darkMode: false, fontSize: 16, language: "en" };
editable.darkMode = true; // allowed: readonly removed
console.log(touched, partial, editable.darkMode);
{ darkMode: true, fontSize: false, language: false } { fontSize: 18 } trueKey remapping with as
type Getters<T> = {
[K in keyof T & string as `get${Capitalize<K>}`]: () => T[K];
};
interface Profile {
name: string;
age: number;
}
function makeGetters(p: Profile): Getters<Profile> {
return { getName: () => p.name, getAge: () => p.age };
}
const g = makeGetters({ name: "Meera", age: 29 });
console.log(g.getName(), g.getAge() + 1);
Meera 30
Conditional types and infer
A conditional type picks a type based on a test: T extends U ? X : Y. With infer you can pull a type out of another one:
type IsString<T> = T extends string ? "yes" : "no";
type ElementOf<T> = T extends readonly (infer E)[] ? E : never; // infer the element
type Unwrap<T> = T extends Promise<infer V> ? Unwrap<V> : T; // recursive, like Awaited
type A = IsString<"hi">; // "yes"
type B = IsString<42>; // "no"
type C = ElementOf<string[]>; // string
type D = Unwrap<Promise<Promise<number>>>; // number
const a: A = "yes";
const b: B = "no";
const c: C = "TypeScript";
const d: D = 2026;
console.log(a, b, c.length, d);
// distributive: a union is checked member by member
type NonString<T> = T extends string ? never : T;
const x: NonString<string | number | boolean> = 7; // number | boolean
console.log(typeof x);
yes no 10 2026 number
How NonString<string | number | boolean> is evaluated
- Splitstring, number and boolean are tested one at a time
- Teststring → never, number → number, boolean → boolean
- Joinnever | number | boolean = number | boolean
Template literal types
type Size = "sm" | "md" | "lg";
type Color = "primary" | "danger";
type ButtonClass = `btn-${Color}-${Size}`; // all 6 combinations
type EventName<T extends string> = `on${Capitalize<T>}`;
type Handlers = { [E in "click" | "submit" as EventName<E>]: () => string };
type Route = `/courses/${string}` | "/";
function navigate(route: Route): string {
return `navigating to ${route}`;
}
const cls: ButtonClass = "btn-danger-sm";
const handlers: Handlers = { onClick: () => "clicked", onSubmit: () => "submitted" };
console.log(cls, handlers.onSubmit(), navigate("/courses/typescript"));
// navigate("/blog/x"); // error: not assignable to Route
btn-danger-sm submitted navigating to /courses/typescript
as const and satisfies
const ROLES = ["admin", "editor", "viewer"] as const; // readonly tuple of literals
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
type RouteConfig = Record<string, { path: string; auth: boolean }>;
const routes = {
home: { path: "/", auth: false },
dashboard: { path: "/dashboard", auth: true },
} satisfies RouteConfig; // checked against the type, but keeps the exact keys
const role: Role = "editor";
console.log(ROLES.includes(role), routes.dashboard.path, Object.keys(routes));
// routes.profile; // error: 'profile' doesn't exist – a plain annotation would allow any key
true /dashboard [ 'home', 'dashboard' ]
satisfies vs a type annotation
const routes: RouteConfig = … widens the type to Record<string, …>, so TypeScript forgets which keys exist. satisfies RouteConfig checks the value against the type but keeps its precise, inferred type. Use it for config objects, theme tokens and route maps.
Don't over-engineer
Advanced types are powerful but can make code hard to read and slow to compile. Reach for them in shared libraries and core models; in everyday application code, plain interfaces and the built-in utilities are usually clearer.
Practice
Try it yourself
Write
Nullable<T>that makes every propertyT[K] | null, and use it for a form that starts empty.Show solution
type Nullable<T> = { [K in keyof T]: T[K] | null }; interface Signup { name: string; age: number } const empty: Nullable<Signup> = { name: null, age: null }; const filled: Nullable<Signup> = { ...empty, name: "Kiran" }; console.log(empty, filled);{ name: null, age: null } { name: 'Kiran', age: null }
Quick check
What does -readonly do in a mapped type?
Removes the readonly modifier from each property.
What does infer do?
Inside a conditional type, it captures part of a type into a new type variable.
Why use satisfies instead of : Type?
It checks the value but keeps the narrower inferred type.
Summary
- Mapped types loop over keys;
asremaps key names. - Conditional types choose types; infer extracts types; they distribute over unions.
- Template literal types build string patterns; as const and satisfies keep precise types.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.