ProgrammingFullstack Developer

How does Exclude work in TypeScript, when to use it for manipulating union types, and what are the nuances when working with this utility type?

Pass interviews with Hintsage AI assistant

Answer.

Exclude<T, U> is a utility type introduced in TypeScript to subtract one type from another, when certain values need to be excluded from a union type.

Background

Originally, TypeScript did not have an easy way to subtract one type from another. When creating generic APIs and during refactoring, it was often required to obtain a "remaining" type — everything except prohibited values. Instead of manually manipulating unions, it was necessary to maintain several similar interfaces.

Problem

For example, when you have a type A | B | C, but you need to get the type without B. This is often required when building complex function parameter types, filtering allowed values, and dynamically forming types.

Solution

Exclude solves this problem. Its simplified signature is as follows:

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

It returns a type that excludes all members of U from T.

Example:

type Status = 'draft' | 'published' | 'removed'; type UserVisibleStatus = Exclude<Status, 'removed'>; const visible: UserVisibleStatus = 'draft'; // OK

Key features:

  • Allows for dynamic type formation by "subtracting" parts from a union.
  • Simplifies refactoring — when changing the base type, all derived types are automatically updated.
  • Can be used, for example, for filtering switch cases or object keys.

Tricky Questions.

Can Exclude be used for regular types, not just unions?

If T is not a union type but is part of U — Exclude will work nonetheless, but the result may be never or T, which is not always intuitive.

Exclude<'a', 'a'> // result: never Exclude<'a', 'b'> // result: 'a'

Does Exclude remove all mentions of the type in the object's structure?

No, Exclude does not recursively traverse nested fields of the type; it only excludes at the top level of the union.

How does Exclude work with interfaces and object types?

It compares the entire type, not individual properties. Therefore, Exclude from a union of several interfaces only removes those that match U completely.

interface A { x: number }; interface B { y: string }; // Exclude<A|B, B> gives: A (B matches completely)

Type Errors and Anti-patterns

  • Attempts to apply Exclude for nested or partial matches.
  • Using it for "removing" properties of an interface, instead of union variants.
  • Ignoring the possibility of obtaining type never with complete type matches.

Real-life Example

Negative Case

Validating user roles through Exclude<UserRoles, 'admin'>, but forgetting that Exclude does not apply to nested structures — permissions 'admin:sub' were not excluded.

Pros:

  • Simplicity in forming the role type.

Cons:

  • Non-intuitive behavior with nested or similar types; a critical role was missed.

Positive Case

Using Exclude to restrict the public API actions: Exclude<Action, 'delete'>, which excludes a dangerous operation.

Pros:

  • Safety at the type level, cannot invoke a prohibited action.

Cons:

  • Need to maintain up-to-date lists of base types.