DEV Community

Cover image for Testando tipos genéricos com ts-infer
Filipe Roberto Beck
Filipe Roberto Beck

Posted on

Testando tipos genéricos com ts-infer

Entre as linguages de programação que conheço, Typescript é a mais flexível em relção à genéricos. Um exemplo da "magia negra" que pode ser feita está no artigo How to master advanced TypeScript patterns de Pierre-Antoine Mills, criador do ts-toolbel. A complexidade pode crescer ao ponto de se tornar trabalhoso testar se a definição genérica está bem formada. Se formos automatizar com testes unitários usando ts-jest, por exemplo, podemos detectar erros criando testes que instanciam os tipos conforme o esperado e o próprio compilador se encarregará de falhar caso a definição esteja mal formada. Porém, há dois problemas com essa abordagem:

  1. Detecção da unidade de teste: ocorre um erro de sintaxe na compilação do arquivo, e não no teste da unidade, além de encerrar a execução do script. O log de saída do teste mostra a linha onde o erro ocorreu, e não o teste onde ocorreu e o resultado esperado. Erro ts-jest
  2. Testes de cenários inválidos: não é possível verificar se as restrições do tipo genérico estão bem definidas. Se você quiser confirmar se um valor inválido está realmente sendo tratado como erro, não há recursos para isso.

Para tentar resolver esses dois problemas, criei uma lib, ts-infer, que é bem simples de usar. Ela funciona fazendo uso da API do Typescript e do comentário // @ts-ignore. O comentário // @ts-ignore é usado para evitar erros na compilação pelo ts-jest dentro dos callbacks fornecidos para as funções do pacote. Dentro da função, as ocorrências desse comentário serão removidas e o callback será recompilado para que o erro possa ser inferido.

Atualmente, há duas funções exportadas pelo pacote: infer e diagnose. Segue um exemplo de uso:

// `TypeOfProps` infere a união entre os tipos
// das propriedades de uma classe ou interface.
test('união de `Object.TypeOfProps` em conformidade', () => {
  const invalidInferences = () => {
    // A função `infer()` lança uma exceção se encontrar erros de compilação.
    infer(() => {
      interface TProps {
        a: number
        b: any[]
        c: 'c'
        d: never
      }
      // `TypeOfProps` de `<TProps>` deve ser `number | string | any[]`
      let abc: Object.TypeOfProps<TProps>
      // A linha abaixo será removida por `infer()`
      // @ts-ignore
      abc = true
    })
  }

  expect(invalidInferences).toThrow()
})
// `PickPropsWithTypes` infere um subconjunto contendo apenas
// as propriedades que extendam um determinado tipo
test('propriedades de `Object.PickPropsWithTypes` em conformidade', () => {
  // A função `diagnose()` retorna um array com os diagnósticos dos erros encontrados
  const invalidInferences = diagnose(() => {
    interface Inter {
      a: string
      b: number
      c: string
      d: boolean
      e: Object
    }
    // `PickPropsWithTypes` de `<Inter, string | number>`
    // deve ser `{ a: string, b: number, c: string }` 
    let pickedProps: Object.PickPropsWithTypes<Inter, string | number>

    pickedProps = {
      // A linha abaixo será removida por `diagnose()`
      // @ts-ignore
      d: true
    }

    pickedProps = {
      // A linha abaixo será removida por `diagnose()`
      // @ts-ignore
      e: {}
    }
  })

  expect(invalidInferences.length).toBe(2)
})

A função infer() dentro do primeiro teste lança uma exceção se o callback fornecido apresentar erros de compilação. Já a função diagnose() no segundo teste retorna uma array com os diagnósticos dos erros encontrados. Ambas removem os comentário // @ts-ignore e depois compilam o callback.

Atuais problemas:

  • O escopo do callback fornecido se resume às declarações locais desse callback e declarações de importação top-level do arquivo de teste.
  • É criado um arquivo temporário contendo o código do callback e das importações top-level no mesmo diretório do arquivo de teste.
  • Baixa performance. O processo de compilação é realizado para cada callback fornecido.

Pretendo fixar esses 3 problemas em uma próxima versão compilando o arquivo de teste inteiro na primeira execução e depois extrair os erros de cada callback. Isso elimina a restrição do escopo, a necessidade de criar um arquivo temporário e o problema de performance.

Top comments (0)