12 Dicas de TypeScript para Código Limpo

Marcos Vinicius Gouvea
11 min readApr 17, 2023

--

this article also has an english version: 12 Typescript tricks for Clean Code

Vamos explorar doze dicas de TypeScript para escrever um código limpo, com exemplos que demonstram como eles funcionam e por que são úteis. Ao usar essas dicas em seu próprio código TypeScript, você pode criar aplicações mais robustas e fáceis de manter, tornando-as mais simples de compreender e debugar.

typescript

1 — Use Anotações de Tipo: TypeScript é uma linguagem de tipagem estática, o que significa que permite definir tipos para variáveis e funções. Usar anotações de tipo pode ajudar a capturar erros precocemente no processo de desenvolvimento e melhorar a legibilidade do código.

Aqui estão alguns exemplos de anotações de tipo em TypeScript:

// Especificar explicitamente o tipo de dado de uma variável
let count: number = 0;

// Especificar explicitamente o tipo de dado de um parâmetro de função e valor de retorno
function addNumbers(a: number, b: number): number {
return a + b;
}

// Especificar explicitamente o tipo de dado de uma propriedade de classe
class Person {
name: string;
age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

getDetails(): string {
return ${this.name} tem ${this.age} anos de idade.;
}
}

Nesses exemplos, usamos anotações de tipo para especificar o tipo de dado de uma variável, parâmetro de função, valor de retorno de função e propriedade de classe. As anotações de tipo são escritas após o nome da variável, parâmetro ou propriedade, separadas por dois pontos (:), seguido do tipo de dado desejado.

2 — Use Enums: Enums são um recurso poderoso do TypeScript que permite definir um conjunto de constantes nomeadas. Eles podem ajudar a tornar seu código mais legível e fácil de manter, além de reduzir a probabilidade de erros causados por números mágicos.

Aqui está um exemplo de como os enums podem ser usados no TypeScript:

enum Cor {
Vermelho = "VERMELHO",
Verde = "VERDE",
Azul = "AZUL"
}

function imprimirCor(cor: Cor): void {
console.log(A cor é ${cor});
}

imprimirCor(Cor.Vermelho); // saída: A cor é VERMELHO

Neste exemplo, definimos um enum chamado Cor que contém três constantes nomeadas: Vermelho, Verde, e Azul. Cada constante possui um valor associado, que pode ser uma string ou um número. Em seguida, definimos uma função chamada imprimirCor que recebe um parâmetro do tipo Cor e registra uma mensagem no console usando o valor do parâmetro.

Quando chamamos a função imprimirCor com a constante Cor.Vermelho como parâmetro, ela registra a mensagem “A cor é VERMELHO” no console.

3 — Use Encadeamento Opcional: O encadeamento opcional é um recurso do TypeScript que permite acessar com segurança propriedades e métodos aninhados sem se preocupar se os valores intermediários são nulos ou indefinidos. Isso pode ajudar a reduzir a probabilidade de erros de tempo de execução e tornar seu código mais robusto.

Aqui está um exemplo de como o encadeamento opcional pode ser usado no TypeScript:

interface Pessoa {
nome: string;
endereco?: {
rua: string;
cidade: string;
estado: string;
};
}

const pessoa1: Pessoa = {
nome: "João",
endereco: {
rua: "123 Rua Principal",
cidade: "Qualquer Cidade",
estado: "SP",
},
};

const pessoa2: Pessoa = {
nome: "Joana",
};

console.log(pessoa1?.endereco?.cidade); // saída: Qualquer Cidade
console.log(pessoa2?.endereco?.cidade); // saída: undefined

Neste exemplo, temos uma interface chamada Pessoaque define uma propriedade opcional endereco, que é um objeto com as propriedades rua, cidade e estado. Em seguida, criamos dois objetos do tipo Pessoa, um com uma propriedade enderecoe outro sem.

Usamos o encadeamento opcional para acessar com segurança a propriedade cidadedo objeto endereco, mesmo que a propriedade enderecoou qualquer uma de suas subpropriedades seja indefinida ou nula. Se qualquer uma das propriedades na cadeia for indefinida ou nula, a expressão retornará undefined em vez de lançar um TypeError.

4 — Use Coalescência Nula: A coalescência nula é outro recurso do TypeScript que pode ajudar a tornar seu código mais robusto. Ele permite fornecer um valor padrão para uma variável ou expressão quando ela é nula ou indefinida, sem depender de valores falsos.

Aqui está um exemplo de como a coalescência nula pode ser usada no TypeScript:

let valor1: string | null = null;
let valor2: string | undefined = undefined;
let valor3: string | null | undefined = "olá";

console.log(valor1 ?? "valor padrão"); // saída: "valor padrão"
console.log(valor2 ?? "valor padrão"); // saída: "valor padrão"
console.log(valor3 ?? "valor padrão"); // saída: "olá"

Neste exemplo, temos três variáveis que podem conter valores nulos ou indefinidos. Usamos o operador de coalescência nula (??) para verificar se os valores são nulos (seja nulo ou indefinido) e fornecer um valor padrão nesse caso.

Nos dois primeiros casos, as variáveis valor1 e valor2são nulas ou indefinidas, respectivamente, portanto o valor padrão é retornado. No terceiro caso, a variável valor3 contém um valor não nulo/não indefinido, então esse valor é retornado em vez do valor padrão.

5 — Use Genéricos: Genéricos são um recurso poderoso do TypeScript que permite escrever código reutilizável que funciona com diferentes tipos. Eles podem ajudar a reduzir a duplicação de código e melhorar a manutenção do código.

Aqui está um exemplo de como os genéricos podem ser usados no TypeScript:

function identidade<T>(arg: T): T {
return arg;
}

let output1 = identidade<string>("olá"); // saída: "olá"
let output2 = identidade<number>(42); // saída: 42

Neste exemplo, definimos uma função chamada identidade que recebe um parâmetro de tipo T e retorna o mesmo tipo de valor que foi passado. A função pode funcionar com qualquer tipo de dado, e o tipo de dado real é especificado quando a função é chamada.

Em seguida, chamamos a função identidade com dois tipos de dados diferentes: uma string e um número. A função retorna o mesmo tipo de valor que foi passado, então output1 é do tipo string e output2 é do tipo number.

6 — Use Interfaces: Interfaces são outro recurso poderoso do TypeScript que pode ajudá-lo a escrever um código limpo e legível. Elas permitem definir um contrato para uma classe, objeto ou função, o que pode ajudar a evitar erros comuns e tornar seu código mais auto-documentado.

Aqui está um exemplo de como as interfaces podem ser usadas no TypeScript:

interface Pessoa {
primeiroNome: string;
sobrenome: string;
idade?: number;
}

function dizerOla(pessoa: Pessoa): void {
console.log(Olá, ${pessoa.primeiroNome} ${pessoa.sobrenome}!);
if (pessoa.idade) {
console.log(Você tem ${pessoa.idade} anos.);
}
}

let pessoa1 = { primeiroNome: "João", sobrenome: "Silva", idade: 30 };
let pessoa2 = { primeiroNome: "Maria", sobrenome: "Santos" };

dizerOla(pessoa1); // saída: "Olá, João Silva! Você tem 30 anos."
dizerOla(pessoa2); // saída: "Olá, Maria Santos!"

Neste exemplo, definimos uma interface chamada Pessoaque especifica a forma de um objeto pessoa, incluindo uma propriedade primeiroNome e sobrenome e uma propriedade idade opcional. Em seguida, definimos uma função chamada dizerOla que recebe um objeto Pessoacomo argumento e imprime uma saudação no console.

Criamos dois objetos que correspondem à forma da interface Pessoae os passamos para a função dizerOla. A função é capaz de acessar as propriedades primeiroNomee sobrenomede cada objeto, e também verifica se a propriedade idadeexiste antes de imprimi-la no console.

7 — Use Destruturação: A destruturação é uma sintaxe abreviada que permite extrair valores de arrays e objetos. Ela pode ajudar a tornar seu código mais legível e conciso, além de reduzir a probabilidade de erros causados pelo desalinhamento de nomes de variáveis.

Aqui estão alguns exemplos de como a destruturação pode ser usada no TypeScript:

Destruturação de objeto:

let pessoa = { primeiroNome: "João", sobrenome: "Silva", idade: 30 };

let { primeiroNome, sobrenome } = pessoa;

console.log(primeiroNome); // saída: "João"
console.log(sobrenome); // saída: "Silva"

Neste exemplo, criamos um objeto chamado pessoacom três propriedades. Em seguida, usamos a destruturação de objeto para extrair as propriedades primeiroNomee sobrenome e atribuí-las a variáveis de mesmo nome. Isso nos permite acessar essas propriedades de forma mais fácil e com menos código.

Destruturação de array:

let numeros = [1, 2, 3, 4, 5];

let [primeiro, segundo, , quarto] = numeros;

console.log(primeiro); // saída: 1
console.log(segundo); // saída: 2
console.log(quarto); // saída: 4

Neste exemplo, criamos um array de números e usamos a destruturação de array para extrair os elementos primeiro, segundoe quartoe atribuí-los a variáveis. Pulamos o terceiroelemento usando um espaço vazio no padrão de destruturação. Isso nos permite acessar elementos específicos de um array de forma mais fácil e com menos código.

A destruturação também pode ser usada com parâmetros de função, permitindo extrair valores específicos de um objeto passado como argumento:

function cumprimentar({ primeiroNome, sobrenome }: { primeiroNome: string, sobrenome: string }): void {
console.log(Olá, ${primeiroNome} ${sobrenome}!);
}

let pessoa = { primeiroNome: "João", sobrenome: "Silva", idade: 30 };

cumprimentar(pessoa); // saída: "Olá, João Silva!"

Neste exemplo, definimos uma função chamada cumprimentarque recebe um objeto com propriedades primeiroNomee sobrenome como argumento usando a sintaxe de destruturação no parâmetro da função. Em seguida, passamos um objeto pessoae a função cumprimentaré capaz de extrair as propriedades primeiroNomee sobrenome e usá-las na instrução de registro no console.

8 — Use Async/Await: Async/await é um recurso poderoso do TypeScript que permite escrever código assíncrono que se parece e se comporta como código síncrono. Ele pode ajudar a melhorar a legibilidade do código e reduzir a probabilidade de erros causados pelo inferno de callbacks.

Aqui está um exemplo de como async/await pode ser usado no TypeScript:

async function getDados() {
const response = await fetch('https://api.exemplo.com/dados');
const dados = await response.json();
return dados;
}

getDados().then((dados) => {
console.log(dados);
}).catch((error) => {
console.error(error);
});

Neste exemplo, definimos uma função assíncrona chamada getDadosque faz uma requisição fetch a uma API e aguarda a resposta usando a palavra-chave await. Em seguida, analisamos a resposta usando o método json() e novamente aguardamos o resultado usando await. Finalmente, retornamos o objeto dados.

Depois, chamamos a função getDados() e usamos o método then() para lidar com os dados retornados ou o método catch() para lidar com quaisquer erros que possam ocorrer.

9 — Use Técnicas de Programação Funcional: Técnicas de programação funcional, como imutabilidade, funções puras e funções de ordem superior, podem ajudá-lo a escrever código limpo e fácil de manter. Elas podem ajudar a reduzir efeitos colaterais e tornar seu código mais previsível e testável.

Funções puras: Uma função pura é uma função que não tem efeitos colaterais e sempre retorna a mesma saída para a mesma entrada. Funções puras facilitam o raciocínio sobre o código e podem ajudar a prevenir erros. Aqui está um exemplo de uma função pura:

function soma(a: number, b: number): number {
return a + b;
}

Funções de ordem superior: Uma função de ordem superior é uma função que recebe uma ou mais funções como argumentos ou retorna uma função como resultado. Funções de ordem superior podem ser usadas para criar código reutilizável e simplificar a lógica complexa. Aqui está um exemplo de uma função de ordem superior:

function map<T, U>(arr: T[], fn: (arg: T) => U): U[] {
const result = [];
for (const item of arr) {
result.push(fn(item));
}
return result;
}

const numeros = [1, 2, 3, 4, 5];
const duplicarNumeros = map(numeros, (n) => n * 2);
console.log(duplicarNumeros); // Saída: [2, 4, 6, 8, 10]

Neste exemplo, a função map recebe um array e uma função como argumentos e aplica a função a cada elemento do array, retornando um novo array com os resultados.

Dados imutáveis: Dados imutáveis são dados que não podem ser alterados depois de criados. Na programação funcional, a imutabilidade é enfatizada para evitar efeitos colaterais e tornar o código mais fácil de entender. Aqui está um exemplo de uso de dados imutáveis:

const numeros = [1, 2, 3, 4, 5];
const novosNumeros = [...numeros, 6];
console.log(numeros); // Saída: [1, 2, 3, 4, 5]
console.log(novosNumeros); // Saída: [1, 2, 3, 4, 5, 6]

Neste exemplo, usamos o operador spread (...)para criar um novo array com um novo elemento adicionado ao final, sem modificar o array original.

10 — Use o Auxiliar Pick: O auxiliar Pick é um tipo utilitário do TypeScript que permite criar novos tipos a partir de tipos existentes, tornando mais fácil reutilizar e manter o código. Também ajuda a prevenir erros, garantindo que o novo tipo inclua apenas as propriedades que pretendemos usar.

Aqui está um exemplo:

interface Usuario {
nome: string;
email: string;
idade: number;
isAdmin: boolean;
}

type UsuarioResumo = Pick<Usuario, 'nome' | 'email'>;

const usuario: Usuario = {
nome: 'Joao Silva',
email: 'joaosilva@exemplo.com',
idade: 30,
isAdmin: false,
};

const resumo: UsuarioResumo = {
nome: usuario.nome,
email: usuario.email,
};

console.log(resumo); // Saída: { name: 'Joao Silva', email: 'joaosilva@exemplo.com' }

Neste exemplo, definimos uma interface Usuario com várias propriedades. Em seguida, definimos um novo tipo UsuarioResumo usando o tipo utilitário Pick, que seleciona apenas as propriedades nomee email da interface Usuario.

Depois, criamos um objeto usuario com todas as propriedades da interface Usuario e usamos as propriedades nome e email para criar um novo objeto resumo do tipo UsuarioResumo.

11 — Use o Auxiliar Omit: O auxiliar Omit é um tipo utilitário do TypeScript que permite criar novos tipos a partir de tipos existentes, garantindo que certas propriedades sejam excluídas. Isso pode ser útil ao trabalhar com interfaces complexas em que certas propriedades podem não ser necessárias em determinadas situações. Também pode ajudar a prevenir erros, garantindo que certas propriedades não sejam incluídas acidentalmente.

Aqui está um exemplo:

interface Usuario {
nome: string;
email: string;
idade: number;
isAdmin: boolean;
}

type UsuarioSemEmail = Omit<Usuario, 'email'>;

const usuario: Usuario = {
nome: 'Joao Silva',
email: 'joaosilva@exemplo.com',
idade: 30,
isAdmin: false,
};

const usuarioSemEmail: UsuarioSemEmail = {
nome: usuario.nome,
idade: usuario.idade,
isAdmin: usuario.isAdmin,
};

console.log(usuarioSemEmail); // Saída: { nome: 'Joao Silva', idade: 30, isAdmin: false }

Neste exemplo, definimos uma interface Usuario com várias propriedades. Em seguida, definimos um novo tipo UsuarioSemEmail usando o tipo utilitário Omit, que omite a propriedade email da interface Usuario.

Depois, criamos um objeto user com todas as propriedades da interface Usuario e usamos as propriedades nome, idade e isAdmin para criar um novo objeto usuarioSemEmail do tipo UsuarioSemEmail.

12 — Use Uniões Discriminadas: Uniões discriminadas são um recurso do TypeScript que nos permite modelar tipos que podem assumir diferentes formas com base em uma propriedade específica ou combinação de propriedades, e tratá-los de maneira segura em relação ao tipo usando instruções switch. É um recurso poderoso do TypeScript que pode tornar seu código mais expressivo e fácil de manter.

Aqui está um exemplo:

interface Quadrado {
tipo: 'quadrado';
tamanho: number;
}

interface Circulo {
tipo: 'circulo';
raio: number;
}

interface Triangulo {
tipo: 'triangulo';
base: number;
altura: number;
}

type Forma = Quadrado | Circulo | Triangulo;

function area(forma: Forma) {
switch (forma.tipo) {
case 'quadrado':
return forma.tamanho * forma.tamanho;
case 'circulo':
return Math.PI * forma.raio * forma.raio;
case 'triangulo':
return 0.5 * forma.base * forma.altura;
}
}

const quadrado: Quadrado = { tipo: 'quadrado', tamanho: 10 };
const circulo: Circulo = { tipo: 'circulo', raio: 5 };
const triangulo: Triangulo = { tipo: 'triangulo', base: 10, altura: 8 };

console.log(area(quadrado)); // Saída: 100
console.log(area(circulo)); // Saída: 78.53981633974483
console.log(area(triangulo)); // Saída: 40

Neste exemplo, definimos três interfaces Quadrado, Circulo e Triangulo, cada uma representando uma forma diferente. Em seguida, definimos um tipo de união Forma que pode ser um Quadrado, um Circulo ou um Triangulo.

Definimos uma função area que recebe um objeto do tipo Forma como argumento e usa uma instrução switch para calcular a área da forma com base no seu tipo (tipo). A propriedade tipo é usada como propriedade discriminante, pois identifica de maneira única cada tipo de forma.

Em seguida, criamos três objetos, um para cada tipo de forma, e chamamos a função area com cada objeto como argumento para calcular a área.

Em conclusão, essas dicas do TypeScript para escrever código limpo podem ajudá-lo a escrever um código mais expressivo, fácil de manter e livre de erros. Ao usar anotações de tipo, enums, encadeamento opcional, coalescência nula, genéricos, interfaces, desestruturação, async/await, técnicas de programação funcional e vários tipos auxiliares como Pick, Omit e uniões discriminadas, você pode criar aplicações TypeScript mais robustas e escaláveis.

Essas dicas também podem ajudá-lo a detectar erros precocemente, melhorar a legibilidade do seu código e reduzir a quantidade de código que você precisa escrever. Com o sistema de tipos forte do TypeScript e essas dicas, você pode escrever código que é mais fácil de entender e manter ao longo do tempo.

Se você gostou deste artigo e o achou útil, sinta-se à vontade para conferir meu outro artigo sobre 6 — Advanced Typescript Trics for Clean Code. Por enquanto esta na versão em ingles mas em breve terá em pt-Br.

--

--