Language: EN

typescript-uniones

Using Unions in TypeScript

Union types in TypeScript allow a variable or a function to accept multiple data types. This gives us flexibility to define types and helps us in situations where values can be more than one type.

TypeScript still performs type checks at compile time, so we retain most of the advantages of static typing.

Union Type Declaration

To declare a union type, the vertical bar operator (|) is used between the types to be combined. This indicates that a variable can be of one type or another.

let value: number | string;

value = 42;       // Correct
value = "text";   // Correct

value = true;     // ❌ Error: Type 'boolean' cannot be assigned to type 'number | string'.

In the example above, the variable value can contain a number or a string, but not a boolean value.

Using union types in functions

Functions can also use union types for their parameters and return values.

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

printValue(123);       // Correct
printValue("string");  // Correct

printValue(true);   // ❌ Error: Type 'boolean' cannot be assigned to type 'number | string'.

Union types in object properties

We can use union types in the properties of interfaces and types to provide flexibility for the properties within an object.

interface Product {
    id: number;
    name: string;
    price: number | string; // The price property can be a number or a string
}

// product1 has number as price
let product1: Product = {
    id: 1,
    name: "Product A",
    price: 100
};

// product2 has string as price
let product2: Product = {
    id: 2,
    name: "Product B",
    price: "One hundred dollars"
};

In this example, the price property of the Product object can be a number or a string.

Narrowing

When working with union types, it is common to determine the specific type of a variable at runtime.

This is known as “narrowing” and can be achieved using type checks (type guards)

Using typeof

The typeof operator is useful for narrowing types when working with primitive types like numbers and strings.

function processValue(value: number | string): void {
    // Check if the value is a number
    if (typeof value === "number") {
        console.log(`The value is a number: ${value}`);
    } else {
        // If it is not a number, it must be a string
        console.log(`The value is a string: ${value}`);
    }
}

processValue(123);       // The value is a number: 123
processValue("text");    // The value is a string: text

Using instanceof

The instanceof operator is used to check if an object is an instance of a specific class, which is useful for narrowing types in classes and complex objects.

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

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

function makeSound(animal: Dog | Cat): void {
    // Check if the animal is an instance of Dog
    if (animal instanceof Dog) {
        animal.bark();  // If it is a Dog, bark
    } else {
        animal.meow();  // If not a Dog, it must be a Cat and meow
    }
}

let myDog = new Dog();
let myCat = new Cat();

makeSound(myDog);  // Woof!
makeSound(myCat);   // Meow!

Custom Checks

It is also possible to define custom type checks (type predicates) for more precise type narrowing.

// Bird interface
interface Bird {
    fly(): void;
    feathers: number;
}

// Fish interface
interface Fish {
    swim(): void;
    fins: number;
}

// Function that determines if an animal is a Bird
function isBird(animal: Bird | Fish): animal is Bird {
    // Check if the 'fly' method is defined on the object
    return (animal as Bird).fly !== undefined;
}

function doSomething(animal: Bird | Fish): void {
    if (isBird(animal)) {
        animal.fly();  // If it is a Bird, fly
    } else {
        animal.swim();  // If not a Bird, it must be a Fish and swim
    }
}

Using Aliases with Union Types

You can use type aliases to make union types clearer and more concise.

type ID = number | string;

function getUser(id: ID): void {
    // Logic to get user
}

In this example, we create an alias ID that can be number or string.

Compound type unions

Union types can discriminate even when the types are part of other types or objects.

type ApiResponse = { data: any; error: null } | { data: null; error: string };

function handleResponse(response: ApiResponse): void {
    if (response.error) {
        console.error(`Error: ${response.error}`);
    } else {
        console.log(`Data: ${response.data}`);
    }
}

let response: ApiResponse = { data: { id: 1, name: "John" }, error: null };
handleResponse(response);  // Data: { id: 1, name: "John" }

In this example,

  • ApiResponse can be a response with data and no errors, or without data and with errors.
  • The code handles both possibilities appropriately.