Understanding TypeScript Unions

18. July, 2023 6 min read Develop

A Comprehensive Guide Using React

TypeScript has become the go-to language for web developers seeking a safer and more robust way to write JavaScript code.

TypeScript unions are among its many powerful features and a valuable tool for creating flexible and well-typed applications. In this article, we will dive deep into TypeScript unions, explore how to use them effectively in React applications, uncover some hidden features, and discuss common pitfalls to avoid.

What are TypeScript Unions?

TypeScript unions allow developers to define a type that can hold multiple possible data types. It enables us to combine two or more types into one, allowing us to handle different data variations without compromising type safety.

In TypeScript, we denote unions using the pipe symbol |. For instance, consider a simple union of number and string:

type MyUnion = number | string;

Here, MyUnion can hold a number or a string, and any variable of this type can be assigned a value of either type.

How to Use TypeScript Unions

TypeScript unions empower developers to create versatile and type-safe code by allowing them to combine multiple data types into a single, flexible type. Let’s learn how to harness the power of unions to enhance your code’s adaptability and improve your development workflow.

Function Parameters

One common use case for unions is when defining function parameters. Imagine you want to create a function that can accept either a string or an array of strings:

const greet = (names: string | string[]) => {
  if (Array.isArray(names)) {
    names.forEach(name => console.log(`Hello, ${name}!`));
  } else {
    console.log(`Hello, ${names}!`);
  }
};

Now, you can call the function with either a single string or an array of strings:

greet('Alice');
// output: Hello, Alice!

greet(['Bob', 'Charlie']);
// output: Hello, Bob! Hello, Charlie!

Conditional Rendering in React

React developers frequently encounter situations where components render different content based on the type of props they receive. Unions can help us handle such scenarios effectively. Let’s consider a React component that renders differently for numbers and strings:

type DisplayProps = { value: number | string };

const Display: React.FC<DisplayProps> = ({ value }) => {
  return (
    <>
      {typeof value === 'number' ? (
        <p>Number: {value}</p>
      ) : (
        <p>String: {value}</p>
      )}
    </>
  );
};

In this example, the Display component takes a prop value of type number | string. If the value is a number, it displays “Number: {value}”, and if it’s a string, it shows “String: {value}“.

const App = () => {
  return (
    <>
      <Display value={42} />
      {/* output: Number: 42 */}

      <Display value="Hello, TypeScript!" />
      {/* output: String: Hello, TypeScript! */}
    </>
  );
};

Redux Actions and Reducers

TypeScript unions are handy for defining action types and state shapes when working with Redux in React applications. Let’s take an example of a simple counter Redux module:

type CounterAction =
  | { type: 'INCREMENT', payload: number }
  | { type: 'DECREMENT', payload: number };

type CounterState = { count: number };

const counterReducer = (
  state: CounterState,
  action: CounterAction
): CounterState => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + action.payload };
    case 'DECREMENT':
      return { ...state, count: state.count - action.payload };
    default:
      return state;
  }
};

Here, we have defined a CounterAction union representing two actions: INCREMENT and DECREMENT, with a payload of type number. The CounterState type holds the state shape for our counter.

Hidden Features of TypeScript Unions

Unions in TypeScript have more to offer than meets the eye. Let’s dive into some hidden gems to take your TypeScript skills to the next level.

Discriminated Unions

TypeScript supports a concept called “discriminated unions,” where each type in the union has a common property called a “discriminator.” Discriminated unions enable pattern matching in switch statements, which can make your code more readable and less error-prone.

Let’s expand the previous Redux example with a discriminator property:

type CounterAction =
  | { type: 'INCREMENT', payload: number }
  | { type: 'DECREMENT', payload: number };

// ...

The type property acts as the discriminator in this example. Now, the reducer can take advantage of pattern matching:

const counterReducer = (
  state: CounterState,
  action: CounterAction
): CounterState => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + action.payload };
    case 'DECREMENT':
      return { ...state, count: state.count - action.payload };
    default:
      return state;
  }
};

Intersection with Unions

Sometimes, you might need to combine multiple types with a union while maintaining their original structure. You can achieve this by using intersections. Consider an example where you have two different data shapes for a user: one for guests and one for registered users:

type Guest = {
  id: string;
  isGuest: true;
};

type RegisteredUser = {
  id: string;
  isGuest: false;
  email: string;
};

type User = Guest | RegisteredUser;

Here, the User type is a union of Guest and RegisteredUser, allowing you to handle both guest and registered user data.

Pitfalls to Avoid

While unions are a valuable tool, they come with certain caveats that developers should be mindful of. By avoiding these pitfalls, you can make the most of unions and maintain a clean, robust codebase in your TypeScript projects.

Overusing Unions

Unions can be powerful, but it’s essential to use them sparingly. When a type contains too many variations, it may become difficult to reason about or maintain the code. Consider breaking down complex types into smaller, more specific types to keep your codebase clean and manageable.

Ambiguous Types

Be cautious with union overlapping types, as it can lead to ambiguity. For example:

type MyUnion = number | string | boolean;

Using this union, it’s unclear which type a variable of type MyUnion will hold. Aim for clear and distinct types to prevent confusion.

Proper Handling of Union Types

Ensure that you handle all possible variations in your union types. For instance, in the Redux example, take all action types in the reducer’s switch statement. Refrain from handling all cases to avoid unexpected runtime errors.

Conclusion

TypeScript unions provide developers a powerful tool for creating flexible and well-typed applications. By understanding their usage, leveraging hidden features, and avoiding common pitfalls, you can improve the safety and readability of your code. Whether working with React components or managing state with Redux, unions are an invaluable addition to your developer toolkit. Embrace them wisely, and you’ll write more robust and maintainable code in your React applications.

‘Till next time!