4 min read

Advanced Union Type Techniques in TypeScript — 02 Excluding Types

The Exclude utility type in TypeScript filters out unwanted members from a union, leaving only the relevant ones. From hiding internal props to limiting event handlers or restricting state actions, Exclude keeps types precise, modular, and aligned with real-world usage.
Advanced Union Type Techniques in TypeScript — 02 Excluding Types

Leverage Exclude for Enhanced Type Safety and Code Clarity


Welcome back to the “Advanced Union Type Techniques in TypeScript” series. In the previous post, we explored the Extract utility type and how it enhances type safety by precisely selecting types from unions. Building on that, this post shifts the focus to Exclude, another powerful utility type in TypeScript that serves a complementary but distinct purpose.

Advanced Union Type Techniques in TypeScript — 01 Extracting Types
The Extract utility type in TypeScript lets you carve out specific members from a union, aligning types with real-world logic. From filtering text inputs to isolating premium product options or refining state in React, Extract helps create cleaner, safer, and more maintainable code.

The Role of Exclude in TypeScript

While Extract is our precise tool for selecting specific types from a union, Exclude operates like a sieve, filtering out unwanted types and leaving us with just what we need. It’s an invaluable utility type for removing types from a union that are assignable to a specified type, thereby streamlining our type definitions and making our codebase more manageable and error-resistant.

Consider a scenario in a user interface library where you have a set of properties that can be applied to a component. Some properties, however, are reserved for internal use and shouldn’t be exposed in the public API. Here’s where Exclude comes into the picture:

type AllProps = 'children' | 'className' | 'onClick' | '_internalId';   
type PublicProps = Exclude<AllProps, '_internalId'>;

PublicProps will now represent all properties except _internalId, effectively hiding internal implementation details from the user.

Further example: Managing Event Handlers

Let’s delve into a more concrete example: managing event handlers in a complex front-end application. You might have a union type that includes all possible event handler names:

type EventHandlerNames = 'onClick' | 'onHover' | 'onKeyPress' | 'onLoad';

For a particular component, you want to exclude onLoad from the possible handlers since it’s handled differently due to specific performance optimizations:

type ComponentEventHandlers = Exclude<EventHandlerNames, 'onLoad'>;

Now ComponentEventHandlers only includes 'onClick' | 'onHover' | 'onKeyPress', streamlining the event management for that component.

function useComponentEventHandlers(handler: ComponentEventHandlers) {   
  console.log(`Handler used: ${handler}`);   
}   
   
// Correct usages   
// Without TypeScript type error 
useComponentEventHandlers('onClick');   
useComponentEventHandlers('onHover');   
useComponentEventHandlers('onKeyPress');   
   
// Incorrect usage 
// With TypeScript type error 
useComponentEventHandlers('onLoad'); // TypeScript Error: Argument of type '"onLoad"' is not assignable to parameter of type 'ComponentEventHandlers'.

Advanced example: Excluding Types in Action Dispatch with Zustand

Now, let’s examine a more advanced use case in a React application using Zustand, illustrating how Exclude is applied in the context of state management and action dispatch.

Imagine a Zustand store in a React application that handles user-related actions. In some components, we need to exclude specific actions like FetchUser from being dispatched.

type AllActions = 'AddUser' | 'UpdateUser' | 'DeleteUser' | 'FetchUser';   
   
type AllowedActions = Exclude<AllActions, 'FetchUser'>;   
   
interface UserState {   
  users: UserProfile[];   
  dispatchAction: (action: AllowedActions) => void;   
}   
   
const useUserStore = create<UserState>((set) => ({   
  users: [],   
  dispatchAction: (action) => {   
    console.log(`Action dispatched: ${action}`);   
    // Additional logic goes here   
  },   
}));   
   
// Usage in a Component   
const UserComponent = () => {   
  const dispatchAction = useUserStore(state => state.dispatchAction);   
   
  // Correct usage   
  dispatchAction('AddUser');   
   
  // Incorrect usage. Attempting to dispatch a disallowed action   
  dispatchAction('FetchUser'); // TypeScript Type Error: Argument of type '"FetchUser"' is not assignable to parameter of type 'AllowedActions'.   
};

In this example, AllowedActions is a type that includes all AllActions except FetchUser. This enables us to control which actions can be dispatched in different parts of our application. The useUserStore store includes a method dispatchAction that accepts only the allowed actions.

In UserComponent, attempting to dispatch 'FetchUser' would result in a TypeScript error, enforcing the restricted use of actions within this component. This example demonstrates the power of Exclude in a state management scenario, showing how it can be used to tailor the set of actions that can be dispatched in different parts of the application. It's a practical illustration of maintaining modular and maintainable code in complex applications.

This specific example serves as a testament to TypeScript’s broader capabilities. By leveraging features like Exclude, TypeScript enables developers to not only enforce type safety but also to architect applications that are modular and adherent to specific functional constraints.

In this way, Exclude embodies TypeScript's philosophy of ensuring type definitions are not just comprehensive but also precise. It facilitates a modular and maintainable approach, allowing only the relevant subset of types to be used in a given context. This mirrors how functionality is compartmentalized in applications, enhancing the robustness and reliability of the types in line with their intended use.

Through careful application of Exclude, types in your TypeScript code can remain perfectly aligned with their usage scenarios, ensuring they are both relevant and reliable.

Closing Thoughts

In this exploration, I’ve demonstrated how the Exclude utility type in TypeScript is instrumental in streamlining type definitions and enhancing the robustness of our code. From fine-tuning component properties to orchestrating state and actions in React with Zustand, Exclude plays a pivotal role in ensuring that our codebase is not just comprehensive, but also precise and modular.

As we continue with this series, I’ll guide you through more advanced TypeScript features, further expanding our toolkit and deepening our understanding of TypeScript’s powerful capabilities.

Thank you for joining me in this deep dive into TypeScript’s advanced techniques. More insights and practical applications are on the way in the upcoming posts of this series.

See you in the next one 👋!

Subscribe to our newsletter.

Become a subscriber receive the latest updates in your inbox.