Sunday, January 07, 2024 · 5 min read
typescript
frontend
javascript

Advanced Union Type Techniques in TypeScript — 01 Extracting Types

Leverage Extract for Enhanced Type Safety and Code Clarity

Substack Banner TS101.png

Welcome to the first post of the "Advanced Union Type Techniques in TypeScript" series. In this series, we'll be looking into the more complex aspects of TypeScript's type system, starting with this post, which focuses on the Extract utility type.

The Power of Unions in TypeScript

Union types in TypeScript are a fundamental feature that allow a single variable to hold values of different types. Think of a union type as a variable that can wear different hats depending on the situation. For instance, a variable might be a string in one scenario and a number in another. This flexibility is crucial in JavaScript's dynamic behavior, and TypeScript augments this with robust type safety.

Consider a TypeScript application where a function needs to handle various types of input, such as a user’s name string, age number, or their birthdate Date. Union types are invaluable here, enabling the definition of a type that encompasses all these possibilities:

type UserInput = string | number | Date;

Advanced Operations with Union Types

Union types are not just about declaring variables that can be of multiple types. They form the foundation for more advanced operations, making TypeScript a powerful tool for developers. This is where TypeScript’s Extract and Exclude types, along with indexed access types and const assertions, come into play. They provide a sophisticated toolkit for manipulating these union types, resulting in clearer and more maintainable code.

This post will focus on Extract, providing practical examples to demonstrate its effective use in real-world TypeScript scenarios. Let’s explore how Extract can be employed to refine type definitions and streamline TypeScript code.

Extracting Types

TypeScript’s union types are akin to Swiss Army knives in our type toolbelt — multi-functional and essential. Yet, there are scenarios when the best tool is a scalpel: precise and sharp. This is where TypeScript’s Extract type operator comes into play.

Imagine you’re working with a function that can handle various data types or structures. You have a union type representing a user’s input which could be a string, a number, a date, or an array of strings:

type UserInput = string | number | Date | string[];

Now, suppose you want to write a function that should only handle text-based input. You need to extract just the string-related types from UserInput. Here’s where Extract shines:

type TextualInput = Extract<UserInput, string | string[]>;

TextualInput will now be an alias for string | string[], and TypeScript will ensure that only textual data types are processed by your function.

function handleText(input: TextualInput) {  
  console.log(`Handling text: ${input}`);  
}  
  
// Correct usage  
// Without TypeScript type error  
handleText("Hello, world!");  
handleText(["Hello", "world!"]);  
  
// Incorrect usage. Attempting to use a non-textual type will result in a TypeScript error.  
// With TypeScript type error  
handleText(42); // TypeScript Type Error: Argument of type 'number' is not assignable to parameter of type 'TextualInput'.  
handleText(new Date()); // TypeScript Type Error: Argument of type 'Date' is not assignable to parameter of type 'TextualInput'...

By using Extract, we’ve narrowed the focus of handleText without having to redefine the types we already declared.

Further example: Refining Product Options

But let’s dive deeper. Consider an e-commerce application where you have a union type of various product options:

type ProductOptions = 'color' | 'size' | 'material' | 'warranty';

You may have a set of options that are considered premium features, like material and warranty, and you want to segregate them:

type PremiumFeatures = Extract<ProductOptions, 'material' | 'warranty'>;

Now, PremiumFeatures is a type that only includes material and warranty, allowing you to handle those premium features distinctly, perhaps in a different component or a pricing algorithm:

function calculatePremiumCost(features: PremiumFeatures[]) {  
    const cost = features.length * 100; // Example cost calculation  
    console.log(`Total premium cost: ${cost}`);  
    return cost;  
}  
  
// Correct usage  
// Without TypeScript type error  
calculatePremiumCost(['material', 'warranty']);  
  
// Incorrect usage. Attempting to use non-premium features will result in a TypeScript error.  
// With TypeScript type error  
calculatePremiumCost(['color', 'size']);  
// TypeScript Type Error:  
// Type '"color"' is not assignable to type 'PremiumFeatures'.  
// Type '"size"' is not assignable to type 'PremiumFeatures'.

Advanced example: Extracting Specific States with Zustand

In a React application using Zustand for state management, TypeScript’s type safety can be leveraged to prevent errors. Here’s an example demonstrating how we can enforce correct usage patterns with our AppState.

interface UserProfile {  
  id: number;  
  name: string;  
  email: string;  
}  
  
interface UserSettings {  
  theme: 'light' | 'dark';  
  notifications: boolean;  
}  
  
const defaultSettings: UserSettings = {  
  theme: 'light',  
  notifications: true,  
};  
  
interface AppState {  
  userProfile: UserProfile | null;  
  userSettings: UserSettings;  
  notifications: Notification[] | null;  
}  
  
const useStore = create<AppState>(() => ({  
  userProfile: null, // Initially set to null  
  userSettings: defaultSettings,  
  notifications: null  
}));

In this scenario, you might need a selector that specifically targets userProfile when it is not null. This is where Extract comes in handy:

type NonNullUserProfile = Extract<AppState['userProfile'], UserProfile>;  
  
const selectUserProfile = (): NonNullUserProfile | null => {  
  const userProfile = useStore((state: AppState) => state.userProfile);  
  return userProfile ? userProfile as NonNullUserProfile : null;  
};  
  
const userProfile = selectUserProfile();  
  
// Correct usage: Checking for non-null before accessing properties  
// Without TypeScript type error  
if (userProfile) {  
  // Use userProfile here  
  console.log(userProfile.name) // Logs name if provided  
}  
  
// Incorrect usage  
// With TypeScript type error  
console.log(userProfile.name); // TypeScript Type Error: 'userProfile' is possibly 'null'.

Here, NonNullUserProfile is a type that only includes UserProfile, excluding null from the userProfile type. The selectUserProfile function uses this to ensure it works with a non-null profile.

The Extract type gives you a language-level construct to differentiate between subsets of types, providing a mechanism for enforcing a level of type safety that’s in line with the real-world divisions in your application logic.

Extract is not only about eliminating types; it’s about refining our type definitions to match the logical domains of our application. It allows for a clean, maintainable, and type-safe codebase that closely maps to the business logic.

Conclusion

In this post, I've shown you how the Extract utility type in TypeScript can be used to refine type definitions and enhance code clarity. I've shown its application in various scenarios – from handling simple text inputs to managing complex state in React applications – demonstrating its role in maintaining type safety and minimizing errors.

As this series progresses, I'll take you through other advanced features in TypeScript, deepening our collective understanding and broadening our development toolkit.

Thanks for joining me on this exploration into TypeScript's advanced capabilities. Stay tuned for more insights in our next posts.

Till next time 👋!