DEV Community

Cover image for Typescript - Union types e type guards
Luis Gustavo Macedo
Luis Gustavo Macedo

Posted on • Updated on

Typescript - Union types e type guards

Salve galera!

Hoje quero compartilhar mais um pouco sobre typescript, especificamente algumas técnicas utilizando Union Types e type guards.

Vou mostrar como você pode melhorar seu código typescript pra deixá-lo mais legível, e bem documentado.

1 - Union types

Um Union types é um tipo formado por outros tipos, onde pode assumir qualquer um daqueles possíveis valores.

Alguns exemplos

type NumberOrString = number | string;

type Status = "idle" | "loading" | "success" | "failure"

// React useState, can receive a value or a function as parameter to serve as initial value. 
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/a03856975a17eba524739676affbf70ac4078176/types/react/v17/index.d.ts#L920
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
Enter fullscreen mode Exit fullscreen mode

2 - Typeguard

Já o typeguard é uma forma de dizer para o typescript, por meio de algum fluxo de controle if/else, loops, ternários e etc, refinar - chamamos de narrow - um determinado tipo para um tipo mais específico em algum escopo definido.

Dessa forma, conseguimos inferir que em algum ponto do código, um tipo pode assumir um tipo específico.

Pra quem quiser ler mais sobre narrowing e typeguards, lá tem bastante exemplo e uma explicação completa.

Podemos fazer o narrowing apenas com operações javascript.

// using switch
type Order = OrderCompleted | OrderProcessing | OrderReady;

function processOrder(order: Order) {
  switch (order.status) {
    case "completed": {
      console.log(order.deliveryAt) // Narrow to OrderCompleted 
      return;
    }
    case "processing": {
      console.log(order.expectDeliveryAt) // Narrow to OrderProcessing 
      return;
    }
    case "ready": {
      console.log(order.productId) // Narrow to OrderReady 
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

TS Playground

// using in operator
interface Dog {
    walk: () => void;
}

interface Fish {
    swim: () => void;
}

type Animal = Dog | Fish;

function move(animal: Animal) {
    if ("walk" in animal) {
        animal.walk(); // Narrow to Dog
        return;
    }

    animal.swim(); // Narrow to Fish
}


// using typeof
function promise(callback?: () => void) {
    // ...
    if (typeof callback !== "undefined") {
        callback(); // So in this scope you ensure that function exists
    }
}
Enter fullscreen mode Exit fullscreen mode

Ts Playground

Tudo que fizemos até agora foi apartir do fluxo do código usar operações javascript para ajudar o typescript a fazer o narrowing e inferir tipos mais específicos.

Agora, se quisermos fazer nossas próprias validações, sem usar explicitamente operações javascript como typeof, in, instanceof e outros, no fluxo do código, mas sim encapsular toda a lógica de validação em uma função, como um parser ou algo do tipo. Pra isso podemos usar o type predicates do typescript.

3 - Type Predicates

Um exemplo muito comum é quando fazemos uma chamada pra API e apenas inferimos o tipo do retorno, mas o que garante pra gente que a API mandou o objeto correto?

O problema

interface User {
    name: string;
    email: string;
}

declare const api: <T>(endPoint: string) => T

function getUser() {
    const user = api<User>("/user"); // We assume that the response payload actually sent the object with the type strictly equal to User

    console.log(user.email); 
    console.log(user.name);
}
Enter fullscreen mode Exit fullscreen mode

Podemos fazer uma função para garantir que o objeto user é de fato do tipo User.

interface User {
    name: string;
    email: string;
}

function isUser(input: unknown): input is User  {
    return (
        typeof input === "object" && 
        !!input &&
        "name" in input &&
        "email" in input
    ) 
}

declare const api: <T>(endPoint: string) => T

function getUser() {
    const user = api("/user"); // We can pass the User as a generic here, however we have to remember to do the validation

    if (isUser(user)) { // With this type guard, we can be sure that the API response actually returned a User. 
        console.log(user.email); 
        console.log(user.name);
        return;
    }
}
Enter fullscreen mode Exit fullscreen mode

Ts Playground

Porém ainda tem um problema aqui, veja no playground.

Suponha que estamos no final da sprint, e então surge uma urgência e temos que introduzir mais um campo no tipo User, por exemplo, o campo avatar. Nossa função isUser não vai acusar nenhum erro por não estarmos verificando o campo avatar.

Como a sprint está acabando e fizemos a alteração as pressas, esquecemos de alterar a função e nosso isUser não está funcional, não cumpre com o que deveria, por que não valida se de fato o objeto é um User. Então, bummm introduzimos um possível bug em produção.

Podemos resolver isso mudando um pouco nossa função para ser um parser de fato, e deixar ela mais completa validando também o tipo de cada propriedade

function parseUser(input: unknown): User | null {
    const isObject = typeof input === "object" && input
    if (!isObject) return null;

    const hasUserProperties = 
        "name" in input &&
        "email" in input

    if (!hasUserProperties) return null;

    const { email, name } = input;

    if (typeof email !== "string") return null;
    if (typeof name !== "string") return null;

    return { email, name } // Property 'avatar' is missing in type '{ email: string; name: string; }' but required in type 'User'.(2741)
}

function isUser(input: unknown): input is User {
    return input !== null
}
Enter fullscreen mode Exit fullscreen mode

Veja no playground, agora se novas propriedades forem adicionadas nossa função irá acusar erro.

Já temos algumas ótimas ferramentas pra fazer todo esse parser, como o zod

4 - Extract Utility

Pra finalizar, quero mostrar um utilitário muito poderoso que o typescript oferece quando trabalhamos com union types.

Por exemplo, se o union for de uma biblioteca externa, e não temos acesso a cada possível valor de forma isolada. Podemos inferir o tipo específico usando o Extract.

O Extract recebe dois genéricos, o primeiro é o Union de fato e o segundo apenas a união que vai ser extraída.

type Payment = 
    | { status: "pending" }
    | { status: "paid", paidAt: string }
    | { status: "canceled", canceledAt: string } 

type PendingPayment = Extract<Payment, { status: "pending" }>; // { status: "pending" }
type PaidPayment = Extract<Payment, { status: "paid" }>; // { status: "paid", paidAt: string }
type CanceledPayment = Extract<Payment, { status: "canceled" }>; // { status: "canceled", canceledAt: string } 

type Shape = {
  kind: "square";
  size: number;
} | {
  kind: "circle";
  radius: number;
};

type Square = Extract<Shape, { status: "square" }>; // { kind: "square"; size: number; } 
type Circle = Extract<Shape, { status: "circle" }>; // { kind: "circle"; radius: number; }
Enter fullscreen mode Exit fullscreen mode

TS Playground

É isso, acho que assim da pra deixar o código mais explícito, legível e bem documentado.

Referências:

Top comments (0)