In the previous tutorial, we learned about built-in utility types. Now let’s learn how those utility types are built — using mapped types and conditional types.

By the end of this tutorial, you will know how to transform object types, extract types with infer, and build your own custom utility types.

What Are Mapped Types?

A mapped type creates a new type by transforming every property of an existing type. It loops over each key and applies a transformation.

The basic syntax is:

type MappedType<T> = {
  [K in keyof T]: SomeTransformation;
};

Building Your Own Partial

Here is how TypeScript’s Partial<T> works internally:

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

Let’s break it down:

  • keyof T gives all property names of T as a union
  • K in keyof T loops over each property name
  • ? makes each property optional
  • T[K] keeps the original value type
interface User {
  id: number;
  name: string;
}

type PartialUser = MyPartial<User>;
// Result: { id?: number; name?: string; }

Building Your Own Readonly

type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

Building Your Own Required

The -? syntax removes the optional modifier:

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

You can also remove readonly with -readonly:

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

Transforming Value Types

You can change the value type of every property:

// Make every property a boolean
type Flags<T> = {
  [K in keyof T]: boolean;
};

interface User {
  name: string;
  email: string;
  age: number;
}

type UserFlags = Flags<User>;
// Result: { name: boolean; email: boolean; age: boolean; }

Real-World Use: Form Validation

type ValidationResult<T> = {
  [K in keyof T]: string | null; // error message or null
};

interface LoginForm {
  email: string;
  password: string;
}

type LoginErrors = ValidationResult<LoginForm>;
// Result: { email: string | null; password: string | null; }

const errors: LoginErrors = {
  email: "Invalid email address",
  password: null, // no error
};

Key Remapping with as

TypeScript 4.1 added the as clause to remap keys during mapping:

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

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

type UserGetters = Getters<User>;
// Result: { getName: () => string; getAge: () => number; }

You can also filter out keys by remapping to never:

// Keep only string properties
type StringProps<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

type StringUserProps = StringProps<User>;
// Result: { name: string; email: string; }

What Are Conditional Types?

Conditional types choose a type based on a condition. They use the same syntax as the ternary operator:

type Result = T extends Condition ? TrueType : FalseType;

Basic Example

type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;  // "yes"
type B = IsString<number>;  // "no"
type C = IsString<"hello">; // "yes" — "hello" extends string

Checking for Arrays

type IsArray<T> = T extends unknown[] ? true : false;

type A = IsArray<string[]>;  // true
type B = IsArray<number>;    // false
type C = IsArray<[1, 2, 3]>; // true — tuples are arrays

The infer Keyword

infer lets you extract a type from within another type. Think of it as a type-level variable:

type ElementType<T> = T extends (infer E)[] ? E : never;

type A = ElementType<string[]>;  // string
type B = ElementType<number[]>;  // number
type C = ElementType<string>;    // never — not an array

Extracting Return Types

This is how ReturnType works internally:

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
  return { id: 1, name: "Alex" };
}

type User = MyReturnType<typeof getUser>;
// Result: { id: number; name: string; }

Extracting Promise Values

This is how Awaited works (simplified):

type UnwrapPromise<T> = T extends Promise<infer V> ? V : T;

type A = UnwrapPromise<Promise<string>>;  // string
type B = UnwrapPromise<Promise<number[]>>; // number[]
type C = UnwrapPromise<string>;            // string — not a promise, return as-is

Extracting Function Parameters

type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

function greet(name: string, age: number): void {}

type Name = FirstParam<typeof greet>; // string

Distributive Conditional Types

When a conditional type receives a union, it distributes over each member:

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

type Result = ToArray<string | number>;
// Distributes: ToArray<string> | ToArray<number>
// Result: string[] | number[]

This is different from:

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

type Result = ToArrayNonDist<string | number>;
// Result: (string | number)[] — treated as one type

Wrapping T in a tuple [T] prevents distribution. This is a common trick when you want to treat the union as a whole.

Practical Use: Filtering Union Types

type NonString<T> = T extends string ? never : T;

type Mixed = string | number | boolean | null;
type WithoutStrings = NonString<Mixed>;
// Result: number | boolean | null

This is exactly how Exclude works internally:

type MyExclude<T, U> = T extends U ? never : T;

Building Custom Utility Types

DeepPartial

Partial only makes the top level optional. DeepPartial makes nested properties optional too:

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

interface Config {
  server: {
    host: string;
    port: number;
  };
  database: {
    url: string;
    pool: {
      min: number;
      max: number;
    };
  };
}

type PartialConfig = DeepPartial<Config>;
// Now you can provide any subset:
const config: PartialConfig = {
  server: { port: 8080 },
  // database is completely optional
};

DeepReadonly

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

Nullable

Make all properties nullable:

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

interface Form {
  name: string;
  email: string;
}

type NullableForm = Nullable<Form>;
// Result: { name: string | null; email: string | null; }

Combining Mapped and Conditional Types

You can use conditional types inside mapped types for powerful transformations:

// Convert function properties to their return types
type ReturnTypes<T> = {
  [K in keyof T]: T[K] extends (...args: any[]) => infer R ? R : T[K];
};

interface API {
  getUser: () => User;
  getProducts: () => Product[];
  version: string;
}

type APIResults = ReturnTypes<API>;
// Result: { getUser: User; getProducts: Product[]; version: string; }

Reading Cryptic Error Messages

When mapped or conditional types produce errors, the messages can be hard to read. Here are some tips:

  1. Hover over the type in VS Code to see the resolved type
  2. Break complex types into smaller pieces and check each one
  3. Use type aliases for intermediate steps:
// Hard to debug
type Result = {
  [K in keyof T as T[K] extends Function ? K : never]: T[K] extends (...args: infer P) => infer R ? (...args: P) => Promise<R> : never;
};

// Easier to debug — break it into steps
type FunctionKeys<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

type AsyncFunction<F> = F extends (...args: infer P) => infer R
  ? (...args: P) => Promise<R>
  : never;

type Result<T> = {
  [K in FunctionKeys<T>]: AsyncFunction<T[K]>;
};

What’s Next?

In the next tutorial, we will learn about template literal types — how to create type-safe string patterns and manipulate string types.