10 hábitos ruins ao usar TypeScript que você deve evitar!

27 de julho de 2022
Ronaldo B.

Neste artigo iremos comentar 10 hábitos ruins ao programar em TypeScript que devemos evitar. Iremos informar qual é o hábito ruim, por que é comum tê-lo e como podemos evitá-la.

Este artigo é uma tradução do artigo original em inglês, feito por Daniel Bartholomae. Você pode ler o artigo original clicando aqui. Esperamos que gostem!

1 - Não usar o modo strict

Como o código pode estar

Poderíamos criar o arquivo tsconfig.json sem definir o modo estrito:

{
  "compilerOptions": {
  	"target": "ES2015",
  	"module": "commonjs"
  }
}

Como o código deveria estar

Devemos simplesmente habilitar o modo estrito:

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs",
    "strict": true
  }
}

Por que temos esse hábito?

Introduzir regras restritas a uma base de código já existente leva tempo. Por isso, pode ser que não desejemos habilitar esse modo.

Por que deveríamos evitar esse hábito?

Regras mais estritas irão tornar mais fácil a mudança do código no futuro. Assim, valerá a pena o tempo que usarmos para corrigir os erros, pois seremos muito recompensados quando formos trabalhar com esse código em um repositório no futuro.

 

2 - Definir valores padrão com o operador ||

Como o código pode estar

Poderíamos definir valores opcionais usando o operador ||:

function createBlogPost (text: string, author: string, date?: Date) {
    return {
        text: text,
        author: author,
        date: date || new Date()
    }
}

Como o código deveria estar

Podemos usar o novo operador ??, ou, até melhor, definir os valores padrão no momento da declaração dos parâmetros:

function createBlogPost (text: string, author: string, date: Date = new Date())
    return {
        text: text,
        author: author,
        date: date
    }
}

Por que temos esse hábito?

O operador ?? é um recurso novo, que foi introduzido há pouco tempo, no ano passado. Além disso, quando estamos trabalhando com uma função muito grande e precisamos usar os valores no meio dela, pode ser difícil definir essas informações como valores padrão na declaração dos parâmetros.

Por que deveríamos evitar esse hábito?

O operador ??, diferentemente do operador ||, é acionado apenas nos valores null e undefined, e não em qualquer outro valor que poderia ser identificado como false. Além disso, se suas funções são tão longas que você não pode definir valores padrão na declaração de seus parâmetros, dividi-las ou “quebrá-las” em funções menores pode ser uma boa ideia.

 

3 - Usar o tipo any como tipo de variáveis

Como o código pode estar

Pode ser que decidamos usar o tipo any em variáveis que não temos certeza sobre o formato de suas informações, como uma variável que espera o retorno dos dados de uma API:

async function loadProducts(): Promise<Product[]> {
  const response = await fetch('https://api.mysite.com/products')
  const products: any = await response.json()
  return products
}

Como o código deveria estar

Na maioria dos casos em que definimos uma variável com o tipo any, deveríamos substituí-lo com o tipo unknown:

async function loadProducts(): Promise<Product[]> {
  const response = await fetch('https://api.mysite.com/products')
  const products: unknown = await response.json()
  return products as Product[]
}

Por que temos esse hábito?

O tipo any é muito conveniente, pois basicamente desabilita todas as verificações de tipos, o que evita erros na compilação do TypeScript. Além disso, frequentemente o tipo any é usado até mesmo em tipagens oficiais (por exemplo, response.json() no exemplo acima é tipado como Promise<any> pela equipe do TypeScript).

Por que deveríamos evitar esse hábito?

Esse tipo basicamente desabilita todas as verificações de tipo. Qualquer variável que for criada com o tipo any vai ser ignorada pela checagem de tipo. Isso faz com que seja muito difícil encontrar bugs, pois o código irá falhar apenas quando nossas suposições sobre a estrutura dos tipos forem relevantes para o compilador em tempo de execução.

 

4 - Definir o retorno de um valor com as

Como o código pode estar

O código pode estar informando para o compilador um tipo que ele não pode deduzir (inferir):

async function loadProducts(): Promise<Product[]> {
  const response = await fetch('https://api.mysite.com/products')
  const products: unknown = await response.json()
  return products as Product[]
}

Como o código deveria estar

Neste caso pode usar os type guards:

function isArrayOfProducts (obj: unknown): obj is Product[] {
    return Array.isArray(obj)  obj.every(isProduct)
}
function isProduct (obj: unknown): obj is Product {
    return obj != null
        typeof (obj as Product).id === 'string'
}
async function loadProducts(): Promise<Product[]> {
    const response = await fetch('https://api.mysite.com/products')
    const products: unknown = await response.json()
    if (!isArrayOfProducts(products)) {
        throw new TypeError('Received malformed products API response')
    }
    return products
}

Por que temos esse hábito?

Quando convertemos um código JavaScript para TypeScript, a base de código já existente geralmente realiza suposições sobre tipos que não podem ser automaticamente identificadas pelo compilador TypeScript. Nesses casos, informar uma valor diferente por meio do as OutroTipo pode aumentar a velocidade da conversão sem precisar que mudemos as configurações no arquivo tsconfig.json.

Por que deveríamos evitar esse hábito?

Mesmo que a definição dos tipos possa ser salva no momento, ela pode mudar quando alguém mover o código. Os type guards irão garantir que todas as verificações de tipo sejam explícitas.

 

5 - Usar as any em testes

Como o código pode estar

Pode ocorrer de criarmos uma estrutura incompleta para os testes de nosso código:

interface User {
    id: string
    firstName: string
    lastName: string
    email: string
}
test('createEmailText returns text that greats the user by first name', () => {
    const user: User = {
        firstName: 'John'
    } as any

    expect(createEmailText(user)).toContain(user.firstName)
}

Como o código deveria estar

Se for necessário simular dados para os nossos testes, é melhor mover a lógica da simulação para perto da estrutura que queremos simular e torná-la reutilizável:

interface User {
    id: string
    firstName: string
    lastName: string
    email: string
}
class MockUser implements User {
    id = 'id'
    firstName = 'John'
    lastName = 'Doe'
    email = '[email protected]'
}
test('createEmailText returns text that greats the user by first name', () => {
    const user = new MockUser()
    expect(createEmailText(user)).toContain(user.firstName)
}

Por que temos esse hábito?

Quando escrevemos testes em uma base de código que ainda não possui uma grande cobertura de testes, geralmente encontramos grandes estruturas de dados, mas apenas partes delas são necessárias para a funcionalidade específica que está sendo testada. Assim, não precisamos nos preocupar com as outras propriedades no curto prazo.

Por que deveríamos evitar esse hábito?

Abandonar a criação de uma simulação de dados vai nos afetar no futuro, quando uma das propriedades de nossa estrutura mudar e precisarmos realizar uma alteração em todos os testes, em vez de em um único lugar. Isso pode ser um incômodo. Além disso, haverá situações em que o código testado dependerá de propriedades que não consideramos importantes antes e, por causa disso, será necessário alterar todos os testes desta funcionalidade.

 

6 - Propriedades opcionais

Como o código pode estar

Pode ser que criemos propriedades como opcionais que existem em alguns casos, e em outros não:

interface Product {
    id: string
    type: 'digital' | 'physical'
    weightInKg?: number
    sizeInMb?: number
}

Como o código deveria estar

Devemos definir explicitamente as combinações que existem e as que não existem:

interface Product {
    id: string
    type: 'digital' | 'physical'
}
interface DigitalProduct extends Product {
    type: 'digital'
    sizeInMb: number
}
interface PhysicalProduct extends Product {
    type: 'physical'
    weightInKg: number
}

Por que temos esse hábito?

Definir propriedades como opcionais em vez de separar os tipos em mais interfaces é algo mais fácil e também resulta em menos código. Isso também exige um conhecimento mais profundo do produto que está sendo feito e pode limitar o uso do código se as suposições sobre o produto mudarem.

Por que deveríamos evitar esse hábito?

O grande benefício dos sistemas de tipos é que eles podem substituir as verificações em tempo de execução pelas verificações em tempo de compilação. Ao definir tipos de maneira mais explícita, é possível acessar verificações em tempo de compilação para bugs que de outra forma poderiam ser despercebidos. Por exemplo, garantir que toda interface DigitalProduct possui uma propriedade sizeInMb.

 

7 - Nomear generics com apenas uma letra

Como o código pode estar

Talvez tenhamos o hábito de criar generics usando apenas uma letra:

function head<T> (arr: T[]): T | undefined {
  return arr[0]
}

Como o código deveria estar

É melhor definir um nome que descreva por completo o contexto do generic:

function head<Element> (arr: Element[]): Element | undefined {
  return arr[0]
}

Por que temos esse hábito?

Esse hábito cresceu pois até mesmo a documentação oficial do TypeScript usa uma letra para criar generics. Além disso, é mais rápido e não precisamos raciocinar tanto para apenas pressionar a letra T do que escrever um nome completo.

Por que deveríamos evitar esse hábito?

Variáveis de tipo em generics são variáveis como qualquer outra. Nós abandonamos a ideia de descrever os detalhes técnicos das variáveis em seus nomes quando as IDEs começaram a nos mostrar esses detalhes. Por exemplo, em vez de escrever const strName = 'Daniel' nós apenas escrevemos const name = 'Daniel'. Além disso, variáveis criadas com apenas uma letra são desincentivadas, pois pode ser difícil entender o objetivo delas sem olhar sua declaração.

 

8 - Realizar a verificação de booleanos não-booleanos

Como o código pode estar

Talvez tenhamos o costume de verificar se uma variável é definida apenas informando seu valor em uma declaração if():

function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages) {
        return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
}

Como o código deveria estar

Devemos explicitamente informar a condição que estamos esperando:

function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages !== undefined) {
        return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
}

Por que temos esse hábito?

Escrever a condição que desejamos realizar de maneira resumida é mais sucinto e nos ajuda a evitar ficar pensando sobre o que realmente desejamos verificar.

Por que deveríamos evitar esse hábito?

Talvez devamos pensar sobre o que realmente queremos verificar. Os exemplos acima por exemplo lidam com o caso de a variável countOfNewMessages seja 0 de maneira diferente.

 

9 - O operador “Bang Bang”

Como o código pode estar

Pode ser que tentemos converter um valor não-booleano para booleano:

function createNewMessagesResponse (countOfNewMessages?: number) {
    if (!!countOfNewMessages) {
        return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
}

Como o código deveria estar

Devemos explicitamente informar a condição que estamos esperando:

function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages !== undefined) {
        return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
}

Por que temos esse hábito?

Na opinião de algumas pessoas, só podemos dizer que estamos realmente iniciando com JavaScript quando conseguimos entender o operador !!. Ele é muito curto e sucinto, e se você já o usou algumas vez ou está acostumado a usá-lo, sabe por experiência própria que ele pode ser muito útil. Ele é um atalho que nos permite converter qualquer valor para booleano. Isso pode ser útil especialmente em códigos onde não há uma separação clara entre os valores que podem ser considerados falsos, como null, undefined e “”.

Por que deveríamos evitar esse hábito?

Assim como muitos outros atalhos, usar !! ofusca o verdadeiro objetivo do código, o que obriga um maior conhecimento interno para entendê-lo. Isso faz com que a base de código se torne menos acessível para novos programadores, sejam eles novos na programação como um todo, ou apenas novos na linguagem JavaScript. Além disso, esse hábito pode sutilmente inserir bugs em nosso código. O problema com a variável countOfNewMessages sendo 0 que mostramos no último tópico continua com o !!.

 

10 - != null

Como o código pode estar

A estrutura != null, a irmã mais nova do Operador “Bang Bang”, nos permite verificar por valores null e undefined ao mesmo tempo:

function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages != null) {
        return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
}

Como o código deveria estar

Devemos explicitamente informar a condição que estamos esperando:

function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages !== undefined) {
        return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
}

Por que temos esse hábito?

Se você chegou até aqui, sua base de código e habilidade de desenvolvedor com certeza já estão em um bom formato. Até mesmo a maioria das ferramentas de linting que reforçam o uso do operador !== em vez de != fazem uma exceção para a estrutura != null. Se não há uma distinção clara no código sobre a diferença de null e undefined, usar != null nos ajuda a verificar as duas possibilidades de uma maneira mais curta.

Por que deveríamos evitar esse hábito?

Embora os valores nulos tenham sido um incômodo no início do JavaScript, com o uso do TypeScript no modo estrito, esses valores podem se tornar um membro valioso do cinto de utilidades da linguagem. Um padrão que tenho visto é definir valores nulos como coisas que não existem, e undefined como valores desconhecidos. Por exemplo, user.firstName === null pode significar que o usuário literalmente não possui um primeiro nome, enquanto user.firstName === undefined significa apenas que não perguntamos isso para o usuário ainda (e user.firstName === “” poderia significar que o primeiro nome é literalmente uma string vazia).

Conclusão

Neste artigo vimos algumas práticas que podemos ter desenvolvido com o tempo e que podem ser ruins. Quando entendemos em que situações elas podem nos prejudicar sutilmente, fica mais fácil aceitar a mudança que precisamos fazer.

Sabemos que programar é um constante aprimoramento de nossas técnicas e ao dar atenção a elas nos tornamos programadores cada vez melhores. E se você deseja aprender ou aprimorar seu TypeScript, não deixe de se inscrever em nosso curso Dominando TypeScript, onde ensinamos todos os conceitos desse incrível superset ?.

Até o próximo artigo!

Hcode: Utilizamos cookies para a personalização de anúncios e experiências de navegação dentro de nosso site. Ao continuar navegando, você concorda com as nossas Política de Privacidade.