;

TypeScript Union Types


Union types in TypeScript allow you to work with variables that can hold multiple types, providing both flexibility and type safety. Whether you’re working with dynamic data from APIs or defining complex structures, union types can help make your code more predictable and error-free. This tutorial will cover everything you need to know about union types, from basic usage to real-world applications.

Introduction to TypeScript Union Types

Union types are one of TypeScript’s most powerful features, allowing you to combine multiple types in a single variable. They’re perfect for cases where a variable might hold different kinds of values, such as a string or number. Union types make your code more flexible and robust while still maintaining type safety, helping you handle real-world data effectively.

What is a Union Type in TypeScript?

A union type in TypeScript is a type that allows a variable to hold values of more than one type. You define union types using the | symbol, specifying each type that the variable can hold.

let value: string | number;
value = "Hello"; // OK
value = 42;      // OK
// value = true; // Error: Type 'boolean' is not assignable to type 'string | number'

In this example:

  • value can hold either a string or number, providing flexibility.
  • If you assign a type not specified in the union (like boolean), TypeScript will throw an error.

Declaring Union Types

Union types can be used for variables, parameters, and return types, allowing you to specify multiple acceptable types in various parts of your code.

Basic Syntax of Union Types

The syntax for union types is straightforward. Use the | symbol to separate the types that a variable can hold.

let id: string | number;
id = "user123";
id = 1001;

Here, id can hold either a string or number, making it suitable for scenarios where an identifier might be alphanumeric or numeric.

Union Types in Function Parameters and Return Types

You can also use union types in function parameters and return types, making your functions more adaptable.

function printValue(value: string | number): void {
  console.log(`Value: ${value}`);
}

printValue("Hello"); // Output: Value: Hello
printValue(123);     // Output: Value: 123

In this example:

  • printValue accepts either a string or number as the parameter, providing flexibility without sacrificing type safety.

Using Union Types in Functions

Union types are especially useful in functions, allowing you to handle multiple data types without writing separate functions for each type.

Example: Processing Different Types

Suppose you have a function that needs to process both strings and numbers differently. You can use a union type to define the parameter, then use conditional checks to handle each type appropriately.

function processInput(input: string | number): void {
  if (typeof input === "string") {
    console.log(`String input: ${input.toUpperCase()}`);
  } else {
    console.log(`Number input: ${input.toFixed(2)}`);
  }
}

processInput("hello"); // Output: String input: HELLO
processInput(3.1415);  // Output: Number input: 3.14

In this example:

  • processInput accepts either a string or number, handling each type differently based on the type check.

Type Narrowing with Union Types

Type narrowing is a way to refine union types within a function to allow type-specific operations. TypeScript uses conditional statements and type guards like typeof and instanceof to determine the type at runtime.

Type Narrowing with typeof

The typeof operator is commonly used with union types to check the type of a variable.

function describeValue(value: string | number | boolean): string {
  if (typeof value === "string") {
    return `String: ${value}`;
  } else if (typeof value === "number") {
    return `Number: ${value}`;
  } else {
    return `Boolean: ${value ? "true" : "false"}`;
  }
}

console.log(describeValue("TypeScript")); // Output: String: TypeScript
console.log(describeValue(42));           // Output: Number: 42
console.log(describeValue(true));         // Output: Boolean: true

In this example:

  • describeValue uses typeof to determine the specific type of value and then processes it accordingly.

Type Narrowing with instanceof

When working with classes and objects, you can use instanceof to narrow down the type in union types.

class Dog {
  bark() {
    console.log("Woof!");
  }
}

class Cat {
  meow() {
    console.log("Meow!");
  }
}

function makeSound(animal: Dog | Cat): void {
  if (animal instanceof Dog) {
    animal.bark();
  } else {
    animal.meow();
  }
}

makeSound(new Dog()); // Output: Woof!
makeSound(new Cat()); // Output: Meow!

Here:

  • makeSound accepts either a Dog or Cat and uses instanceof to check the specific type before calling the appropriate method.

Real-World Examples of Using Union Types

Example 1: Handling API Response Types

API responses often have different types based on the outcome, such as success or error. Union types allow you to handle these cases flexibly.

type SuccessResponse = {
  status: "success";
  data: string;
};

type ErrorResponse = {
  status: "error";
  error: string;
};

type ApiResponse = SuccessResponse | ErrorResponse;

function handleApiResponse(response: ApiResponse): void {
  if (response.status === "success") {
    console.log("Data:", response.data);
  } else {
    console.error("Error:", response.error);
  }
}

handleApiResponse({ status: "success", data: "User data" });
handleApiResponse({ status: "error", error: "Something went wrong" });

In this example:

  • ApiResponse is a union of SuccessResponse and ErrorResponse.
  • The function handleApiResponse checks status to determine which structure it’s handling.

Example 2: Configuring Function Parameters Dynamically

Union types are useful for functions that handle different input formats, such as specifying configuration options.

type Config = { path: string } | { url: string };

function loadResource(config: Config): void {
  if ("path" in config) {
    console.log(`Loading resource from path: ${config.path}`);
  } else {
    console.log(`Loading resource from URL: ${config.url}`);
  }
}

loadResource({ path: "/local/file.txt" });  // Output: Loading resource from path: /local/file.txt
loadResource({ url: "https://example.com" }); // Output: Loading resource from URL: https://example.com

In this example:

  • The Config type is a union of objects with either a path or a url property.
  • The loadResource function checks for the presence of each property to determine which logic to execute.

Union Types vs. Literal Types

Union types and literal types are related but serve different purposes.

  • Union Types: Allow variables to hold multiple types, such as string | number.
  • Literal Types: Specify exact values a variable can hold, such as "success" | "error".

Example of Union Types with Literal Values

type Status = "success" | "error" | "loading";

function updateStatus(status: Status): void {
  console.log(`Status updated to: ${status}`);
}

updateStatus("success"); // OK
updateStatus("loading"); // OK
// updateStatus("failed"); // Error: Argument of type '"failed"' is not assignable to parameter of type 'Status'

In this example:

  • Status is a union of literal types, restricting status to specific values.

Key Takeaways

  1. Union Types for Flexibility: Union types allow you to specify multiple types for a single variable, making code more flexible while maintaining type safety.
  2. Type Narrowing: Use typeof, in, and instanceof to narrow down union types, enabling specific operations based on the type.
  3. Handling API Responses: Union types are ideal for handling different structures in dynamic data, such as API responses.
  4. Difference from Literal Types: Union types combine multiple types, while literal types specify exact values.
  5. Best Practices: Use union types for parameters, return types, and handling multiple types in a single variable, always using type narrowing for safe operations.

Summary

Union types in TypeScript allow you to work with variables that can hold multiple types, enhancing flexibility without sacrificing type safety. Through type narrowing, you can create functions and structures that handle dynamic data accurately. By understanding and implementing union types effectively, you can make your TypeScript applications more adaptable and resilient in handling various data types and conditions.