TypeScript: Mapped Types and Template Literal Types
An advanced TypeScript type-safety question is:
What are Mapped Types and Template Literal Types in TypeScript? How do you use key remapping (the
asclause) to dynamically transform object key names or filter keys during compilation?
Mapped types let you take an existing type and transform each of its properties into a new type. Combined with template literal types (introduced in TS 4.1), you can build complex, automated type-safety transformations for state stores, API clients, and theme configurations.
1. What is a Mapped Type?
A mapped type is a generic type that iterates over a union of keys (usually created via keyof) to construct an object type:
type ReadOnly<T> = {
readonly [P in keyof T]: T[P];
};
interface User {
id: number;
name: string;
}
type ReadOnlyUser = ReadOnly<User>;
// Result:
// {
// readonly id: number;
// readonly name: string;
// }Here, [P in keyof T] acts as a compile-time loop that maps over each property name P in keyof T and resolves its value type as T[P].
2. Using Mapping Modifiers
You can add or remove property modifiers (like readonly or optional ?) during the mapping loop using prefix operators + (default) or -:
// Remove the optional modifier from all properties
type Concrete<T> = {
[P in keyof T]-?: T[P];
};
interface MaybeUser {
id: number;
name?: string;
age?: number;
}
type StrictUser = Concrete<MaybeUser>;
// Result: { id: number; name: string; age: number; }3. Template Literal Types
Template literal types have the same syntax as ES6 template strings but are resolved entirely at the type level. They combine strings to generate new unions:
type Position = "Top" | "Bottom";
type Alignment = "Left" | "Right";
type BoxAnchor = `${Position}-${Alignment}`;
// Result: "Top-Left" | "Top-Right" | "Bottom-Left" | "Bottom-Right"TypeScript also provides utility types for string transformations: Uppercase, Lowercase, Capitalize, and Uncapitalize.
4. Key Remapping with as
Key remapping lets you change key names or exclude specific keys altogether during the mapping loop using the as keyword.
A. Renaming Keys (Getter Generation)
Suppose you want to take an object and dynamically generate a type representing all its getter methods:
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// Result:
// {
// getName: () => string;
// getAge: () => number;
// }B. Filtering Keys (Key Omission)
You can filter out keys by remapping them to never. If a key evaluates to never, TypeScript excludes it from the final object:
// Filter out properties that are not functions
type FunctionPropertiesOnly<T> = {
[P in keyof T as T[P] extends Function ? P : never]: T[P];
};
interface Controller {
data: string[];
fetchData: () => Promise<void>;
saveData: () => void;
}
type ControllerActions = FunctionPropertiesOnly<Controller>;
// Result:
// {
// fetchData: () => Promise<void>;
// saveData: () => void;
// }Senior-Level Interview Answer
Mapped types iterate over a set of keys to dynamically generate object structures. Template literal types build upon string union types, allowing string concatenations and modifications to resolve at compile-time. We can combine these features using key remapping via the
asclause to rename keys dynamically (e.g. capitalizing properties to output getter signatures) or filter out keys. By resolving key definitions toneverinside a conditional type check during theasremapping cycle, TypeScript excludes those properties entirely, providing a declarative mechanism to implement custom utility transformations.
Common Interview Mistakes
❌ Forgetting the string & P constraint in template strings
When mapping properties (P in keyof T), keyof T can include strings, numbers, or symbol types. If you try to concatenate P inside a template literal directly (like ${P}), TypeScript will throw a compilation error. You must constraint P by intersection, e.g., ${string & P}, or check its type using conditionals.
❌ Using mapped types inside interfaces
Mapped types are type aliases and cannot be declared inside standard interfaces. Attempting to write interface MyObj { [K in keyof T]: T[K] } is syntactically invalid. You must use type MyObj<T> = { [K in keyof T]: T[K] }.
Key Takeaways
- Type Loop: Mapped types behave like compile-time loops, mapping over unions of keys to generate object definitions.
- Modifiers Tuning: Prefix symbols
+and-add or remove modifiers (like optional?orreadonly) during mapping loops. - Literal Union: Template literal types evaluate string patterns at the type level to compile complex combinations.
- Key Remapping: The
askeyword renames keys dynamically or filters them based on conditional type evaluations. - Exclusion via Never: Evaluating a key as
neverinside a key remapping clause removes that property from the resulting type.