Tuesday, January 09, 2024 · 4 min read
typescript
frontend
javascript

Advanced Union Type Techniques in TypeScript — 02 Excluding Types

Leverage Exclude for Enhanced Type Safety and Code Clarity

Substack Banner TS101.png

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.

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  
useComponentEventHandlers('onClick');  
useComponentEventHandlers('onHover');  
useComponentEventHandlers('onKeyPress');  
  
// Incorrect usage  
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.

Conclusion

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 👋!