Right, so you’ve grasped the basics of mapped types—iterating over keys, slapping on a modifier or two. But that’s like learning how to hold a scalpel. The real fun begins when you start building your own utility types, the ones that solve your specific problems. This is where you move from reading the map to drawing your own.

Let’s start by building something deceptively simple but incredibly powerful. You know how Partial<T> makes everything optional? What if you need the exact opposite? A type where every property is required, even the ones that were originally optional? The existing Required<T> type does this, but let’s build our own to see the gears turn.

We do this by using the - modifier to remove the optionality (?) from properties. It’s like a strict headmaster telling the properties, “No, you will not be late; you will be present.”

type Concrete<T> = {
  [P in keyof T]-?: T[P];
};

interface SketchyConfig {
  apiUrl?: string;
  timeout: number;
  retries?: number;
}

// `Concrete<SketchyConfig>` is now:
// {
//   apiUrl: string; // No longer optional!
//   timeout: number;
//   retries: number; // No longer optional!
// }

const validConfig: Concrete<SketchyConfig> = {
  apiUrl: "https://api.example.com", // Required now
  timeout: 1000,
  retries: 3, // Required now
};

See that -?? That’s the magic. It strips the undefined from the type, forcing you to provide a value. It’s TypeScript’s way of saying, “I don’t care what the original interface designer thought; in my domain, we provide values.”

Key Remapping with as

Now, let’s talk about the big gun: key remapping with the as clause. This feature, introduced in TypeScript 4.1, is an absolute game-changer. It lets you not just change the value types, but rename the keys themselves during the mapping process. It’s the difference between painting a room and moving the walls.

Why is this so brilliant? Because it lets you create new type shapes from old ones programmatically. Let’s say you want to take an existing type and create a new type where all the keys are prefixed with "on" and turned into event handlers. This is trivial now.

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

interface User {
  name: string;
  age: number;
  login: boolean;
}

// `Eventify<User>` becomes:
// {
//   onName: () => string;
//   onAge: () => number;
//   onLogin: () => boolean;
// }

Let’s break down the as clause, because it looks weird until it doesn’t:

  • K in keyof T: We’re iterating over each key K in T.
  • as on${Capitalize<string & K>}``: For each key, we’re creating a new key. We use a template literal type to prefix it with "on" and then capitalize the original key. The string & K is a small type safety dance because keyof T can include symbols, and Capitalize expects a string.

Filtering Properties by Type

You can also use the as clause to filter out properties entirely. You do this by remapping a key to never. It’s like a bouncer for your types. Let’s create a type that only includes the string properties from another type.

type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface MixedBag {
  id: string;
  count: number;
  log: () => void;
  tags: string[];
}

// `OnlyStrings<MixedBag>` becomes:
// {
//   id: string;
// }
// `tags` is an array, not a string, so it's filtered out.

The conditional type T[K] extends string ? K : never is the bouncer. For each property, it asks: “Is your value type a string?” If yes, the key is allowed through (K). If no, the key is remapped to never and is kicked out of the club. This is infinitely more elegant than the old ways of having to use complex conditional type inferencing.

The Readonly Pitfall

Here’s a classic “gotcha.” You might try to create a mutable version of a readonly type like ReadonlyArray. This seems logical:

type MakeMutable<T> = {
  -readonly [P in keyof T]: T[P];
};

type MyArray = MakeMutable<ReadonlyArray<string>>;
// Expect: string[]
// Reality: { [n: number]: string; length: number; ... }

Wait, what? It didn’t give us an Array type! It gave us a mapped object type. Why? Because keyof ReadonlyArray<string> doesn’t give you array method names; it gives you the names of its own properties (like length, slice, etc.). This is a case where the type you’re mapping over is more specific than a simple interface. The lesson here is profound: mapped types work on the shape they are given, not on your conceptual understanding of the type. Sometimes, a conditional type checking if T is an array is the only correct solution. It’s a rough edge, and you just have to know it’s there.