O que fazer e o que não fazer com TypeScript

Este artigo apresenta algumas sugestões básicas sobre o que fazer e o que não fazer com TypeScript.

Lembre-se de que nada aqui descrito é “bala de prata”, tampouco imutável. A melhor maneira de escrever códigos é aquela que foi definida pela equipe do seu projeto e que pode fazer com que, futuramente, outros desenvolvedores do seu projeto (ou até mesmo você) tenham a capacidade de entender e ainda assim julgá-lo como ‘bom’.

O “bom” é subjetivo, mas não vamos falar de boas práticas, né? Então, tenha em mente que toda “regra” abaixo listada será apenas sugestão para um melhor entendimento geral.

Tipos genéricos

Por tipos genéricos entenda Number, String, Boolean e Object.

Evite o uso desses tipos. Eles se referem a tipos de objetos não primitivos e quase nunca são usados apropriadamente no JavaScript.

/* Não recomendado */
function upper(s: String): String;

Use os tipos number, string e boolean.

/* Recomendado */
function upper(s: string): string;

Em vez de Object, use o tipo não primitivo object (adicionado no TypeScript 2.2).

Genéricos

Evite ao máximo o uso de tipos genéricos. Procure criar tipos específicos sempre que possível.

Tipos de retorno

Tipos de retorno de chamadas

Não use o tipo de retorno any para callbacks cujo valor será ignorado:

/* Não recomendado */
function fn(x: () => any) {
  x();
}

Use o tipo de retorno void para callbacks cujo valor será ignorado:

/* Sugerido */
function fn(x: () => void) {
  x();
}

Usar o void é mais seguro porque impede que você use acidentalmente o valor de retorno de x de uma maneira não verificada:

function fn(x: () => void) {
  var k = x(); // oops! meant to do something else
  k.doSomething(); // erro, mas seria **OK** se o tipo de retorno tivesse sido `any`
}

Parâmetros opcionais em retornos de chamada

Não use parâmetros opcionais em retornos de chamada, a menos que você queira/precise fazer isso realmente:

/* Não recomendado */
interface Colecionador {
  getObject(concluido: (data: any, tempoGasto?: number) => void): void;
}

Isso tem um significado muito específico: o retorno da chamada concluido pode ser chamado com um argumento ou pode ser chamado com dois argumentos. O autor provavelmente pretendia dizer que o retorno de chamada pode não se importar com o parâmetro tempoGasto, mas não há necessidade de tornar o parâmetro opcional para isso – é sempre legal fornecer um retorno de chamada que aceite menos argumentos.

Se escrevermos parâmetros de retorno de chamada como não opcionais:

/* Sugerido */
interface Colecionador {
    getObject(concluido: (data: any, tempoGasto: number) => void): void;
}

Sobrecargas e retornos de chamada

Não escreva sobrecargas separadas que diferem apenas no número de argumentos de retorno de chamada:

/* Não recomendado */
declare function beforeAll(action: () => void, timeout?: number): void;
declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;

Procure escrever uma única sobrecarga usando o maior número de argumentos necessários:

/* Sugerido */
declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;

É aceitável, para um retorno de chamada, desconsiderar um parâmetro. Portanto, não há necessidade de uma sobrecarga menor.

Sobrecargas de função

Ordem

Evite colocar as sobrecargas genéricas antes de sobrecargas mais específicas:

/* Não recomendado */
declare function fn(x: any): any;
declare function fn(x: HTMLElement): number;
declare function fn(x: HTMLDivElement): string;

var myElem: HTMLDivElement;
var x = fn(myElem); // x: any, wat?

Classifique sobrecargas colocando as assinaturas mais genéricas depois de assinaturas mais específicas:

/* Sugerido */
declare function fn(x: HTMLDivElement): string;
declare function fn(x: HTMLElement): number;
declare function fn(x: any): any;

var myElem: HTMLDivElement;
var x = fn(myElem); // x: string, :)

O TypeScript escolhe a primeira sobrecarga correspondente ao resolver chamadas de função. Quando uma sobrecarga anterior é “mais geral” que uma posterior, a última é efetivamente ocultada e não será chamada.

Use Parâmetros opcionais

Não escreva várias sobrecargas que diferem apenas nos parâmetros finais:

/* Não recomendado */
interface Example {
    diff(one: string): number;
    diff(one: string, two: string): number;
    diff(one: string, two: string, three: boolean): number;
}

Use parâmetros opcionais sempre que possível:

/* Sugerido */
interface Example { diff(one: string, two?: string, three?: boolean): number; }

Observe que apenas fizemos isso. É recomendável que isso ocorra somente quando todas as sobrecargas tiverem o mesmo tipo de retorno.

Isso é importante por dois motivos.

O TypeScript resolve a compatibilidade de assinatura, verificando se qualquer assinatura do destino pode ser invocada com os argumentos da origem (argumentos estranhos são permitidos). O código a seguir, por exemplo, expõe um bug somente quando a assinatura é escrita corretamente usando parâmetros opcionais:

/* Não recomendado */
function fn(x: (a: string, b: number, c: number) => void) { }
var x: Example;
// When written with overloads, OK -- used first overload
// When written with optionals, correctly an error
fn(x.diff);

A segunda razão é por conta do recurso “strict null checking/verificação de nulos” do TypeScript. Como os parâmetros não especificados aparecem como undefined em JavaScript, é melhor passar um undefined explícito para uma função com argumentos opcionais. Esse código, por exemplo, deve estar OK em relação a “under strict nulls”:*

/* Sugerido */
var x: Example;
// When written with overloads, incorrectly an error because of passing 'undefined' to 'string'
// When written with optionals, correctly OK
x.diff("something", true ? undefined : "hour");

Use união de tipos

Procure não escrever sobrecargas que diferem por tipo em apenas uma posição de argumento:

/* Não recomendado */
interface Moment {
    utcOffset(): number;
    utcOffset(b: number): Moment;
    utcOffset(b: string): Moment;
}

Use união de tipos sempre que possível:

/* Sugerido */
interface Moment {
  utcOffset(): number;
  utcOffset(b: number|string): Moment;
}

Note que nós não fizemos b opcional nesta abordagem porque os tipos de retorno das assinaturas diferem.

É importante ficar atento aos casos onde estamos expondo valores de parâmetros para funções internas:

/* Sugerido */
function fn(x: string): void;
function fn(x: number): void;
function fn(x: number|string) {
    // When written with separate overloads, incorrectly an error
    // When written with union types, correctly OK
    return moment().utcOffset(x);
}

Conclusão

Este foi um breve artigo com algumas sugestões do que fazer e não fazer com TypeScript, mas lembre-se de que cada caso deve ser avaliado miniciosamente para que seja feito o melhor para o projeto em um contexto geral.

Se você ficou curioso e quiser conhecer mais a respeito das sugestões de boas práticas com TypeScript, mantenha-se atento aos seguintes tópicos:


BrazilJS é uma iniciativa NASC