TypeScript: Guia para Declarações de Tipos de Ambiente

Criando arquivos .d.ts de qualidade!

Eduardo Rabelo
6 min readSep 17, 2018
Créditos da imagem Okazuki

🌟 Créditos

Uma das minhas coisas favoritas sobre o TypeScript é a habilidade de utilizar todo o ecosistema do JavaScript. Se o seu código TypeScript precisa usar uma biblioteca que está escrita em puro JavaScript, você sempre pode escrever declarações de ambiente que descrevem os tipos que estariam lá, se ela fosse escrita em TypeScript.

Tipos de Ambiente

Por exemplo, se você usar um pacote do npm chamado firetruck, que se parece com:

/**
* Orienta o bico de água e joga água por 30 segundos
* @param theta ângulo de rotação horizontal do bico
* @param phi ângulo vertical do bico
* @param pressure pressão da água
*/
async function extinguish(theta, phi, pressure) {
await aimHose(theta, phi);
await sprayWater(pressure);
}
export default extinguish;

Em TypeScript, você pode receber vários erros sobre tipos de módulos não encontrado ou os “implíticos” any, que TypeScript não consegue inferir o tipo sem maiores informações.

Para resolver esse problema, você definiria alguns tipos no seu projeto para descrever esses códigos. Esses tipos geralmente são escritos em arquivos de declarações, *.d.ts, que contém apenas tipos e nenhum valor. Um exemplo, seria:

// firetruck.d.ts
declare function extinguish(theta: number,
phi: number,
pressure: number): Promise<void>;
export default extinguish;

Agora, ao tentarmos utilizar extinguish em algum arquivo TypeScript, nós iremos ter a checagem de tipo nos argumentos e saberíamos que o valor retornado é uma Promise.

Normalmente, esses arquivos são repassados ​​entre os desenvolvedores interessados ​​neste pacote npm e, eventualmente, alguém decide publicá-los no NPM. Isso certamente significa que serão enviados ao repositório DefinitelyTyped, pois o TypeScript é explicitamente configurado para tornar o uso dos tipos declarados neste pacote muito simples para os usuários consumirem e o Visual Studio Code pode encontrá-los e baixá-los automaticamente sob demanda.

DefinitelyTyped

No nosso caso, nós abriríamos um pull request em DefinitelyTyped criando uma nova pasta como types/firetruck/. Nós iríamos pegar o nosso firetruck.d.ts e copiar ele em types/firetruck/index.d.ts - indicando que estamos descrevendo o ponto de entrada primário desse pacote npm.

Há MUITA coisa acontecendo nesse repositório — literalmente milhares de pacotes npm sendo trabalhados por milhares de colaboradores. Você acabará interagindo com o dt-review-bot, para facilitar o merge dos seus tipos. Depois que seu trabalho for publicado, o bot notificará os colaboradores listados na parte superior dos arquivos *.d.ts, sempre que problemas relevantes ou solicitações de pull request forem abertas.

Os cabeçalhos desses arquivos *.d.ts seguem um formato específico que é analisado como parte do processo de publicação de tipos. Abaixo, nós declaramos um número de versão (falaremos mais sobre isso), a versão mínima suportada do TypeScript e os colaboradores que devem ser alertados sobre PRs/problemas.

// Type definitions for firetruck 1.3
// Project: https://github.com/firetruck/firetruck#readme
// Definitions by: Mike North<https://github.com/mike-north>
// TypeScript Version: 2.4

Logo após sua colaboração ser aceita, o serviço types-publisher irá publicar as alterações no npm como pacotes @types. O nosso exemplo, acabaria sendo chamado de @types/firetruck na v1.3.0. Alterações subseqüentes nos tipos irão incrementar automaticamente o número de versão pacth.

Dicas gerais para escrever boas declarações de tipos

Espero que eu encontre tempo para escrever mais sobre isso, mas, de um modo geral, há alguns princípios importantes que irão te ajudar a começar.

Tipos inadequadamente restritos são mais irritantes (e mais destrutivos para o código de seus usuários) do que os tipos inadequadamente frouxos.

Vamos dar uma olhada em um exemplo simples:

declare function generateId(): string | number;

Considere uma situação em que o tipo de retorno dessa função é inadequado (ou seja, 100% do tempo em que uma string é retornada). O resultado, é que os usuários podem fazer algo como:

let id = '' + generateId ();

Ou aplicar um type guard que nunca será usado:

let id = generateId();
if (typeof id === ‘string’) { // não é necessário

}

Sabendo que é um tipo desnecessário, nós podemos pensar que isso é um pouco de programação defensiva e certamente não causará erros em tempo de execução.

Agora, considere uma situação em que generateId falha e retorna null. Nossos tipos não estão dizendo nada sobre isso. O que poderia nos levar a sérios problemas:

let id = generateId();
if (typeof id === ‘string’) {
id.toLowerCase(); // 💥 Erro em tempo de execução
}

Aqui está outro exemplo: considere uma função que serve para desabilitar algum componente de texto:

declare function setDisabled(elem: HTMLInputElement): void;

Se eu quisesse usar isso com um <textarea>, eu teria que jogar fora toda a segurança de tipo para fazer as coisas funcionarem:

setDisabled(myTextArea as any);

Uma vez que any entram em seu código, é improvável que novas versões dos tipos no seu código sejam seguras. O desenvolvedor teria que estar prestando muita atenção para saber que ele pode remover as any. O resultado dessa história é que as declarações de tipo, inadequadamente estritas, resultam em usuários utilizando toneladas de atalhos (ex: any) em sua segurança de tipo.

Testes

Como qualquer tipo de código, os tipos de ambiente precisam ter testes para evitar regressões. Você vai seguir as mesmas práticas que está acostumado a seguir para outros tipos de código, como:

  • As correções de bugs devem ser acompanhadas de casos de teste que comprovam a eficácia da correção e protegem contra futuras regressões
  • Têm casos de teste positivos e negativos
  • Testa que a solução funciona e não os aspectos internos de como você chegou lá

O que é diferente sobre o teste de tipos de ambiente é o modo como você os escreve. Não temos valores a serem declarados e nenhuma função a ser chamada. Felizmente, nossas necessidades de teste são muito mais simples! Tudo o que precisamos testar tem a ver com a ideia de equivalência de tipos (ou seja, se os valores do tipo T são designáveis ​​a uma variável do tipo V).

Escrever um simples teste positivo é fácil. Tudo o que você precisa fazer é criar um novo arquivo TypeScript e tentar uma atribuição:

const result: Promise<any> = extinguish(30, 20, 100); // ✅

Se isso for compilado com sucesso, significa que o que quer que extinguish retorne, é atribuível a um Promise<any>. A função extinguish realmente retorna Promise<void>, então vamos alterar nosso teste:

const result: Promise<void> = extinguish(30, 20, 100); // ✅

Hmm. Isso também passa. Acontece que Promise<void> e Promise<any> são atribuíveis uns aos outros (any vai corresponder a qualquer coisa).

Se realmente queremos ter certeza de que o sistema de tipos nos ajude com qualquer um que tente usar o valor que esta Promise resolve (é isso que estamos dizendo ao retornar a Promise<void>), temos que envolver outra ferramenta.

dtslint

A biblioteca dtslint executa o TSLint em seus arquivos TypeScript (incluindo declarações) e compara a lista de erros encontrados com os comentários em seu código-fonte. Poderíamos anotar o tipo retornado pela nossa função usando um comentário:

extinguish(30, 20, 100); // $ExpectType Promise<void> ✅

Como o tipo do valor é será stringified e comparado ao nosso comentário, não estamos mais usando a atribuição como o mecanismo por trás do teste. Estamos comparando diretamente as strings. Se você alterar o tipo comentado em Promise<any>, este teste falhará.

Outro tipo de teste que poderíamos escrever seria um erro esperado:

extinguish(30, 20, 100).then(data => { 
data.abc // $ ExpectError ✅
});

Se data for do tipo void, tentar ler qualquer propriedade causará um erro de tipo. Se uma linha com $ExpectError for encontrada onde nenhum erro de tipo ocorre, dtslint indicará uma falha de teste. Se você já usou algo como QUnit.throws ou Chai throws esse comportamento será familiar para você.

Nesse repositório temos um exemplo com um caso de teste positivo e negativo aplicado aos tipos de ambiente.

A taxa na qual o TypeScript está se tornando popular é realmente incrível, e a quantidade de bibliotecas populares com informações de tipo é uma grande parte que facilita a adoção. Quando feito corretamente, os consumidores vão jurar que essas bibliotecas foram escritas em TypeScript desde o início!

--

--