Typescript - Union types e type guards
Tags: typescript, react, programming, functional
Published: 2023-07-17 14:45:16

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
```ts
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>>];
```

## 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`](https://www.typescriptlang.org/docs/handbook/2/narrowing.html), lá tem bastante exemplo e uma explicação completa.

Podemos fazer o `narrowing` apenas com operações javascript.
 
```ts
// 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 
    }
  }
}
```
[TS Playground](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgPJQCbQEJwM4oDeAUMsiHALYQBcyeYUoA5qcgA5QD2GArgmACSGOgyYhWAX2TFioSLEQp0WKAGEuldgBsIkDMggAPSCAx40mHPiJsGcMLzx0ARAk069EDC4A0bLG1gADdoAE8AQTA6ABEHCH9JWXloeCRLVQAFbiQ8PBZDEwgzCxVrAmQSMntHZ2QXTi5c-Ik-NmN2CAEYiCDQqEjo5DjIYiS5cFSlDOgAJQg4DDDC03MZqFwKqvowBydXKAWllzHZMDDO9eQAXnWNLV19ZAAfdeymiDyC17KoecWwgBuWQwXggATALggDg5T54X4ACi4VigdF+AEpKnYAO7AMAIAAWyCRKIAdDUnJjtmQEDZ6u4Hl4fHRqWRkO4QHguLpSdouMwSapSYEQuEopiAPQS5AAOTgUG42OQYC4dw8j28MjZbMOjigIGB2qS2tpFQasK+rRZbBNUK5PL5AuRQo6XTAPT6YrAkulcoVXCVKreFpazC12uQut4+sNbONbNNKBchwBLmtEfZdu5EF5-MF0FJjT4AmEPtl8sVytVv3+S3DZHjSUkQA)

```ts
// 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
    }
}
```
[Ts Playground](https://www.typescriptlang.org/play?#code/PTAEFcGcEsDsHNR1AewA4FMBOBDALilgFBx7YBmOAxhqACIqIDeRoboA7jgDYDWAXKAAUASlABeAHygAbimgATANxEAvkRKwyWSjVAAxaJAAWoFu1CQO0ALaDRE6XMUr1RPAE9MoAIKxbPBL0jKAAPgZGxipE5OCwVHjQKLCgNigyGEI4-jY8gn4B3GLm7NDkwgBEXHwVSCnZhcWsFuwNudwAdNW8okqgIHXk2KB4xvgjXrRGoDjB8M0toFgYeOBYsCoWbhZtPB1Wtr39YHBDWCNjeBPe07OGJmoaRANQcIiemCjkMXEJSSloLAoGxGTJUHjcABG1F4AH57GIpLJ5AomhYBh1MQskOUhB8MF9QODuFCYaAAITicSgCpxBQYchwDAKCpoxZEiHQqg9ER9AYAZRQdQu00gVHQtA8KHAoAwsEga1oo3GsXiiWSsoAHkY8JBsepVEA)

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`](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-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
```ts
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);
}
```
Podemos fazer uma função para garantir que o objeto `user` é de fato do tipo `User`.
```ts
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;
    }
}
```
[Ts Playground](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgKoGdrIN4ChkHIhwC2EAXMumFKAOYDc+hEJcwANpdbSI7gF9cuGAFcQCMMAD2IZMHQZoAClAAHUWEriA1iGkB3EAEpK6zfPRpMUAnkLIoEMKKhzlzBwTABPNRGkYeRANMGQAXkjkACJpACMAKwhJaOQAMjTkTy8AQhzzMIzsh2jiMlTQYND0tOLCaNZ2Dgq5AuzjLKFcABNkjjgnZARZamQ4NWBKAB4AFQA+ZQgQboAFaVAtKhp6DvC55BnhMQkpWWQ6ZyUoZQ77QmGQUdEbCLGJ5WiAemfoaOMGZCfT7IADqKAQcDkajg6CsYAAFigrmMrHBzktoMAEMhEU4ADQ4wwQABuWAMKHhcFJyDA0kcrFYcSwtOQ3TpCJQxLgHGA3TgpxA2WywCCqkUNmUPygxlugOBIOACJp8IUNL8KDoogG3QJ5KGkOQTKorhQCP5ypQAEEVgBJenoNQjFCIFzcjg+ekuNwQbpjazQAB0WS89xG0g4EADHGkdElNgDjU4-2DIaGYYjUZjccDZQg-zqBCcXpATAcQgEQA)

Porém ainda tem um problema aqui, veja no [playground](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgKoGdrIN4ChkHIhwC2EAXMumFKAOYDc+hEJcwANpdbSI8wTgA3OGDhRuNekwC+uXDACuIBGGAB7EMmDoM0ABSgADorCVlAaxDqA7iACUlY6e3o0mKAWzIA9D+QAchAgABaKJMjQUOrIcACOisACyFAQYIpQWvrJhMhgAJ5GEOow2iAmYMgAvDXIAETqAEYAVhCqdcgAZJ3IOYQAhP3Old19BHXEZB2gZRVdnWP1rOwc01rDyfa9crgAJm0c4igImtSxRsCUADwAKgB8+sG7AArqoGZUUnxbVXfIN-IlCo1JpkHQ0nooPotnhcicQGdFB5qudgPo6j4kdA6vYGL5-ABBMAAS+QRnU+xI6jcRjg6HQ4mQMUhyBOVOQ4TBwWgwAQsQSwAANGT1FBiRFIFS3AkUBxWI0oIz9sh4AAvLBwZAiDjAXZwADnAGP1PJcsBSoZdB59FioPYYfjkABhdQRCD0lAFIpgxTiXbC+GYOiJKV5DWs6CQVWamWxFLu8nUTV65AE54ASWQyvgYBiqVzmXUig5ERZi0D6jlADoOOo6DaPFXlpxcb1cnDTpWIDW6w3oFXJhBcYt8xkQExcnIZEA). 

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
```ts
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
}
```
Veja no [playground](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgKoGdrIN4ChkHIhwC2EAXMumFKAOYDc+hEJcwANpdbSI8wTgA3OGDhRuNekwC+uXDACuIBGGAB7EMgAO4zBmgAKUNsVhKygNYh1AdxABKSgajIAPkUUcOOAcgSa1MjA6ADyAEYAVhCqyAC8yGAAntoQ6jDBIKZg8XEJAETqUTFg+cgAZOWZ2X7AGYYAhCER0aoOyFAQYIpQWiBeHEx+ASBBABZw6C4AClDqqVBqEOjxyH6EyPnEZGWg1WYV5euE+azsHLtaJmbyG3XIjRNTmFCz89BL6O2d3b2e3kMNiMgthkGdOAAaIikFAyVbXMCAwj3QzJVLpMFsTjIBp5TY8ej5b5dHp9AZMO71NFpDLbFC4goEvhEjokv79AG3Qg-Uk4THnKF05BwgD0IuQbwWyWQAHJhKJxDLgisSCF0PRMokUigZaDwVwqFI+AxoWRJLxGMKleEDp0AI6KYCdAAmmupspcMoAdIYAEwAdgALABGBy4OQKZSqDRXZ5GBEWEDWOyOSgI5VoF6+DY8v7phn-Djh+TOmIccQoYE5ODaYCUAA8ABUAHyGCAgZ3TdSgcyGi3tOLN5CN+RKFRqTTIOhdFyGdp4IGBHKKLMJGvAQz5EUr6BEk1cggokKzndQBzztYbRejdQcCBejjqOiGU9e-UOfdXwjA2-3x-P186Q-Y4CFzEAKUIOQZCAA), 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](https://zod.dev/)

## 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.
```ts
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; }
```
[TS Playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBACghiAthAdsKBeKAoKeoA+UA3lAM7BzACuZAXFAESQoAmAligOaNQC+ufEVIUqtBszjtWjADRQwU1gEFgDCgCdOXfoLzDylGvSYBjOClMQANhBnzzlm3dXrgW7vxzZQkWKg5ueCRUdCwAUQAPdzhTYAAeYOQ0eREjcSYWQJ5+AD4AbigAeiKSQzETZgDtXgFfaHhpJNDMKCiYuMSEZOBU8uMJRWlaguLStIrBpTkFJVdDDx068GgAYQsrW1ZmtFb2jViEnd6y0QGzDecZPMKS0-TKx027Gaer+c1tL2wfFagAZQAFnA-FhiIIANacVgSMgAR2ocA0EEY+UEZHYAC8IAwUNREAAjCAaNF8QgkSHQiSmdgaUy2VGCA4cDJ4wnE0lo35+f4IpHQCLRA6dIEgiB9M4ZRjwxHIka3cZQKFsWF8uWFDHY3H4okk771KCrWn0gVtIWHeKiyASh7U40Mm5jMrKmFme0owrM9isnUc-hAA)

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

Referências:
- https://www.typescriptlang.org/docs/handbook/advanced-types.html
- https://www.typescriptlang.org/docs/handbook/2/narrowing.html#typeof-type-guards
- https://dev.to/noriste/keeping-typescript-type-guards-safe-and-up-to-date-a-simpler-solution-ja3
- https://levelup.gitconnected.com/typescript-keeping-type-guards-safe-and-up-to-date-2457d52bd722
- https://www.typescriptlang.org/pt/play#example/type-guards