Types made from other types

Most of the time you write types by hand: an interface for a meter reading, a union of ticket statuses. In a growing code base, many types are variations on the same shape. A form needs every field of a customer as optional; an API answer leaves out the internal fields; an event system needs one handler per event name. Writing each variation by hand works, until someone adds a field to the original and forgets one of the copies.

TypeScript lets you compute such types from the original. The building blocks are on earlier pages: generics give types parameters, keyof and indexed access read keys and property types, and unions and literal types describe sets of values. This page adds the three tools that combine them: mapped types, conditional types and template literal types. Together they are what the utility types are made of.

Mapped types

A mapped type is an object type that is created by looping over a set of keys, usually keyof T, and giving each key a property type. The syntax looks like an index signature with in: { [K in keyof T]: NewType } reads as "for every key K of T, a property K of type NewType". Here every field of a meter reading becomes a Boolean that says whether the field was changed:

interface MeterReading {
  meterId: string;
  kWh: number;
  estimated: boolean;
}

// For every key K of MeterReading, make a property of type boolean.
type ChangedFields<T> = { [K in keyof T]: boolean };

const changes: ChangedFields<MeterReading> = {
  meterId: false,
  kWh: true,
  estimated: false,
};

const changed = Object.entries(changes)
  .filter(([, isChanged]) => isChanged)
  .map(([field]) => field);
console.log(changed);
[ 'kWh' ]

ChangedFields<MeterReading> is the type { meterId: boolean; kWh: boolean; estimated: boolean }. When someone adds a field to MeterReading, the compiler requires it in every ChangedFields object as well, with no extra work.

A mapped type walks over the keys of MeterReading. Each key, meterId, kWh and estimated, becomes a property of ChangedFields with the new type boolean. MeterReading meterId: string kWh: number estimated: boolean [K in keyof T] for every key ChangedFields meterId: boolean kWh: boolean estimated: boolean
A mapped type keeps the keys and replaces the property types. The result follows the original automatically.

Adding and removing readonly and ?

Inside a mapped type, T[K] is the original property type, so { [K in keyof T]: T[K] } is an exact copy. The interesting part is what you put around it. readonly and ? in front of or after the brackets add those modifiers to every property; with a minus sign, -readonly and -?, they are removed:

interface Tariff {
  readonly code: string;
  pricePerKWh?: number;
}

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

const draft: Mutable<Tariff> = { code: "DAY" };
draft.code = "NIGHT";

const full: Complete<Tariff> = { code: "DAY", pricePerKWh: 0.32 };
console.log(draft, full);
{ code: 'NIGHT' } { code: 'DAY', pricePerKWh: 0.32 }

In Complete<Tariff> the price is no longer optional: leave it out and the compiler reports Property 'pricePerKWh' is missing. A mapped type over keyof T also keeps the modifiers it does not touch, so a copy of a readonly property stays readonly unless you remove it.

Key remapping with as

Key remapping is an as clause inside a mapped type that gives each new property a different name than the key it came from: [K in keyof T as NewName]. The new name is usually a template literal type (see below). This turns every field of a customer into a getter function:

interface Customer {
  id: string;
  name: string;
  monthlyKWh: number;
}

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

function makeGetters(c: Customer): Getters<Customer> {
  return {
    getId: () => c.id,
    getName: () => c.name,
    getMonthlyKWh: () => c.monthlyKWh,
  };
}

const g = makeGetters({ id: "C-2201", name: "Lucía Romero", monthlyKWh: 212 });
console.log(g.getName(), g.getMonthlyKWh());
Lucía Romero 212

Capitalize makes the first letter uppercase, so "monthlyKWh" becomes "getMonthlyKWh". The string & K is needed because keys can also be numbers or symbols, and Capitalize only accepts strings. Forget a getter, and the error names the property the compiler calculated:

interface Customer {
  id: string;
  name: string;
  monthlyKWh: number;
}

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

const g: Getters<Customer> = {
  getId: () => "C-2201",
  getName: () => "Lucía Romero",
};
example.ts(11,7): error TS2741: Property 'getMonthlyKWh' is missing in type '{ getId: () => string; getName: () => string; }' but required in type 'Getters<Customer>'.

When the as clause produces never, the key is dropped. Combined with a conditional type, this filters properties, here keeping only the text fields of a ticket, minus an internal note that must not leave the service desk:

interface Ticket {
  id: string;
  title: string;
  openedAt: string;
  priority: number;
  internalNote: string;
}

// Keep only the keys whose value type is string, and drop internalNote.
type PublicStrings<T> = {
  [K in keyof T as T[K] extends string ? (K extends "internalNote" ? never : K) : never]: T[K];
};

const view: PublicStrings<Ticket> = {
  id: "INC-30412",
  title: "Portal login slow",
  openedAt: "2026-10-05T08:12:00Z",
};
console.log(Object.keys(view).join(", "));
id, title, openedAt

Conditional types

A conditional type chooses between two types with the same shape as the conditional operator in JavaScript: T extends U ? X : Y. If T is assignable to U, the result is X, otherwise Y. "Extends" here means "fits into", the same test as when you assign a value of type T to a variable of type U:

type Unit<T> = T extends number ? "kWh" : "text";

const a: Unit<4.2> = "kWh";
const b: Unit<"E-1001"> = "text";
const c: Unit<number> = "text";
example.ts(5,7): error TS2322: Type '"text"' is not assignable to type '"kWh"'.

4.2 and number fit into number, so both give "kWh"; only "E-1001" gives "text". The third line is the mistake. Conditional types can be chained like else if, and they are most useful inside a mapped type, where they decide something per property:

// The type of the value a meter field holds, decided by the field's type.
type FieldKind<T> =
  T extends number ? "numeric" :
  T extends string ? "text" :
  T extends boolean ? "flag" :
  "other";

interface MeterReading {
  meterId: string;
  kWh: number;
  estimated: boolean;
  takenAt: Date;
}

type Schema<T> = { [K in keyof T]: FieldKind<T[K]> };

const schema: Schema<MeterReading> = {
  meterId: "text",
  kWh: "numeric",
  estimated: "flag",
  takenAt: "other",
};
console.log(schema);
{
  meterId: 'text',
  kWh: 'numeric',
  estimated: 'flag',
  takenAt: 'other'
}

Note the order of the checks: a literal type such as "E-1001" fits into string, and Date is not a number, string or Boolean, so it ends in the last branch.

Distribution over unions

A distributive conditional type is a conditional type whose checked type is a bare type parameter, such as T extends …. When it receives a union, it is applied to each member separately, and the results are joined into a new union. ToArray<string | number> therefore becomes string[] | number[]: an array of only strings or of only numbers, not a mixed one. To switch distribution off, wrap both sides in square brackets:

type ToArray<T> = T extends unknown ? T[] : never;
type NotDistributed<T> = [T] extends [unknown] ? T[] : never;

// ToArray<string | number> is string[] | number[]
const ids: ToArray<string | number> = ["E-1001", "E-1002"];
const kWh: ToArray<string | number> = [4.2, 3.1];

// NotDistributed<string | number> is (string | number)[]
const mixed: NotDistributed<string | number> = ["E-1001", 4.2];
console.log(ids.length, kWh.length, mixed.length);
2 2 2

With the distributive version, a mixed array is an error, and the message shows the union the compiler built:

type ToArray<T> = T extends unknown ? T[] : never;

const mixed: ToArray<string | number> = ["E-1001", 4.2];
example.ts(3,7): error TS2322: Type '(string | number)[]' is not assignable to type 'string[] | number[]'.
ToArray applied to string or number. The union is split into its members. string goes through the conditional type and becomes string array; number becomes number array. The results are joined again into string array or number array. string | number string → string[] number → number[] each member separately string[] | number[]
A distributive conditional type handles each member of a union on its own and joins the results.

Distribution is what makes filtering a union possible. The built-in Exclude returns never for the members to remove, and a union with never in it simply loses that member:

type Status = "open" | "in-progress" | "resolved" | "closed";

// The same definition as the built-in Exclude
type MyExclude<T, U> = T extends U ? never : T;
type Active = MyExclude<Status, "resolved" | "closed">;

const s: Active = "closed";
example.ts(7,7): error TS2322: Type '"closed"' is not assignable to type 'Active'.

Extracting types with infer

The infer keyword declares a new type variable inside the extends part of a conditional type. TypeScript fills it in with whatever stands at that position when the match succeeds, and the variable can be used in the true branch. It works like a pattern with a placeholder: T extends (infer Item)[] asks "is T an array, and if so, of what?"

// Pull a type out of another type with infer.
type ElementOf<T> = T extends (infer Item)[] ? Item : never;
type Unwrap<T> = T extends Promise<infer Value> ? Value : T;
type ResultOf<F> = F extends (...args: any[]) => infer R ? R : never;

async function loadTariff(code: string) {
  return { code, pricePerKWh: code === "DAY" ? 0.32 : 0.25 };
}

type Tariff = Unwrap<ResultOf<typeof loadTariff>>;
type Reading = ElementOf<{ meterId: string; kWh: number }[]>;

const night: Tariff = { code: "NIGHT", pricePerKWh: 0.25 };
const r: Reading = { meterId: "E-1001", kWh: 4.2 };
console.log((r.kWh * night.pricePerKWh).toFixed(2));
1.05

loadTariff has no written return type. The type Tariff is read from the function itself: first the return type (Promise<{ code: string; pricePerKWh: number }>), then the value inside the promise. When the function changes, the type follows. The built-in ReturnType and Awaited do the same, and are what you would use in real code; infer is for the cases they do not cover.

Template literal types

A template literal type describes a set of strings with the same backtick syntax as a JavaScript template literal, but with types in the placeholders. `reading_${Unit}` with Unit = "kWh" | "m3" is the union "reading_kWh" | "reading_m3". A typo in such a key is caught, and the compiler suggests the closest match:

type Unit = "kWh" | "m3";
type ReadingKey = `reading_${Unit}`;

const key: ReadingKey = "reading_kwh";
example.ts(4,7): error TS2820: Type '"reading_kwh"' is not assignable to type '"reading_kWh" | "reading_m3"'. Did you mean '"reading_kWh"'?

With more than one union in the template, the result contains every combination, like a multiplication table. Two regions and two kinds of event give four channel names:

type Region = "north" | "south";
type Kind = "outage" | "maintenance";

// Every combination: "north:outage" | "north:maintenance" | ...
type Channel = `${Region}:${Kind}`;

const channels: Channel[] = [
  "north:outage",
  "north:maintenance",
  "south:outage",
  "south:maintenance",
];
console.log(channels.length);
4

That is convenient, and also a trap: three unions of ten members each already make a thousand strings. Keep the unions small.

Patterns such as INC-${number}

A placeholder can also hold string or number. Then the type is not a list of strings but a pattern: `INC-${number}` accepts "INC-30412" and rejects a problem record ID:

type TicketId = `INC-${number}`;

function openTicket(id: TicketId): string {
  return `Opening ${id}`;
}

console.log(openTicket("INC-30412"));
openTicket("PRB-0042");
example.ts(8,12): error TS2345: Argument of type '"PRB-0042"' is not assignable to parameter of type '`INC-${number}`'.

Such a pattern only checks strings the compiler can see in the code. A ticket ID typed by a user or received from an API is a plain string, and you need a check at run time before you may call it a TicketId. A type guard with a regular expression connects the two, and can be stricter than the type (exactly five digits):

type TicketId = `INC-${number}`;

function isTicketId(text: string): text is TicketId {
  return /^INC-\d{5}$/.test(text);
}

for (const input of ["INC-30412", "inc-30412", "INC-3O412"]) {
  console.log(input, isTicketId(input) ? "valid" : "rejected");
}
INC-30412 valid
inc-30412 rejected
INC-3O412 rejected

Changing case and taking strings apart

Four built-in types change the case of string literal types: Uppercase, Lowercase, Capitalize (first letter uppercase) and Uncapitalize (first letter lowercase). They are built into the compiler; their definition in the standard library just says intrinsic. Together with infer, a template literal type can also take a string type apart:

// Take a template literal type apart again with infer.
type SplitChannel<C> = C extends `${infer Region}:${infer Kind}` ? { region: Region; kind: Kind } : never;

type Parsed = SplitChannel<"north:outage">;

const p: Parsed = { region: "north", kind: "outage" };
console.log(p);
{ region: 'north', kind: 'outage' }
// The first character of a string type, taken apart with infer.
type FirstChar<S> = S extends `${infer F}${string}` ? F : never;

const icon: FirstChar<"🔌 E-1001 offline"> = "🔌";
console.log(icon, icon.length, [...icon].length);
🔌 2 1

How the utility types are built

The utility types are not special compiler features. They are ordinary type definitions in the standard library file lib.es5.d.ts, and with the tools on this page you can read them:

// From lib.es5.d.ts, the standard library that ships with TypeScript (comments removed)
type Partial<T> = {
    [P in keyof T]?: T[P];
};
type Required<T> = {
    [P in keyof T]-?: T[P];
};
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type Uppercase<S extends string> = intrinsic;
  • Partial, Required and Readonly are mapped types that add or remove a modifier.
  • Pick and Record are mapped types over a given set of keys instead of keyof T.
  • Exclude and Extract are distributive conditional types that filter a union.
  • Omit combines them: pick every key except the excluded ones.
  • Parameters and ReturnType use infer to read a function type.

keyof any is string | number | symbol, every type a property key can have. Reading these definitions is a good exercise: once they make sense, so does most type-level code you will meet in libraries.

Worked example: typed event handlers

The outage dashboard at Kestrel Energy reacts to a fixed list of events. Each event needs a handler called on plus the event name. One mapped type with key remapping and a template literal type derives the handler object from the list of event names:

type EventName = "outageReported" | "outageResolved" | "readingReceived";

type Handlers = {
  [E in EventName as `on${Capitalize<E>}`]: (id: string) => void;
};

const handlers: Handlers = {
  onOutageReported: (id) => console.log(`Page the NOC for ${id}`),
  onOutageResolved: (id) => console.log(`Close ${id}`),
  onReadingReceived: (id) => console.log(`Store reading for ${id}`),
};

function emit(event: EventName, id: string): void {
  const name = `on${event[0].toUpperCase()}${event.slice(1)}` as keyof Handlers;
  handlers[name](id);
}

emit("outageReported", "INC-30415");
emit("readingReceived", "E-1001");
Page the NOC for INC-30415
Store reading for E-1001

Add an event name to the union, and every handler object that lacks the new handler fails to compile. A misspelled handler name is caught with a suggestion:

type EventName = "outageReported" | "outageResolved";

type Handlers = {
  [E in EventName as `on${Capitalize<E>}`]: (id: string) => void;
};

const handlers: Handlers = {
  onOutageReported: (id) => console.log(id),
  onOutageResolve: (id) => console.log(id),
};
example.ts(9,3): error TS2561: Object literal may only specify known properties, but 'onOutageResolve' does not exist in type 'Handlers'. Did you mean to write 'onOutageResolved'?

One detail in emit deserves attention: the handler name is built at run time with ordinary string methods, and as keyof Handlers tells the compiler to trust that the result is right. The type-level Capitalize and the run-time toUpperCase() do the same thing, but the compiler does not check that they agree. Keep such assertions small, and in one place.

When to use them, and when not

These tools are powerful, and that is exactly the risk. Some guidelines:

  • Use the built-in utility types first. Partial<Customer> is clear to every TypeScript developer; a home-made DeepOptionalExcept<T, K> is not.
  • Derive types where drift is a real risk: forms and API views of a model, handler maps, configuration keys. That is where a computed type pays for itself.
  • Name intermediate steps. A type alias such as Active or Tariff in the examples above makes errors readable, because the compiler shows the name instead of the full expansion.
  • Expect slower checks and harder messages with deeply nested or recursive types. Large template literal unions and recursive conditional types are a common cause of slow editors; the compiler also stops recursion that goes too deep.
  • Remember they are erased. A computed type is as blind to real data as any other type. Data from outside still needs a check at run time, as async code and typing API data shows.