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>>];
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
}
}
}
// 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
}
}
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);
}
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;
}
}
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
}
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; }
É 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
Top comments (0)