Right, let’s talk about the TypeScript janitors: NonNullable<T>, Exclude<T, U>, and Extract<T, U>. These are the utility types you call in when you’ve got a mess of possible types and you need to clean house, removing the junk and keeping only what you actually want. They’re the foundation for writing type logic that feels intelligent, rather than just descriptive.

What They Do (The Short Version)

Think of these three as a set of filtering operations for types.

  • NonNullable<T>: “Remove null and undefined from this type T.” It’s the simplest of the bunch.
  • Exclude<T, U>: “From type T, remove any types that are assignable to type U.” It’s for subtraction.
  • Extract<T, U>: “From type T, keep only the types that are assignable to type U.” It’s for finding the intersection.

Exclude and Extract are two sides of the same coin. If you understand one, you understand the other. NonNullable is actually just a specific, common-use case of Exclude. Let’s break them down.

The Workhorse: Exclude<T, U>

This is the most fundamental of the three. Its job is straightforward: it takes a union type T and kicks out any member of that union that can be assigned to type U.

type EventTypes = 'click' | 'scroll' | 'mouseover' | 'keypress' | 404 | null;

// Let's say we only want the string events. We can exclude the number and null.
type StringEvents = Exclude<EventTypes, number | null>;
// Result: type StringEvents = "click" | "scroll" | "mouseover" | "keypress"

// We can be more specific. Maybe we hate the 'scroll' event.
type EventsWithoutScroll = Exclude<EventTypes, 'scroll'>;
// Result: type EventsWithoutScroll = "click" | "mouseover" | "keypress" | 404 | null

Why it works: Under the hood, Exclude is a distributive conditional type. This is the key bit of magic. It means if T is a union like A | B | C, it applies the condition to each member individually: (A extends U ? never : A) | (B extends U ? never : B) | (C extends U ? never : C). Any member that matches U gets mapped to never—TypeScript’s way of saying “poof, you don’t exist anymore”—and the resulting union is simplified.

The Other Side of the Coin: Extract<T, U>

As you might guess, Extract is the inverse of Exclude. Instead of removing what matches U, it keeps only what matches U.

type EventTypes = 'click' | 'scroll' | 'mouseover' | 'keypress' | 404 | null;

// "Just give me the numeric event types, please."
type NumericEvents = Extract<EventTypes, number>;
// Result: type NumericEvents = 404

// "Give me only the events that are also in this other specific set."
type GUIEvents = Extract<EventTypes, 'click' | 'mouseover' | 'submit'>;
// Result: type GUIEvents = "click" | "mouseover"

It uses the same distributive magic, but its conditional is flipped: T extends U ? T : never. So it keeps the matching types and throws the rest into the never void.

The Special Case: NonNullable<T>

Now, let’s look at NonNullable<T>. It’s not a separate, bespoke utility. It’s literally just Exclude<T, null | undefined>. That’s the entire implementation. It’s a brilliantly specific application of the general Exclude tool.

type UserInput = string | null | undefined;

// We need a function that requires a real value.
function processInput(input: NonNullable<UserInput>) {
  return input.trim(); // Safe to do, because input can't be null/undefined here.
}

processInput("hello"); // 👍 Works
processInput(null);    // ❌ Compile Error: Argument of type 'null' is not assignable...

// This is exactly equivalent to:
function processInput2(input: Exclude<UserInput, null | undefined>) {
  return input.trim();
}

Common Pitfalls and “Oh, Really?” Moments

  1. Distribution is Key: Remember, these utilities only distribute over union types. If you give Exclude a non-union type for T, it just does a single check. This usually does what you expect, but it’s crucial to understand the mechanism.

  2. The U Type in Exclude/Extract is a Filter, Not a Value: A common beginner mistake is to think Exclude<T, 'A' | 'B'> will remove the values ‘A’ and ‘B’ from a union. It won’t. It will remove the types ‘A’ and ‘B’. If your union is 'A' | 'B' | SomeComplexObject, it will remove the entire string literal type 'A', not just an instance of it. This is almost always what you want, but the distinction is important.

  3. never is Your Friend: These types heavily rely on reducing things to never. A union that is entirely never (e.g., never | never) will simplify to just never. This is the correct behavior! If you Exclude<string, string>, you get never. It means “there is no type here that isn’t a string,” which is perfectly accurate. Don’t be afraid of never; it’s the type system’s way of telling you your filter worked a bit too well.

Best Practices and Real-World Use

You’ll use these constantly when you’re refining types from broader libraries or your own system’s boundaries.

  • API Responses: You get a Promise<Response | null> from a fetch call. Wrapping the response type in NonNullable inside your handler function tells the next part of your code it can stop worrying about the null case.
  • Config Objects: You have a library that accepts a wide Options union. You can use Extract<Options, { specificFlag: boolean }> to get only the config shapes that are relevant for a specific mode.
  • Event Handling: As shown above, they’re perfect for narrowing down event type unions to precisely what a specific event listener should handle.

They are not flashy, but Exclude, Extract, and NonNullable are the quiet, competent utilities that form the bedrock of advanced, precise type logic. Learn them, use them, and appreciate the fact that you’re not writing T extends null | undefined ? never : T by hand every single time.