DEV Community

Cover image for TypeScript: Os 5 mandamentos ao usar TypeScript
Eduardo Rabelo
Eduardo Rabelo

Posted on

TypeScript: Os 5 mandamentos ao usar TypeScript

Créditos da Imagem

Mais e mais projetos e equipes estão adotando o TypeScript. No entanto, há uma enorme diferença entre usar o TypeScript e tirar o máximo proveito dele.

Apresento a você esta lista de práticas de alto nível recomendadas para TypeScript que ajudarão você a aproveitar ao máximo suas funcionalidades.

Este artigo também está disponível em russo: 5 заповедей TypeScript-разработчика (por Vadim Belorussov).

Não minta

Tipos são um contrato. O que isso significa? Quando você implementa uma função, seu tipo é uma promessa para outros desenvolvedores (ou para você no futuro)!

No exemplo a seguir, o tipo da promessa getUser retornará um objeto que sempre terá duas propriedades: name e age.

interface User {
  name: string;
  age: number;
}

function getUser(id: number): User { /* ... */ }

TypeScript é uma linguagem muito flexível. Está cheio de suposições feitas para facilitar sua adoção. Por exemplo, TypeScript permite implementar getUser assim:

function getUser(id: number): User {
  return { age: 12 } as User;
}

Não faça isso! Isso é mentira. Ao fazer isso, você mente para outros desenvolvedores também (que usarão essa função em suas funções). Eles esperam que o objeto retornado em getUser sempre tenha algum name. Mas isso não acontece! Então, o que acontece quando seu companheiro de equipe escreve getUser(1).name.toString()? Você conhece bem...

Claro, essa mentira parece muito óbvia. No entanto, ao trabalhar com uma enorme base de código, você geralmente se encontra em uma situação em que um valor que deseja retornar (ou passar) quase corresponde ao tipo esperado. Descobrir o motivo da incompatibilidade de tipos leva tempo e esforço e você está com pressa ... então decide fazer utilizar type casting com as.

No entanto, ao fazer isso, você viola o contrato, que é sagrado! É sempre melhor dedicar um tempo para descobrir por que os tipos não combinam do que para fazer type casting. É muito provável que algum bug em tempo de execução esteja oculto sob a superfície.

Não minta. Respeite seus contratos.

Seja preciso

Tipos são documentação. Quando você documenta uma função, não deseja transmitir o máximo de informação possível?

// Retorna um objeto
function getUser(id) { /* ... */ }

// Retorna um objeto com duas propriedades: name e age
function getUser(id) { /* ... */ }

// Se id for um número e um usuário com esse id existe,
// retorna um objeto com duas propriedades: name e age
// Caso contrário, retorna undefined
function getUser(id) { /* ... */ }

Qual comentário em getUser você prefere? Quanto mais você souber sobre o que a função retorna, melhor. Por exemplo, sabendo que poderia retornar undefined, você pode escrever uma instrução if para verificar se o valor retornado está definido antes de acessar suas propriedades.

É exatamente o mesmo com os tipos. Quanto mais preciso for um tipo, mais informações ele transmite.

function getUserType(id: number): string { /* ... */ }

function getUserType(id: number): 'standard' | 'premium' | 'admin' { /* ... */ }

A segunda versão do getUserType é muito mais informativa e, portanto, coloca o chamador em uma situação muito melhor. É mais fácil manipular um valor se você souber o que ele é com toda a certeza (contratos, lembra?), uma das três strings, ao invés de saber que ele pode ser qualquer string. Para iniciantes, você tem certeza de que o valor não é uma sequência vazia.

Vamos ver um exemplo mais realista. O tipo State representa o estado de um componente que busca alguns dados do back-end. Esse tipo é preciso?

interface State {
  isLoading: boolean;
  data?: string[];
  errorMessage?: string;
}

O consumidor desse tipo deve lidar com algumas combinações improváveis ​​de valores de propriedade. Por exemplo, não é possível que ambos data e errorMessage sejam definidos (a busca de dados pode ser bem-sucedida ou resultar em um erro).

Podemos tornar um tipo muito mais preciso com a ajuda de tipos de união discriminados:

type State =
   | { status: 'loading' }
   | { status: 'successful', data: string[] }
   | { status: 'failed', errorMessage: string };

Agora, o consumidor desse tipo tem muito mais informações. Eles não precisam lidar com combinações ilegais de valores de propriedades.

Seja preciso. Transmita o máximo de informações possível em seus tipos!

Comece com tipos

Como os tipos são contrato e documentação, eles são ótimos para projetar suas funções (ou métodos).

Existem muitos artigos na Internet que aconselham os engenheiros de software a pensar antes de escreverem o código . Eu concordo totalmente com essa abordagem. É tentador pular direto para o código, mas geralmente leva a algumas decisões ruins. Passar algum tempo pensando na implementação sempre compensa.

Os tipos são super úteis nesse processo. Pensar pode resultar na anotação das assinaturas de tipo das funções envolvidas na sua solução. É incrível porque permite que você se concentre no que suas funções fazem, ao invés de como elas fazem.

O React.js tem um conceito de componentes de ordem superior. São funções que aumentam determinado componente de alguma maneira. Por exemplo, você pode criar um componente withLoadingIndicator que adiciona um indicador de carregamento a um componente existente.

Vamos escrever a assinatura de tipo para esta função. Ele pega um componente e retorna um componente. Podemos usar o React ComponentType para indicar um componente.

ComponentType é um tipo genérico parametrizado pelo tipo de propriedades do componente. withLoadingIndicator pega um componente e retorna um novo componente que mostra o componente original ou mostra um indicador de carregamento. A decisão é tomada com base no valor de uma nova propriedade booleana isLoading. Portanto, o componente resultante deve exigir as mesmas propriedades que o componente original mais a nova propriedade.

Vamos finalizar o tipo. withLoadingIndicator pega um componente de um tipo em ComponentType<P> que P denota o tipo das propriedades. Retorna um componente com propriedades aumentadas do tipo P & { isLoading: boolean }.

const withLoadingIndicator = <P>(Component: ComponentType<P>) 
    : ComponentType<P & { isLoading: boolean }> =>
        ({ isLoading, ...props }) => { /* ... */ }

Descobrir o tipo dessa função nos forçou a pensar sobre sua entrada e saída. Em outras palavras, nos fez projetá-la. Escrever a implementação é um pedaço de bolo agora.

Comece com tipos. Deixe os tipos forçarem você a projetar antes de implementar.

Abrace o rigor

Os três primeiros pontos exigem que você preste muita atenção nos tipos. Felizmente, você não está sozinho na tarefa - o compilador TypeScript geralmente informa quando seus tipos estão ou quando não são precisos o suficiente.

Você pode tornar o compilador ainda mais útil ativando o sinalizador --strict do compilador. É uma bandeira meta que permite que todas as opções estritas verificação de tipo: --noImplicitAny, --noImplicitThis, --alwaysStrict, --strictBindCallApply, --strictNullChecks, --strictFunctionTypes e --strictPropertyInitialization.

O que eles fazem? Em geral, habilitá-los resulta em mais erros do compilador TypeScript. Isso é bom! Mais erros do compilador significam mais ajuda do compilador.

Vamos ver como a ativação --strictNullChecks ajuda a identificar algumas mentiras.

function getUser(id: number): User {
    if (id >= 0) {
        return { name: 'John', age: 12 };
    } else {
        return undefined;
    }
}

O tipo de getUser diz que sempre retornará um User. No entanto, como você pode ver na implementação, ele também pode retornar um valor undefined!

Felizmente, ativando --strictNullChecks retorna um erro do compilador:

Type 'undefined' is not assignable to type 'User'.

O compilador TypeScript detectou a mentira. Você pode se livrar do erro dizendo a verdade:

function getUser(id: number): User | undefined { /* ... */ }

Abrace o tipo de verificação da rigidez. Deixe o compilador observar seus passos.

Mantenha-se atualizado

A linguagem TypeScript está sendo desenvolvida em um ritmo muito rápido. Há um novo lançamento a cada dois meses. Cada versão traz melhorias significativas no idioma e novos recursos.

Geralmente, os novos recursos de idioma permitem tipos mais precisos e uma verificação mais rigorosa.

Por exemplo, a versão 2.0 introduziu Tipos de União Discriminada (que eu mencionei no tópico Seja preciso).

Versão 3.2 introduziu a opção --strictBindCallApply do compilador que permite a digitação correta das funções bind, call e apply.

A versão 3.4 melhorou a inferência de tipo em funções de ordem superior, facilitando o uso de tipos precisos ao escrever código em estilo funcional.

O que quero dizer aqui é que realmente vale a pena conhecer os recursos de linguagem introduzidos nas versões mais recentes do TypeScript. Eles geralmente podem ajudá-lo a aderir aos outros quatro mandamentos desta lista.

Um bom ponto de partida é o roteiro oficial do TypeScript. Também é uma boa idéia verificar a seção TypeScript do Microsoft Devblog regularmente, pois todos os anúncios de lançamento são feitos por lá.

Mantenha-se atualizado com os novos recursos de idioma e deixe a linguagem fazer o trabalho para você.

Finalizando

Espero que você ache essa lista útil. Como qualquer coisa na vida, esses mandamentos não devem ser seguidos cegamente. No entanto, acredito firmemente que essas regras o tornarão um programador TypeScript melhor.

Eu adoraria ouvir sua opinião sobre isso na seção de comentários.


Créditos

Top comments (0)