Mapped Types: Systematically Transforming Existing Types
AI generated
<T>
type
TypeScript · Mapped Types · Type-Level Programming
Mapped Types: Systematically Transforming Existing Types
Modifiers, the minus-prefix, and key remapping with as

Maintaining a separate hand-written interface for every variant of a TypeScript type eventually causes drift between the original and its copies. Mapped types systematically transform existing types with the K in keyof T syntax, apply and strip readonly and optional modifiers on demand, and rename keys with as, all without duplicating a single field.

14 min. read keyof · readonly · optional · as-remapping TypeScript 5.x

1. What mapped types actually solve

A mapped type systematically transforms an existing type by iterating over its property keys and deriving a new property for each one. The alternative is maintaining two or more interfaces by hand that describe the same field list, just with different modifiers such as readonly or optional. The moment someone adds, renames, or removes a field on the source type, that change has to be repeated manually in every copy, which in practice almost always gets forgotten.

Mapped types solve exactly this synchronization problem structurally: instead of maintaining a copy, the new type is defined as a function of the source type. If Product changes, Partial<Product>, Readonly<Product>, and any custom derived type automatically change along with it. This property makes mapped types the foundation of nearly every built-in utility type in TypeScript and one of the most important tools for type-safe libraries, API clients, and form validation.

2. Base syntax: { [K in keyof T]: ... }

The base syntax of a mapped type is { [K in keyof T]: T[K] }, and it reads almost like a for-in loop at the type level. keyof T produces a union of all property keys of T, K in ... iterates over every member of that union, and T[K] looks up the type of the corresponding property. The result is a new object type with the same structure as T, except each field can be freely transformed before it lands in the new type.

The key source doesn't have to come from keyof T at all. Any union of string, number, or symbol literals works as a source, which makes mapped types useful for generic helper types too, such as a Flags<Keys> pattern that turns a list of feature names into an object with a boolean value for each name. This flexibility is what sets mapped types apart from plain utility types: they're a general language feature, not a fixed collection of pre-built special cases.


// Basic mapped type: iterate over every key of T
type Stringify<T> = {
  [K in keyof T]: string;
};

interface Product {
  id: number;
  name: string;
  inStock: boolean;
}

// Every property becomes a string, keys and structure stay the same
type StringifiedProduct = Stringify<Product>;
// { id: string; name: string; inStock: string }

// Mapped types work with any key source, not just keyof
type Flags<Keys extends string> = {
  [K in Keys]: boolean;
};

type FeatureFlags = Flags<"darkMode" | "betaCheckout" | "newSearch">;
// { darkMode: boolean; betaCheckout: boolean; newSearch: boolean }

3. Modifiers: applying readonly and optional on purpose

Inside the square brackets of a mapped type, two modifiers can be prepended: readonly makes the generated property immutable, and a question mark after the key makes it optional. Both modifiers can be applied independently, combined, or left off entirely. { readonly [K in keyof T]: T[K] } turns every field into a read-only counterpart, while { [K in keyof T]?: T[K] } makes every field optional without altering the underlying value types at all.

The important distinction is between a modifier inside a mapped type and the same annotation written directly on the source type: the mapped type doesn't rewrite the modifier field by field by hand, it applies it structurally to every key at once. That not only saves typing, it also guarantees no field gets missed during manual copying whenever the source type grows another property.

4. Removing modifiers: the minus-prefix -readonly and -?

Modifiers can not only be added, they can be removed on purpose with a leading minus sign. -readonly before the key strips a previously applied readonly modifier, and -? removes the optional flag and makes a field mandatory again. This is particularly useful when an already-transformed type serves as the starting point for a further transformation, for example when a draft type with optional fields needs to be fully filled in again before it's persisted.

Without the minus-prefix, you'd have to hand-write a completely new type for that case that just happens to share the original's structure. With -readonly and -?, the derivation stays transparently traceable: anyone reading the code immediately sees that CompleteOrder matches Order exactly, just routed through an optional intermediate step. The + in front of a modifier, incidentally, is the default and is almost never written out explicitly, since TypeScript assumes it implicitly.


// Adding modifiers: readonly and optional
type Frozen<T> = {
  readonly [K in keyof T]: T[K];
};

type Draft<T> = {
  [K in keyof T]?: T[K];
};

interface Order {
  id: number;
  total: number;
  items: string[];
}

type FrozenOrder = Frozen<Order>;
// { readonly id: number; readonly total: number; readonly items: string[] }

// Removing modifiers with the minus prefix
type Unfrozen<T> = {
  -readonly [K in keyof T]: T[K];
};

type Complete<T> = {
  [K in keyof T]-?: T[K];
};

type MutableOrder = Unfrozen<FrozenOrder>;
// readonly is stripped again, all properties become writable

type PartialOrder = Draft<Order>;
type CompleteOrder = Complete<PartialOrder>;
// -? removes the optional modifier added by Draft, all fields required again

5. Rebuilding Partial<T> from scratch

The built-in utility type Partial<T> from lib.es5.d.ts is itself nothing more than a mapped type: { [K in keyof T]?: T[K] }. Once you've typed that one line yourself, it's immediately obvious that Partial<T> isn't magic, it's just one of many possible derivations of the base syntax. That realization is the actual learning outcome here: once the mapped-type syntax clicks, you can not only understand the built-in utility types, you can build your own project-specific variants for whatever your codebase needs.

In practice, a hand-rolled MyPartial<T> works great for PATCH endpoints or update functions where the caller should only send the fields that actually changed. The compiler still enforces that no unknown field names or wrong value types slip through, since the structure remains derived from Customer. If Customer gains a new field, MyPartial<Customer> is automatically up to date, with nobody needing to maintain the patch type by hand.


// Reimplementing the built-in Partial<T> utility type
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

interface Customer {
  id: number;
  email: string;
  newsletter: boolean;
}

// Every property becomes optional, same shape otherwise
type CustomerPatch = MyPartial<Customer>;

function updateCustomer(id: number, patch: MyPartial<Customer>): void {
  // patch.email, patch.newsletter, patch.id are all optional here
  // the caller only needs to send the fields that actually changed
}

updateCustomer(42, { newsletter: false });
updateCustomer(42, { email: "new@example.com", newsletter: true });

// This is exactly what lib.es5.d.ts ships as the built-in Partial<T>

6. Implementing Readonly<T> and Required<T> yourself

Readonly<T> and Required<T> follow the same construction principle as Partial<T>, just with different modifiers. Readonly<T> puts readonly in front of every key, making an object immutable after initialization, which is especially useful for configuration objects or Redux-style state structures that shouldn't be mutated by accident. Required<T> instead uses -? and strips every optional flag, turning a type with partially optional fields into one where every field must be present.

Both can be combined freely, for example in Required<Readonly<Config>> to produce a fully populated, immutable configuration object right after loading it from an environment variable. It's important to note that readonly only applies at compile time: it prevents assignments in the TypeScript compiler but has no runtime effect whatsoever. Anyone who needs actual runtime immutability also has to call Object.freeze(), since the type system alone only guards against accidental assignments in your own code.


// Reimplementing Readonly<T> and Required<T>
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

interface Config {
  apiUrl: string;
  timeoutMs?: number;
  retries?: number;
}

type FrozenConfig = MyReadonly<Config>;
const config: FrozenConfig = { apiUrl: "https://api.mironsoft.de", timeoutMs: 5000, retries: 3 };
// config.apiUrl = "other"; // Error: cannot assign to read-only property

type FullConfig = MyRequired<Config>;
// timeoutMs and retries lose their ? and become mandatory

function bootstrap(config: FullConfig): void {
  // no need for `config.timeoutMs ?? 5000` fallbacks anymore
}

7. Key remapping with as: renaming keys

Since TypeScript 4.1, the as clause inside a mapped type allows transforming not just the value type but the key name itself: { [K in keyof T as NewKey]: T[K] }. Combined with template literal types, this produces powerful derivations such as a Getters<T> type that automatically turns every property into a matching getter method prefixed with get and a capitalized field name, without listing the individual methods by hand.

Key remapping is the building block that turns mapped types from a pure modifier-application tool into a full-fledged transformation mechanism. Capitalize<string & K> in the template literal expression is necessary because K could theoretically also be a symbol or a number, and TypeScript only works with string keys inside template literal types. The & string narrows the key down to the string case before Capitalize is applied.


// Key remapping with `as`: turn properties into getter method names
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Product {
  id: number;
  name: string;
  price: number;
}

type ProductGetters = Getters<Product>;
// { getId: () => number; getName: () => string; getPrice: () => number }

// Filtering keys: remap unwanted keys to `never` to drop them entirely
type OmitByType<T, TypeToOmit> = {
  [K in keyof T as T[K] extends TypeToOmit ? never : K]: T[K];
};

// Removes every property whose value type is a function
type OnlyDataFields = OmitByType<ProductGetters, (...args: never[]) => unknown>;
// {} - every property was a getter function, so all keys were filtered out

8. Filtering keys: conditional remapping with as never

When a key in the as expression is mapped to never, the corresponding property disappears entirely from the resulting type. This pattern enables conditional filtering: { [K in keyof T as T[K] extends Function ? never : K]: T[K] } keeps only the fields whose value type isn't a function, and drops all the others completely from the type, rather than merely setting them to never, which wouldn't remove the key itself.

The distinction between a value mapped to never and a key mapped to never matters a great deal: { [K in keyof T]: never } keeps every key, just with the value type never, while { [K in keyof T as never]: T[K] } collapses the type down to {} entirely. This filtering principle is the foundation of how built-in utility types like Pick<T, K> and Omit<T, K> work under the hood, even though Omit itself is technically defined via Exclude and Pick.

9. Mapped types compared side by side

Nearly every transformation shown so far can also be achieved without mapped types, simply by hand-writing the target type as a standalone interface. The difference only shows up once the source type changes: mapped types stay automatically in sync, while manually duplicated types drift apart the moment someone forgets to update the copy. The table below contrasts the naive, manual variant with the corresponding mapped-type solution.

Task Naive / manual Mapped-type pattern Benefit
Making every field optional Duplicate each field manually with ? Partial<T> One place to change, no drift
Making every field readonly Rewrite the interface from scratch Readonly<T> Immutability without code duplication
Making optional fields mandatory again Maintain a separate interface Required<T> with -? Stays in sync with the base type
Turning properties into getter methods Write one method per property by hand Key remapping with as Scales automatically with new fields
Removing specific fields from a type Maintain a new interface without those fields as never in the mapped type Type stays coupled to the source

In larger codebases with many domain types, this decoupling pays off especially well: a single Product interface from which every read, write, and patch variant is derived via mapped types can be extended in exactly one place. Manually maintained parallel interfaces, on the other hand, produce exactly the kind of silent drift that code reviews rarely catch reliably, because both types still compile correctly even after they've stopped describing the same thing.

Mironsoft

TypeScript tooling, type safety, and build pipelines for Magento and Hyvä projects

Looking for type-safe build scripts and frontend tooling?

We build TypeScript tooling for Magento and Hyvä projects, from type-safe build scripts to generated API clients and maintainable utility-type libraries that grow with your codebase instead of breaking with every change.

Type-level reviews

Auditing existing type definitions for redundancy, drift, and missing mapped-type patterns

Build tooling

Type-safe scripts for deployment, code generation, and CI/CD pipelines

Training & pairing

Hands-on onboarding for teams moving from JavaScript to TypeScript

10. Summary

Mapped types systematically transform existing types through the { [K in keyof T]: ... } syntax instead of duplicating them by hand. readonly and ? apply modifiers on purpose, -readonly and -? strip them again, and both variants can be freely combined. Partial<T>, Readonly<T>, and Required<T> aren't magic, they're just mapped types with a handful of lines of implementation, as the rebuild in this article shows.

Key remapping with as extends mapped types from a pure modifier-application tool into a full-fledged transformation mechanism: keys can be renamed, combined with template literal types, or dropped entirely from the result type by mapping them to never. Once you've mastered these building blocks, you not only understand the built-in utility types, you can build your own project-specific type transformations that automatically grow along with the source type.

Mapped Types: Systematically Transforming Existing Types - The Essentials at a Glance

Base syntax

{ [K in keyof T]: T[K] } iterates over every property key of a type and derives a new object type from it.

Modifiers

Apply readonly and ?, strip them again on purpose with -readonly and -?.

Utility types rebuilt from scratch

Partial, Readonly, and Required are just a handful of lines of mapped-type code, not magic.

Key remapping

as renames keys or drops them entirely from the result type via never.

11. FAQ: Mapped Types in TypeScript

1What is a mapped type in TypeScript?
A construct that iterates over the property keys of an existing type and systematically derives a new type from it, instead of manually duplicating interfaces.
2How is a mapped type different from a regular interface?
A mapped type is derived from another type and stays automatically in sync. A manually maintained interface has to be updated by hand with every change.
3What does the minus-prefix in -readonly and -? do?
Removes a previously applied modifier. -readonly makes a field writable again, -? makes it mandatory again.
4How is Partial<T> actually implemented?
Defined in lib.es5.d.ts as { [K in keyof T]?: T[K] }, a simple mapped type with no hidden special-case logic.
5Can mapped types also rename keys?
Yes, since TypeScript 4.1 via the as clause, combinable with template literal types for automatically derived names.
6How do I remove properties from a type?
Via conditional remapping to never inside the as expression. A key mapped to never disappears entirely from the result type.
7Homomorphic vs. non-homomorphic mapped types?
Homomorphic mapped types iterate over keyof T and carry over existing modifiers. Mapped types iterating over an explicit union don't.
8Do mapped types work with union types as the key source?
Yes, any union of string, number, or symbol literals works as the key source, not just keyof T.
9When should I use a custom mapped type instead of a built-in utility?
Whenever built-in utility types can't express the transformation, such as key remapping or conditional filtering by value type.
10Performance differences versus manual types?
None at runtime, types are erased at compile time. Deeply nested mapped types can noticeably increase build-time type-checking.