Kodumaro :: Sendo honesto em Typescript

Publicado em 11 de Julho, 2017
As sombras da programação.
Typescript

Recentemente comecei minhas aventuras em Typescript e percebi algo importante: é preciso ser muito honesto com a declaração de tipos.

O problema com a tipagem do Typescript é que ela só pode ser verificada em tempo de transpilação, uma vez que a transpilação gera código Javascript, cuja tipagem é dinâmica e também bastante fraca.

Vamos a um exemplo:

import * as sequelize from "sequelize"

export interface Employee {
  id: number
  name: string
  birth: Date
}

export function registerEmployee(employee: Employee): Promise<boolean> {
  return sequelize.query(
    "INSERT INTO t_employee VALUES (?, ?, ?)",
    {
      replacements: [ employee.id, employee.name, employee.birth ],
      type: sequelize.QueryType.INSERT,
    }
  ).then(() => true).catch(err => {
    console.error(err)
    return false
  })
}

Parece um código bastante seguro, uma vez que, se for passado um elemento que não seja um Employee para registerEmployee, o código nem mesmo transpila.

Porém esta é uma falsa segurança. Considere dois casos:

  1. A função é chamada de um código Javascript.
  2. A rotina que chama esta função recebe employee de uma resposta ou uma requisição do Express.

Nesses dois casos, não há como garantir que employee será do tipo Employee.

Para solucionar este problema precisamos ser bastante honestos – e verborrágicos – quanto à representação dos tipos.

Neste caso, o objeto recebido pode não ter qualquer um dos atributos esperados – ou pior: nem ser um object.

Temos então de lidar com as possibilidades – usaremos Underscore.js para ajudar:

import * as _ from "underscore"
import * as sequelize from "sequelize"

export interface Employee {
  id: number
  name: string
  birth: Date
}

function castEmployee(data: any): Employee {
  const check = _.isObject(data)
    && _(data).has("id")    && _.isNumber(data.id)
    && _(data).has("name")  && _.isString(data.name)
    && _(data).has("birth") && _.isDate(birth)

  if (check)
    return <Employee>data

  throw new TypeError(`${data} is not an Employee`)
}

export function registerEmployee(employee: Employee): Promise<boolean> {
  employee = castEmployee(employee)
  return sequelize.query(
    "INSERT INTO t_employee VALUES (?, ?, ?)",
    {
      replacements: [ employee.id, employee.name, employee.birth ],
      type: sequelize.QueryType.INSERT,
    }
  ).then(() => true).catch(err => {
    console.error(err)
    return false
  })
}

Parece rude levantar uma exceção, mas é honesto – tipo errado leva a um TypeError.

Em resumo: se é possível que um valor seja any, é preciso que ele seja tratado como tal, e o tratamento e casting precisam ser feitos manualmente. Se um valor puder ser undefined, é preciso tratá-lo como tal (Employee | undefined, por exemplo). Não podemos confiar totalmente na tipagem estática se o código será compilado para uma tecnologia que não suporta tal ferramenta.