Testando tipos genéricos com ts-infer
Tags: testing, typescript
Published: 2020-01-29 01:34:13

---
title: Testando tipos genéricos com ts-infer
cover_image: https://i.imgur.com/upop01A.png
published: true
description: 
tags: testing, typescript
---

Entre as linguages de programação que conheço, [Typescript](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html) é a mais flexível em relção à [genéricos](https://www.typescriptlang.org/docs/handbook/generics.html). Um exemplo da "magia negra" que pode ser feita está no artigo [How to master advanced TypeScript patterns](https://www.freecodecamp.org/news/typescript-curry-ramda-types-f747e99744ab/) de [Pierre-Antoine Mills](https://github.com/pirix-gh), criador do [ts-toolbel](https://github.com/pirix-gh/ts-toolbelt). 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](https://github.com/kulshekhar/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](https://i.imgur.com/vW65QjU.png)
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](https://www.npmjs.com/package/ts-infer), que é bem simples de usar. Ela funciona fazendo uso da [API do Typescript](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) e do comentário [`// @ts-ignore`](https://medium.com/xp-inc/dica-r%C3%A1pida-typescript-conhecendo-o-ts-ignore-1aea51f0b7cc). O comentário [`// @ts-ignore`](https://medium.com/xp-inc/dica-r%C3%A1pida-typescript-conhecendo-o-ts-ignore-1aea51f0b7cc) é usado para evitar erros na compilação pelo [ts-jest](https://github.com/kulshekhar/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:

```typescript
// `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`](https://medium.com/xp-inc/dica-r%C3%A1pida-typescript-conhecendo-o-ts-ignore-1aea51f0b7cc) 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.