Difficulty: Advanced
Mapped types in TypeScript allow you to create new types by transforming each property of an existing type. The syntax `{ [K in keyof T]: NewType }` iterates over every key `K` in type `T` and produces a new type with potentially different value types, modifiers, or names. This is the mechanism behind many of TypeScript's built-in utility types like `Partial`, `Readonly`, and `Required`.
The `keyof` operator extracts the union of all property names from a type as string literal types. For example, `keyof { name: string; age: number }` produces `"name" | "age"`. Combined with the `in` operator in a mapped type, you can iterate over these keys and apply transformations. This is the foundation for creating generic type transformations.
Mapped type modifiers let you add or remove `readonly` and `?` (optional) markers. Prefixing with `+` adds the modifier and `-` removes it. For instance, `{ [K in keyof T]-?: T[K] }` removes the optional modifier from all properties, making them required. Similarly, `{ [K in keyof T]+readonly: T[K] }` makes all properties readonly. The `+` prefix is optional and implied when no sign is present.
Property remapping with `as` clauses lets you transform key names during mapping. The syntax `{ [K in keyof T as NewKey]: T[K] }` allows filtering, renaming, or creating entirely new key patterns. Combined with template literal types, you can produce powerful transformations like generating getter/setter method names from property names.
type User = {
name: string;
age: number;
email: string;
};
// Make all properties optional
type OptionalUser = { [K in keyof User]?: User[K] };
// Make all properties readonly
type ReadonlyUser = { readonly [K in keyof User]: User[K] };
// Make all properties string
type StringifiedUser = { [K in keyof User]: string };
const partial: OptionalUser = { name: "Alice" };
const frozen: ReadonlyUser = { name: "Bob", age: 25, email: "bob@test.com" };
const stringed: StringifiedUser = { name: "Carol", age: "30", email: "carol@test.com" };
console.log(partial.name);
console.log(frozen.age);
console.log(stringed.age);
Three mapped types transform User differently: OptionalUser makes all properties optional, ReadonlyUser makes them readonly, and StringifiedUser changes all value types to string.
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Config {
host: string;
port: number;
}
const nullable: Nullable<Config> = { host: null, port: 8080 };
console.log(nullable.host);
console.log(nullable.port);
// Getters<Config> produces:
// { getHost: () => string; getPort: () => number }
const getters: Getters<Config> = {
getHost: () => "localhost",
getPort: () => 3000
};
console.log(getters.getHost());
console.log(getters.getPort());
Nullable<T> adds null to every property type. Getters<T> uses key remapping with `as` and template literals to generate getter method names from property names.
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
active: boolean;
}
// OnlyStrings<Mixed> = { name: string; email: string }
const stringsOnly: OnlyStrings<Mixed> = {
name: "Alice",
email: "alice@test.com"
};
console.log(stringsOnly.name);
console.log(stringsOnly.email);
The `as` clause uses a conditional type to filter out non-string properties by mapping them to `never`, which removes them from the resulting type.
keyof, in operator, mapped type modifiers, property remapping, generic mapped types