Ler em português

Estreitando tipos no Typescript

Mesmo que você tenha ou não ouvido falar sobre "Type Narrowing" (estreitando tipos), aposto que tem usado isso se você tem escrito código TypeScript ultimamente. Em resumo, Type Narrowing é uma estratégia usada pra "estreitar" um union type (ex: number | string) a um tipo específico com a ideia de tomar uma ação sobre ele.

A informação nesse artigo foi obtida da documentação original e bem escrita, que encorajo você a visitar caso esteja interessado em maiores detalhes.

Estreitando por "verdade"

Essa é provavelmente a prática mais comum dentre as demais. Ao checar pela "verdade" de uma dada expressão, conseguimos estreitar um tipo:

function sayHello(name: string | null) {
  if (name == null) {
    return 'Hi, guest' // (parameter) name: null
  }

  return `Hello, ${name}` // (parameter) name: string
}

Estreitando por igualdade

Ao usar operadores de igualdade, também é possível estreitar um tipo. No caso abaixo, o TypeScript assume que se um valor é igual ao outro, então ambos também tem o mesmo tipo:

function compare(a: string | null, b: string | number) {
  if (a === b) {
    a // (parameter) a: string
    b // (parameter) b: string
  } else {
    a // (parameter) a: string | null
    b // (parameter) b: string | number
  }
}

O operador typeof

typeof é um operador muito conhecido no mundo JavaScript. Quando TypeScript lê esse operador em um bloco de condição, ele enxerga como type guard (guarda-tipos) e estreita o tipo para do elemento:

function getYear(value: Date | number) {
  if (typeof value === 'number') {
    return new Date(value).getFullYear() // (parameter) value: number
  }

  return value.getFullYear() // (parameter) value: Date
}

O operador in

O operador in é usado para identificar se um objeto tem uma dada propriedade. Quando usado numa condição cujo resultado é true, ele estreita os tipos onde a propriedade checada seja opcional ou obrigatória. Caso contrário, se a condição é falsa, o tipo é estreitado para aqueles onde a propriedade opcional ou não foi definida.

type Sandwich = { ingredients: String[] }
type Pizza = { size: string }
type Salad = { size?: string, ingredients?: String[] }

function eat(food: Sandwich | Pizza | Salad) {
  if ('ingredients' in food) {
    // (parameter) food: Sandwich | Salad
  } else {
    // (parameter) food: Pizza | Salad
  }
}

Predicados do tipo (Type predicates)

Da mesma que usamos o operador in, também podemos usar o que é chamado de "type predicates" para estreitar um tipo, mas dessa vem sem utilizar construtores JavaScript. Para isso, precisamos criar uma função cujo tipo retornado seja um "type predicate", que possui o seguinte formato: parameterName is Type, veja abaixo:

type Sandwich = { ingredients: string[] }
type Pizza = { size: string }

function isPizza(food: Sandwich | Pizza): food is Pizza {
  return (food as Pizza).size !== undefined
}

const food = getPizzaOrSandwich() // Sandwich | Pizza
if (isPizza(food)) {
  // const food: Pizza
} else {
  // const food: Sandwich
}

União discriminada

Em alguns casos podemos precisar estreitar tipos para estruturas mais complexas, como uniões de tipos customizados. Suponhamos que temos os tipos a seguir:

type Fruit = { type: "fruit", color: string }
type Snack = { type: "snack", size: string }
type Food = Fruit | Snack

Mesmo que o exemplo abaixo esteja correto, o TypeScript não conseguirá identificar o tipo e vai exibir erro:

function getRecipe(food: Food) {
  if (food.color !== undefined) {
    // Property 'color' does not exist on type 'Food'.
      // Property 'color' does not exist on type 'Snack'.
  }
}

Para estreitar o tipo de Food, nós precisamos usar uma propriedade comum para ambos, Apple e Pizza. Nós consideramos essa propriedade como discriminante:

function getRecipe(food: Food) {
  if (food.type === 'fruit') {
    // (parameter) food: Fruit
  } else {
    // (parameter) food: Snack
  }
}

Conclusão

Estreitar tipos é uma tarefa comum e normalmente simples. No entanto, pode causar confusão em alguns cenários. Entender os conceitos que o TypeScript usa para estreitar tipos pode nos previnir dessas situações.