---
title: 'Mapped Types'
source: 'https://academia.sh/en/courses/typescript/mapped-types'
course: TypeScript
language: en
updated: '2026-08-17T18:09:53+00:00'
license: 'CC BY-SA 4.0'
---

# Mapped Types

Producing a new type from an existing one; adding and removing field modifiers, key remapping, filtering fields, and how mapping behaves on arrays and tuples.

The various shapes of a measurement record have so far been written out by hand: a
read-only version for archived records, a version with optional fields for an update
request, a summary carrying only a few fields for listing.

Each is a transform of the original type. When written by hand, they need to be
updated together whenever the original type changes, and that is a maintenance debt
easy to forget. A **mapped type** computes the transform at the type level: the field
list stays in one place, and the derived types update themselves.

## Basic Notation

A mapped type resembles a loop iterating over a set of keys:

```typescript
interface Measurement {
  id: string;
  value: number;
  unit: "C" | "Pa" | "%";
}

type Locked<T> = { readonly [A in keyof T]: T[A] };
type Patchable<T> = { [A in keyof T]?: T[A] };

const archived: Locked<Measurement> = {
  id: "s-01",
  value: 21.4,
  unit: "C",
};
console.log(archived.value);

const patch: Patchable<Measurement> = { value: 22.1 };
console.log(patch.value, patch.id);
```

Output:

```text
21.4
22.1 undefined
```

If the line `archived.value = 22.1;` is added at the end of the file, after a blank
line:

```text
o2.ts(20,10): error TS2540: Cannot assign to 'value' because it is a read-only property.
```

The notation has these parts. The `[A in keyof T]` part binds each member of the
`keyof T` union to the name `A` in turn — the type-level counterpart of the `for` loop
from the Programming Fundamentals course. `T[A]` after the colon is that field's
original type. The `readonly` written at the front and the `?` appended to the name
are **modifiers** applied to every field.

Because every field of `Patchable<Measurement>` is optional, the literal
`{ value: 22.1 }` is accepted; the `patch.id` access has type `string | undefined`, and
`undefined` appears in the output.

The counterparts of these two transforms already exist in the standard library
(`Readonly` and `Partial`); they are covered in the Utility Types lesson. The reason
they are written by hand here is to make the mechanism visible.

## Removing a Modifier

Just as modifiers can be added, they can be removed. Removal is written by putting a
minus sign in front of the modifier:

```typescript
interface Measurement {
  readonly id: string;
  readonly value?: number;
}

type Resolved<T> = { -readonly [A in keyof T]-?: T[A] };

const full: Resolved<Measurement> = { id: "s-01", value: 21.4 };
full.value = 22.1;
console.log(full.value.toFixed(1));
```

The output is `22.1`. If the line
`const missing: Resolved<Measurement> = { id: "s-02" };` is added at the end of the
file, after a blank line:

```text
o4.ts(12,7): error TS2741: Property 'value' is missing in type '{ id: string; }' but required in type 'Resolved<Measurement>'.
```

In type `Measurement`, `id` is read-only and `value` was both read-only and optional.
In type `Resolved<Measurement>`, both are writable and required. The result is that the
`full.value.toFixed(1)` call needs no narrowing: the field is now `number`, not
`number | undefined`.

This transform's use case is representing the completed form of a partial record.
Rather than writing a separate type for the record obtained after defaults have been
applied, it is computed from the original type.

## Key Remapping

The `as` notation lets the name of the produced field be changed:

```typescript
interface Measurement {
  id: string;
  value: number;
  unit: "C" | "Pa" | "%";
}

type Readers<T> = {
  [A in keyof T as `read${Capitalize<string & A>}`]: () => T[A];
};

const reader: Readers<Measurement> = {
  readId: () => "s-01",
  readValue: () => 21.4,
  readUnit: () => "C",
};

console.log(reader.readValue().toFixed(1), reader.readId());
```

The output is `21.4 s-01`.

The produced type declares a reader function for every field and takes the return type
from the original field: the call `reader.readValue()` gives `number`, the call
`reader.readUnit()` gives `"C" | "Pa" | "%"`. The template literal notation used to
produce the name, along with the `Capitalize` transform, is the subject of the next
lesson; here it should only be seen that the name can be computed.

Remapping's second and more powerful use is **filtering out** a field. When the
produced name is `never`, that field does not enter the output:

```typescript
interface Measurement {
  id: string;
  value: number;
  unit: "C" | "Pa" | "%";
  time: number;
}

type NumericFields<T> = {
  [A in keyof T as T[A] extends number ? A : never]: T[A];
};

const numeric: NumericFields<Measurement> = { value: 21.4, time: 1706000000000 };
console.log(numeric.value + numeric.time);
```

The output is `1706000000021.4`. If the line
`const wrong: NumericFields<Measurement> = { value: 21.4, time: 1, id: "s-01" };` is
added at the end of the file, after a blank line:

```text
o7.ts(15,67): error TS2353: Object literal may only specify known properties, and 'id' does not exist in type 'NumericFields<Measurement>'.
```

The resulting type carries only the `value` and `time` fields; `id` and `unit` have
been filtered out. The notation `T[A] extends number ? A : never` is a **conditional
type** and is the subject of one of the next lessons; here its job is to keep the name
based on the field's type or produce `never` and filter it out.

This type can be used in the signature of code that extracts a numeric summary of a
measurement record. When a new numeric field is added to the record, the summary type
grows on its own; when a string field is added, it is unaffected.

## Mapping over Arrays and Tuples

When a mapping iterating over `keyof T` is applied to an array, nothing unexpected
**happens**: the result is still an array.

```typescript
type Locked<T> = { readonly [A in keyof T]: T[A] };

const values: Locked<number[]> = [21.4, 22.1];
const range: Locked<[min: number, max: number]> = [-40, 85];

console.log(values.length, values[0], range[1]);
console.log(values.map((d) => d * 2));
```

Output:

```text
2 21.4 85
[ 42.8, 44.2 ]
```

`Locked<number[]>` is the type `readonly number[]`, `Locked<[number, number]>` is a
read-only tuple. Array methods (`map`, `length`, indexed access) remain usable, mutating
methods are gone:

```typescript
type Locked<T> = { readonly [A in keyof T]: T[A] };

const values: Locked<number[]> = [21.4, 22.1];
values.push(23.0);
```

```text
u4.ts(4,8): error TS2339: Property 'push' does not exist on type 'readonly number[]'.
```

This behavior holds when the mapping's source is **directly** `keyof T`. The compiler
recognizes this form and keeps arrays and tuples in their own kind. When the source is
written as any other expression, the recognition breaks:

```typescript
type MapA<T> = { readonly [A in keyof T]: T[A] };
type MapB<T> = { readonly [A in keyof T & string]: T[A] };

declare const a: MapA<number[]>;
declare const b: MapB<number[]>;

const x: null = a;
const y: null = b;
```

```text
u11.ts(7,7): error TS2322: Type 'readonly number[]' is not assignable to type 'null'.
u11.ts(8,7): error TS2322: Type 'MapB<number[]>' is not assignable to type 'null'.
```

The difference between the two diagnostics is decisive. `MapA<number[]>` resolved to
`readonly number[]` — the compiler is printing the result as an array. `MapB<number[]>`
stayed an unresolved mapping — the array structure was lost, replaced by an ordinary
object type carrying the array's property names.

Practical rule: **if a type's structure needs to be preserved, the mapping source is
written as `keyof T`.** If keys need to be filtered, the filtering is done with an
`as` remapping — the source stays `keyof T`, and the name computation lives on the
right side.

## Mapping over Any Union

The mapping source does not have to be `keyof T`; any string union can be used:

```typescript
type Unit = "C" | "Pa" | "%";

type Intervals = {
  [B in Unit]: [min: number, max: number];
};

const intervals: Intervals = {
  C: [-40, 85],
  Pa: [0, 200000],
  "%": [0, 100],
};

console.log(intervals.C[1], intervals["%"][1]);
```

The output is `85 100`. If the line
`const missing: Intervals = { C: [-40, 85], Pa: [0, 200000] };` is added at the end of
the file, after a blank line:

```text
o9.ts(15,7): error TS2741: Property '"%"' is missing in type '{ C: [number, number]; Pa: [number, number]; }' but required in type 'Intervals'.
```

This is the correct alternative to the index signature criticized in the Interfaces
lesson. An index signature says "every string key is valid" and would not have
counted access to a nonexistent key as an error. A mapped type, by contrast, fixes the
key set **exactly**: a missing key produces a diagnostic, and so does an extra one.

When a new unit is added to the `Unit` union, updating the interval table becomes
mandatory. This is the data-table counterpart of the exhaustiveness checking built with
`never` in a discriminated union.

## Summary

- A mapped type iterates over a key set and produces a new object type; derived types
  update together with the original type.
- The `readonly` and `?` modifiers can be added during mapping and removed with the
  `-readonly` and `-?` notations.
- The `as` notation computes the produced field's name; when the name is `never`, the
  field is filtered out of the output.
- The mapping source does not have to be `keyof T`; mapping can be done over any
  string union.
- When the mapping source is directly `keyof T`, arrays and tuples stay in their own
  kind; when the source is written as another expression, this structure is lost.
- For a fixed key set, a mapped type is safer than an index signature: both a missing
  and an extra key are reported.

## Next Step

In the previous two examples, a notation of the form `T[A] extends number ? A : never`
appeared and branched at the type level. This is the structure at the center of the
type system's computing ability. The next lesson covers conditional types, distribution
rules, and the `infer` notation that extracts a piece from within a type.
