DEV Community

Cover image for Solved: Omit for Discriminated Unions in TypeScript
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Omit for Discriminated Unions in TypeScript

🚀 Executive Summary

TL;DR: TypeScript’s built-in Omit utility type fails when applied directly to discriminated unions, collapsing them to never because it isn’t distributive. This issue is effectively solved by creating custom distributive utility types, such as DistributiveOmit, which apply the Omit operation to each union member individually.

🎯 Key Takeaways

  • The Omit utility type is not distributive by default, meaning it operates on the intersection of properties across union members rather than each member individually, leading to the never type for discriminated unions.
  • Distributive conditional types (e.g., T extends any ? Omit : never) are the canonical solution to force Omit to apply to each member of a union, creating a reusable DistributiveOmit utility.
  • An advanced alternative involves key remapping within mapped types ({ [P in keyof T as P extends K ? never : P]: T[P] }) to filter out unwanted properties distributively across union members.

Learn why TypeScript’s built-in Omit utility type fails with discriminated unions, often collapsing them to the never type, and discover three robust solutions. This guide provides practical code examples using manual re-unioning, distributive conditional types, and key remapping to correctly handle type manipulation in complex scenarios.

The Problem: Why Omit Breaks Discriminated Unions

As a DevOps engineer, you frequently model complex event streams, CI/CD pipeline states, or infrastructure configurations. Discriminated unions in TypeScript are a perfect tool for this, allowing you to create a type that can be one of several distinct shapes, differentiated by a common “discriminant” property like type or status.

The problem arises when you need to create a new type by removing a property from each member of that union. Your first instinct is to reach for the built-in Omit utility type, but you quickly run into a frustrating issue.

Symptoms: The never Type

Let’s model a set of server deployment events. Each event has a unique type and some specific properties, but they all share a timestamp and correlationId we might want to remove for a specific use case.

type DeployStartEvent = {
  type: 'DEPLOY_START';
  env: 'staging' | 'production';
  commitSha: string;
  timestamp: number;
  correlationId: string;
};

type DeploySuccessEvent = {
  type: 'DEPLOY_SUCCESS';
  url: string;
  timestamp: number;
  correlationId: string;
};

type DeployFailureEvent = {
  type: 'DEPLOY_FAILURE';
  error: string;
  logs: string;
  timestamp: number;
  correlationId: string;
};

type ServerEvent = DeployStartEvent | DeploySuccessEvent | DeployFailureEvent;
Enter fullscreen mode Exit fullscreen mode

Now, let’s try to create a new event type without the correlationId using the standard Omit utility:

// This does NOT work as expected!
type EventWithoutCorrelation = Omit<ServerEvent, 'correlationId'>;

// When you hover over EventWithoutCorrelation in your IDE, it shows:
// type EventWithoutCorrelation = never
Enter fullscreen mode Exit fullscreen mode

Instead of a new union of our event types, we get never. This means the resulting type is an empty set, which is useless and will cause type errors downstream.

Root Cause: Omit Isn’t Distributive

The core of the issue is that utility types like Omit and Pick are not “distributive” by default. When you apply Omit to a union type, it doesn’t apply the operation to each member of the union individually. Instead, it operates on the intersection of the properties available across all members.

In our ServerEvent example, keyof ServerEvent resolves to only the properties common to all three types: type | 'timestamp' | 'correlationId'. Properties like commitSha or error are not included because they don’t exist on every member of the union.

The Omit utility is defined roughly as Pick>. When TypeScript tries to resolve Omit, it can successfully exclude correlationId from the common keys. However, the resulting Pick operation fails because it cannot guarantee that the remaining properties (which differ between union members) can be safely constructed into a valid type. This conflict and ambiguity lead TypeScript to collapse the result into never to maintain type safety.

Solution 1: Manual Re-Unioning

The most straightforward and explicit solution is to manually apply Omit to each constituent member of the union and then join them back together with the | operator.

How It Works

This approach sidesteps the core problem by deconstructing the union, operating on each concrete type individually (where Omit works perfectly), and then reconstructing the union from the modified types.

Example Implementation

type EventWithoutCorrelation =
  | Omit<DeployStartEvent, 'correlationId'>
  | Omit<DeploySuccessEvent, 'correlationId'>
  | Omit<DeployFailureEvent, 'correlationId'>;

// This works! The resulting type is now a correct discriminated union:
// type EventWithoutCorrelation =
//   | { type: 'DEPLOY_START'; env: ...; commitSha: ...; timestamp: ...; }
//   | { type: 'DEPLOY_SUCCESS'; url: ...; timestamp: ...; }
//   | { type: 'DEPLOY_FAILURE'; error: ...; logs: ...; timestamp: ...; }
Enter fullscreen mode Exit fullscreen mode

While effective, this method is not scalable. If you add a new event type to ServerEvent, you must remember to also update EventWithoutCorrelation. It’s a maintainability burden best reserved for simple, one-off transformations.

Solution 2: The DistributiveOmit Utility Type

The canonical and most reusable solution is to create a custom utility type that forces the Omit operation to be “distributive” over the union.

How It Works

This technique leverages a powerful feature of TypeScript called distributive conditional types. When a conditional type (T extends U ? X : Y) has a “naked” type parameter on the left side of extends (like T in our example), it distributes the condition over a union.

By wrapping Omit in a conditional type like T extends any ? Omit<T, K> : never, we tell TypeScript: “For each member T in the union, apply Omit.”

Example Implementation

First, define the reusable utility type:

type DistributiveOmit<T, K extends keyof any> = T extends any
  ? Omit<T, K>
  : never;
Enter fullscreen mode Exit fullscreen mode

Now, you can use it on any union type, and it will work as expected:

// Use the new utility type
type EventWithoutCorrelation = DistributiveOmit<ServerEvent, 'correlationId'>;

// The result is the correct, maintainable union type, same as the manual solution.
// If you add a new event to the ServerEvent union, this type updates automatically.
Enter fullscreen mode Exit fullscreen mode

This is the preferred solution for most use cases due to its elegance, reusability, and maintainability.

Solution 3: Key Remapping with Mapped Types

A third, more advanced approach involves creating a distributive utility type using mapped types and key remapping. This offers another way to achieve the same result and demonstrates a deeper TypeScript pattern.

How It Works

This solution also uses a distributive conditional type to iterate over the union. Inside the conditional, it uses a mapped type { [P in keyof T as …] } to construct a new object type. The key remapping as P extends K ? never : P is the crucial part: for each property P in the original type, it checks if P is one of the keys to be omitted (K). If it is, the key is remapped to never, effectively filtering it out. Otherwise, the key P is kept.

Example Implementation

Here is the utility type definition:

type OmitDistributiveMapped<T, K extends PropertyKey> = T extends unknown
  ? { [P in keyof T as P extends K ? never : P]: T[P] }
  : never;
Enter fullscreen mode Exit fullscreen mode

And its usage is identical to the previous solution:

type EventWithoutCorrelation = OmitDistributiveMapped<ServerEvent, 'correlationId'>;

// This produces the exact same, correct result.
Enter fullscreen mode Exit fullscreen mode

While slightly more complex to read, this pattern is incredibly powerful and is the foundation for how TypeScript’s own Omit is implemented internally. Understanding it provides deeper insight into TypeScript’s type manipulation capabilities.

Comparison of Solutions

Choosing the right solution depends on your specific context, such as the complexity of your types and the need for reusability.

Solution Readability Reusability & Maintainability Best Use Case
1. Manual Re-Unioning High (Very explicit) Low (Requires manual updates) Quick, one-off transformations on a small, stable union.
2. DistributiveOmit Utility Medium (Requires understanding of distributive conditionals) High (Set-and-forget, fully automatic) The default, idiomatic solution for any project. Put it in a shared types.ts file.
3. Key Remapping Mapped Type Low (Combines multiple advanced concepts) High (Also fully automatic) When you need a deep understanding or want to build more complex type transformations.

Darian Vance

👉 Read the original article on TechResolve.blog


Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)