DEV Community

Cover image for Mapped and Conditional Types in TypeScript: A Deep Dive
Rushi Patel
Rushi Patel

Posted on

Mapped and Conditional Types in TypeScript: A Deep Dive

TypeScript is a top choice for large-scale JavaScript apps, thanks to its powerful type system. Two key features— Mapped Types and Conditional Types —let you transform and optimize types efficiently.

In this post, we’ll explore their syntax, applications, and how to build custom utility types.

Pro Tip:
If you are new to Utility Types of TypeScript, it would be great to refer this article. It'll help you getting maximum from this article. Read Article here

Introduction

In TypeScript, you often need to transform types—making properties optional, required, or filtering keys. While Mapped & Conditional Types aren’t used daily, understanding them can be valuable when needed.

This post covers:

  • Mapped Types – Syntax, functionality, and common use cases.
  • Conditional Types – Filtering, transformations, and applications.
  • Custom Utility Types – Practical snippets combining both concepts.

Let's take User type for reference as below:

interface User {
  id: string;
  name: string;
  isAdmin: boolean;
  age?: number;
}
Enter fullscreen mode Exit fullscreen mode

Mapped Types

Mapped types allow you to create new types by iterating over the properties of an existing type. They are essential when you want to transform each property of a type uniformly.

General Syntax

type MappedType<T> = {
  [Key in UnionType]: ValueType;
};
Enter fullscreen mode Exit fullscreen mode

Here, Key iterates over a union (commonly derived from keyof T), and you assign a new value type to each key.

Applications and Use Cases

Mapped types are incredibly versatile. Let’s explore some common applications with examples:

1. Making All Properties Optional

Use Case: When you need to create a version of an interface where not every property is mandatory—for instance, when accepting partial updates in an API.

type PartialMapped<T> = {
  [key in keyof T]?: T[key];
};

// Valid (isAdmin is no longer a required property)
const userPayload: PartialMapped<User> = {
  id: '1',
  name: 'John'
};
Enter fullscreen mode Exit fullscreen mode

2. Making All Properties Required

Use Case: When you want to enforce that every property is provided, even if the original type allowed some properties to be optional (useful when you’re certain that all data will be available).

type RequiredMapped<T> = {
  [key in keyof T]-?: T[key];
};

// Valid (all property are specified)
const user1: RequiredMapped<User> = {
  id: '1',
  name: 'John',
  isAdmin: false,
  age: 32
};

// In Valid
// Error: Missing properties age, isAdmin from RequiredMapped<User>'
const user2: RequiredMapped<User> = {
  id: '1',
  name: 'John',
};
Enter fullscreen mode Exit fullscreen mode

3. Creating Non-Nullable Versions of a Type

Use Case: Sometimes you want to ensure that none of the properties can be null or undefined. This is very useful when sanitizing or processing data that must be complete.

type NonNullableMapped<T> = {
  [key in keyof T]: Exclude<T[key], null | undefined>;
};

type UserNonNullable = NonNullableMapped<User>;
// Now all properties of UserNonNullable will have null and undefined removed from their types.
Enter fullscreen mode Exit fullscreen mode

4. Creating Read-Only Versions of a Type

Use Case: Enforcing immutability in parts of your application, such as configuration objects or API responses, to prevent accidental changes.

type ReadOnlyMapped<T> = {
  readonly [Key in keyof T]: T[Key];
};

const user: ReadOnlyMapped<User> = { id: "1", name: "John", isAdmin: false  };

user.name = "Bob"; // Error: Cannot assign to 'name' because it is a read-only property.
Enter fullscreen mode Exit fullscreen mode

These examples illustrate how mapped types can be applied to modify or enhance the shape of your types to better fit the needs of your application. Mapped types is not limited to only above mentioned usecases, you can leverage it for any suitable scenario as applicable.


Conditional Types

Conditional types introduce logic into your type definitions. They let you create types that can vary based on conditions, similar to the ternary operator in JavaScript but operating at the type level.

General Syntax

T extends U ? X : Y
Enter fullscreen mode Exit fullscreen mode

If type T is assignable to type U, the type resolves to X; otherwise, it resolves to Y.

Applications and Use Cases

Conditional types are ideal for filtering and transforming union types, and making types more dynamic. Here are a few detailed examples:

1. Excluding Specific Types from a Union

Use Case: You might have a union type where certain members are not applicable in a given context. For example, excluding customer role from a set of user roles.

type ExcludeMapped<T, K> = T extends K ? never : T;

// Example
type UserRoles = 'ADMIN' | 'CUSTOMER' | 'SUPER_ADMIN'
type AdminRole = ExcludeMapped<UserRoles, 'CUSTOMER'>;

const role: AdminRoles = 'CUSTOMER' // Not Valid ❌
const role: AdminRoles = 'ADMIN' // Valid ✅
Enter fullscreen mode Exit fullscreen mode

2. Extracting Specific Types from a Union

Use Case: You may need to isolate a subset of types from a union, such as extracting roles that have admin responsibilities.

type ExtractMapped<T, K> = T extends K ? T : never;

// Example
type UserRoles = 'ADMIN' | 'CUSTOMER' | 'SUPER_ADMIN'

type AdminRoles = ExtractMapped<UserRoles, 'ADMIN' | 'SUPER_ADMIN'> // Only include 'ADMIN' & 'SUPER_ADMIN' from UserRoles

const role: AdminRoles = 'CUSTOMER' // Not Valid ❌
const role: AdminRoles = 'ADMIN' // Valid ✅
Enter fullscreen mode Exit fullscreen mode

3. Dynamic Type Transformation Based on Conditions

Use Case: Conditional types can help adjust the type of a property based on some condition. For example, transforming a type based on whether it extends a particular interface or type.

type ConditionalExample<T> = T extends string ? string[] : T;

// Example
// If T is string, then type becomes string[], otherwise it remains T.
type Test1 = ConditionalExample<string>;  // Test1 is string[]
type Test2 = ConditionalExample<number>;  // Test2 is number
Enter fullscreen mode Exit fullscreen mode

These examples show how conditional types empower you to build flexible, context-aware type definitions that can adapt to various scenarios in your codebase.


Custom Utility Types with Mapped & Conditional Types

One of the most powerful aspects of TypeScript is the ability to create your own utility types. By combining mapped and conditional types, you can design custom types that enhance type safety and code reuse. Below are shared snippets of such utility types.

1. Partial<Type>

Creates a version of a type where all properties are optional.

type PartialMapped<T> = {
  [key in keyof T]?: T[key];
};
Enter fullscreen mode Exit fullscreen mode

2. Required<Type>

Creates a version of a type where all properties are required.

type RequiredMapped<T> = {
  [key in keyof T]-?: T[key];
};
Enter fullscreen mode Exit fullscreen mode

3. NonNullable<Type>

Transforms a type to exclude null and undefined from each property.

type NonNullableMapped<T> = {
  [key in keyof T]: Exclude<T[key], null | undefined>;
};
Enter fullscreen mode Exit fullscreen mode

4. Pick<Type>

Extracts a subset of properties from a type.

type PickMapped<T, K extends keyof T> = {
  [Key in K]: T[Key];
};
Enter fullscreen mode Exit fullscreen mode

5. Omit<Type>

Creates a type by omitting specific properties from another type.

type OmitMapped<T, K extends keyof T> = {
  [Key in keyof T as Key extends K ? never : Key]: T[Key];
};
Enter fullscreen mode Exit fullscreen mode

6. ReadOnly<Type>

Marks all properties of a type as read-only.

type ReadOnlyMapped<T> = {
  readonly [Key in keyof T]: T[Key];
};
Enter fullscreen mode Exit fullscreen mode

7. Conditional Utility Types: Exclude & Extract

These conditional types allow filtering of union types.

  • Exclude
  type ExcludeMapped<T, K> = T extends K ? never : T;
Enter fullscreen mode Exit fullscreen mode
  • Extract
  type ExtractMapped<T, K> = T extends K ? T : never;
Enter fullscreen mode Exit fullscreen mode

Example

  type UserRoles = 'ADMIN' | 'CUSTOMER' | 'SUPER_ADMIN'

  // Both below resolves to the union 'ADMIN' | 'SUPER_ADMIN'
  // one using Extract and other using Exclude
  type AdminRoles = ExtractMapped<UserRoles, 'ADMIN' | 'SUPER_ADMIN'>;
  type AdminRole = ExcludeMapped<UserRoles, 'CUSTOMER'>;
Enter fullscreen mode Exit fullscreen mode

Closing Comments

Mapped and conditional types are key to TypeScript’s advanced type system, enabling cleaner, more maintainable code through reusable utilities.

Mastering these concepts will help you tackle (or at least understand) complex type scenarios in TypeScript.


For any questions or suggestions, please feel free to comment below. 💬

If you find this article useful, share it with your friends and follow for regular update of such good articles. 🔗

Rushi Patel, Signing Off! 😊

Top comments (0)