20 flags de TypeScript que você deveria estar usando

Faça isso se você usa TypeScript!

LinkIntrodução

Mudar do JavaScript para TypeScript já é uma enorme melhoria em qualquer base de código, mas podemos fazer melhor. Podemos ir além das verificações padrão do TypeScript e tornar nossa base de códigos muito mais segura.

Para isso, precisaremos definir algumas opções para o compilador do TypeScript. Neste artigo, vou mostrar 20 opções do compilador do TypeScript que eu e minha equipe usamos e recomendamos aos nossos clientes. Em sequência, revisaremos cada uma dessas flags para entender o que elas fazem e por que as recomendamos.

Eu sou Lucas Paganini, e neste site, lançamos tutoriais de desenvolvimento web. Inscreva-se na nossa newsletter se você estiver interessado.

LinkLidando com os erros

Eu vou mostrar as flags em um segundo, mas primeiro, um aviso. Às vezes, apenas saber o que fazer não é suficiente. Por exemplo, se você estiver trabalhando em uma grande base de código, habilitar essas flags provavelmente causará muitos erros de compilação.

Minha equipe e eu já passamos por isso. A maioria dos nossos clientes nos contratam para trabalhar em projetos Angular(que usam o TypeScript). Portanto, confie em mim quando digo que entendemos o desafio de lidar com a dívida tecnológica em grandes bases de código legadas.

Portanto, além deste artigo, também estamos trabalhando em outro que mostrará nossas técnicas para lidar com os erros de compilação que surgem da habilitação de cada uma dessas flags em uma base de códigos legada.

LinkNossas recomendações de opção de compilador

Sem mais delongas, estas são as 20 opções de compilação TypeScript que recomendamos:

TypeScript
{
  "compilerOptions": {

    // Strict mode (9)
    "strict": true,
    "alwaysStrict": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,
    "strictBindCallApply": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "useUnknownInCatchVariables": true,

    // No unused code (4)
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "allowUnusedLabels": false,
    "allowUnreachableCode": false,

    // No implicit code (2)
    "noImplicitOverride": true,
    "noImplicitReturns": true,

    // Others (5)
    "noUncheckedIndexedAccess": true,
    "noPropertyAccessFromIndexSignature": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": true,
    "forceConsistentCasingInFileNames": true

  }
}

Óbvio, um tsconfig.json de verdade também teria outras flags, tais como target, outDir, sourceMap, etc. Mas só estamos interessados nas flags de type checking. Nós as agrupamos em 4 categorias:

  1. Strict mode
  2. No unused code
  3. No implicit code
  4. Others

LinkFicando atualizado

Cuidado, à medida que o TypeScript evolui, novas flags serão criadas. Talvez haja uma nova flag que não existia quando escrevemos este artigo, portanto, junto com esta lista, também recomendamos que você dê uma olhada na documentação TypeScript para ver se eles têm alguma outra flag que possa lhe interessar.

👉 Outra forma de manter-se atualizado é inscrever-se nossa newsletter. Apenas avisando...

LinkStrict Mode Flags

Vamos começar pelas flags de strict mode. Como você pode ver, existem 9 flags na categoria de strict mode:

  1. strict
  2. alwaysStrict
  3. noImplicitAny
  4. noImplicitThis
  5. strictNullChecks
  6. strictBindCallApply
  7. strictFunctionTypes
  8. strictPropertyInitialization
  9. useUnknownInCatchVariables

strict

Descrição

A primeira, strict, na verdade é apenas um pseudônimo. Definir strict como true é o mesmo que definir todas as outras 8 flags de modo strict como verdadeiras. É apenas um atalho.

TypeScript
{
  "compilerOptions": {

    "strict": true,
  }
}
TypeScript
{
  "compilerOptions": {

    "alwaysStrict": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,
    "strictBindCallApply": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "useUnknownInCatchVariables": true,
  }
}

Motivação

Mas isso não quer dizer que ela não tenha valor. Como eu já disse antes, o TypeScript está evoluindo. Se futuras versões do TypeScript incluírem novas flags para a categoria strict, elas serão habilitadas por padrão devido a este pseudônimo.

TypeScript
{
  "compilerOptions": {

    "alwaysStrict": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,
    "strictBindCallApply": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "useUnknownInCatchVariables": true,

    "someFlagFromTheFuture": true,
    "someOtherFlagFromTheFuture": true,
    "yetAnotherFlagFromTheFuture": true,
  }
}

alwaysStrict

Descrição

O JavaScript também tem um strict mode, foi introduzido no ES5, há muito tempo. Para ter um arquivo JavaScript em strict mode, basta escrever "use strict" no topo de seu arquivo `js', antes de qualquer outra declaração.

JavaScript
'use strict';
var str = 'Hello World';

Ativando alwaysStrict no compilador do TypeScript garante que seus arquivos sejam analisados em strict mode no JavaScript e que os arquivos transpilados tenham "use strict" na parte superior.

Motivação

O strict mode do JavaScript é sobre transformar de falhas em erros. Por exemplo, se você digitar incorretamente uma variável, você espera que um erro aconteça. Mas se você estiver executando o JavaScript em "sloopy mode", isso não vai lançar um erro. Em vez disso, ele irá definir uma variável global.

JavaScript
'use strict';
mistypeVariable = 17;
//=> ⚠️ RUNTIME ERROR: mistypeVariable is not defined
JavaScript
// 'use strict';
mistypeVariable = 17;
//=> ✅ OK, definindo window.mistypeVariable como 17

O benefício de habilitar a alwaysStrict no TypeScript é que aqueles runtime errors que você obteria no strict mode do JavaScript são transformados em erros de compilação.

TypeScript
mistypeVariable = 17;
//=> ❌ COMPILER ERROR: Cannot find name 'mistypeVariable'

noImplicitAny

Descrição

noImplicitAny é bastante auto-explicativa. Ela não permitirá que seu código seja implicitamente inferido como any. Portanto, se você explicitamente declarar o tipo any, tudo bem. Mas, se o TypeScript declarar implicitamente que algo é any, você receberá um erro de compilação.

TypeScript
function explicit(explicitAny: any) {}
//=> ✅ OK: Estamos definindo explícitamente como 'any'

function implicit(implicitAny) {}
//=> ❌ COMPILER ERROR: Parameter 'implicitAny' implicitly has an 'any' type

Motivação

O benefício aqui é que os problemas de inferência não passarão despercebidos. Vou te dar dois exemplos diferentes onde essa flag te salvaria:

1 - Tipo dos parâmetros

Primeiro, digamos que você declare uma função para dividir uma string por pontos. Sua função deve receber uma string e retornar uma Array<string>. Mas você se esquece de definir o tipo do parâmetro para string, o que acontece agora?

O tipo do parâmetro será definido como any por padrão e o TypeScript nem mesmo o avisará.

TypeScript
// noImplicityAny: false ❌

function splitByDots(value) {
  //=> value: any
  //=> returns: any
  return value.split('.');
}

Aqui está você, pensando que está usando todo o poder do TypeScript, mas sua função não tem nenhuma segurança quanto a checagem de tipo. O TypeScript permitirá que outros desenvolvedores usem sua função com os tipos que eles quiserem. O TypeScript dirá que não há problema em passar um number para sua função, uma Date, ou um avião.

TypeScript
// noImplicityAny: false ❌

function splitByDots(value) {
  //=> value: any
  //=> returns: any
  return value.split(".");
}

splitByDots("www.lucaspaganini.com");
// "Tudo bem, estou satisfeito"

splitByDots(123);
// "Espera, o que é isso??"

splitByDots(new Date());
// "TypeScript, porque você está permitindo isso?"

splitByDots(✈️);
// "Como isso é possível?"

2 - Inferred variable types

Mas não pára por aqui. Vamos ao segundo exemplo. Mesmo que você passe uma string para sua função, ao invés de um avião, você ainda pode ter problemas.

Digamos que você chama sua função e salva o valor retornado em uma variável. Você pensa que sua variável é uma array<string>, mas na verdade é any. Se você tentar chamar Array.forEach e escrever errado, o TypeScript o deixará você atirar no seu pé, porque ele não sabe que sua variável é uma Array.

TypeScript
// noImplicityAny: false ❌

function splitByDots(value) {
  //=> value: any
  //=> returns: any
  return value.split(".");
}

const words = splitByDots("www.lucaspaganini.com");
//=> words: any

words.foreach(...)
//=> Nós digitamos incorretamente Array.forEach e o TypeScript não nos deu um erro 😔

Habilitar noImplicitAny criaria um erro de compilação em sua declaração de função, forçando-o a definir o tipo de seu parâmetro e impedindo que essas inferências passassem despercebidas.

TypeScript
// noImplicityAny: true ✅

function splitByDots(value) {
  //=> ❌ COMPILER ERROR: Parameter 'value' implicitly has an 'any' type
  //=> value: any
  //=> returns: any
  return value.split(".");
}

const words = splitByDots("www.lucaspaganini.com");
//=> words: any

words.foreach(...)
TypeScript
// noImplicityAny: true ✅

function splitByDots(value: string) {
  //=> value: string
  //=> returns: Array<string>
  return value.split(".");
}

const words = splitByDots("www.lucaspaganini.com");
//=> words: Array<string>

words.foreach(...)
//=> ❌ COMPILER ERROR: Property 'foreach' does not exist on type 'Array<string>'. Did you mean 'forEach'?

E se você realmente quiser que seu parâmetro seja do tipo any, você pode declará-lo explicitamente como any.

TypeScript
// noImplicityAny: true ✅

function splitByDots(value: any) {
  //=> value: any
  //=> returns: any
  return value.split('.');
}

noImplicitThis

Descrição

"noImplictThis" é similar, ela irá criar um erro se this é implicitamente any.

Motivação

O valor do this em JavaScript é contextual, portanto, se você não estiver 100% confiante com a forma que o JavaScript define this, você pode escrever uma classe como a seguinte e não notar seu erro:

TypeScript
class User {
  name: string;

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

  getName() {
    return function () {
      return this.name;
    };
  }
}

const user = new User('Tom');

user.getName()();

A questão aqui é que this, em this.name, não se refere à instância da classe. Portanto, chamar user.getName()() irá gerar um runtime error.

TypeScript
class User {
  name: string;

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

  getName() {
    return function () {
      return this.name;
    };
  }
}

const user = new User('Tom');

user.getName()();
//=> ❌ RUNTIME ERROR: Cannot read property 'name' of undefined

Para evitar isso, podemos habilitar noImplictThis, o que geraria um erro de compilação, impedindo que você utilize this se for inferido como any pelo TypeScript.

TypeScript
class User {
  name: string;

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

  getName() {
    return function () {
      return this.name;
      //=> ❌ COMPILER ERROR: 'this' implicitly has type 'any' because it does not have a type annotation.
    };
  }
}

const user = new User('Tom');

user.getName()();

strictNullChecks

Descrição

Por padrão, o TypeScript ignora null e undefined, você pode mudar isso ao habilitar strictNullChecks. Essa é, de longe, a flag mais valiosa de todas. Ela o forçar a lidar com null e undefined.

Motivação

Pense nisso por um segundo. Você está chamando Array.find e esperando receber um elemento da array. Mas e se você não conseguisse encontrar o que estava procurando? Ele retornará undefined. Você está lidando com isso? Ou seu código irá quebrar durante o tempo de execução?

TypeScript
const users = [{ name: 'Bob', age: 13 }];

const user = users.find((u) => u.age < 10);

console.log(user.name);
//=> ❌ COMPILER ERROR: Object is possibly 'undefined'.

Obviamente, devemos sempre lidar com null e undefined. Entendo que você terá milhões de erros quando ativar essa flag, mas se você está apenas iniciando um novo projeto, isto é uma moleza. Definitivamente, você deve tê-la ativada.

strictBindCallApply

Descrição

strictBindCallApply impõe os tipos corretos para função call, bind e apply.

Motivação

Geralmente não utilizo call, bind, ou apply, mas se eu fosse utilizar, gostaria que fosse digitado corretamente. Eu nem sei por que isso é uma opção, eles devem ser digitados corretamente por padrão.

TypeScript
const fn = (x: string) => parseInt(x);

fn.call(undefined, '10');
//=> ✅ OK

fn.call(undefined, false);
//=> ❌ COMPILER ERROR: Argument of type 'boolean' is not assignable to parameter of type 'string'.

strictFunctionTypes

Descrição

strictFunctionTypes faz com que os parâmetros das funções sejam verificados mais corretamente. Eu sei, isso parece estranho, mas é exatamente isso que ela faz.

Por padrão, os parâmetros da função TypeScript são bivariantes. Isso significa que eles são tanto covariantes quanto contravariantes.

Explicando variância de código

Explicar a variância é um tópico por si só, mas basicamente, quando você é capaz de atribuir um tipo mais amplo a um mais específico, isso é contravariação. Quando você é capaz de atribuir um tipo específico a um tipo mais amplo, isso é covariância. Quando você vai para os dois lados, isso é bivariância. Por exemplo, dê uma olhada neste código:

TypeScript
interface User {
  name: string;
}

interface Admin extends User {
  permissions: Array<string>;
}

declare let admin: Admin;
declare let user: User;

Estamos declarando uma interface chamada User que tem um nome, e uma interface chamada Admin que estende a interface User e acrescenta um Array de permissões. Em seguida, declaramos duas variáveis: uma é uma Admin e a outra é uma User.

A partir desse código, vou dar exemplos de covariância e contravariância.

Covariância

Como eu disse, quando você é capaz de atribuir um tipo específico a um tipo mais amplo, isso é covariância. Um exemplo fácil seria atribuir Admin ao User.

TypeScript
interface User {
  name: string;
}

interface Admin extends User {
  permissions: Array<string>;
}

declare let admin: Admin;
declare let user: User;

// Example of Covariance
user = admin; // ✅ OK
admin = user; // ❌ Error

Um Admin é mais específico do que um usuário. Portanto, atribuir um Admin à uma variável que estava esperando um User é um exemplo de covariância.

Contravariância

A contravariância seria o oposto, sendo capaz de definir um tipo mais amplo para um mais específico. As funções são um bom lugar para encontrar contravariâncias.

Por exemplo, vamos definir uma função para obter o nome de um User e outra para obter o nome de um Admin.

TypeScript
interface User {
  name: string;
}

interface Admin extends User {
  permissions: Array<string>;
}

let getAdminName = (admin: Admin) => admin.name;
let getUserName = (user: User) => user.name;

// Example of Contravariance
getAdminName = getUserName; // ✅ OK
getUserName = getAdminName; // ❌ Error (com strictFunctionTypes = ✅true)

getUserName is broader than getAdminName. So, assigning getUserName to getAdminName is an example of contravariance.

Bivariância

Novamente, a bivariância é quando se tem covariância e contravariância.

TypeScript
interface User {
  name: string;
}

interface Admin extends User {
  permissions: Array<string>;
}

let getAdminName = (admin: Admin) => admin.name;
let getUserName = (user: User) => user.name;

// Example of Bivariance
getAdminName = getUserName; // ✅ OK
getUserName = getAdminName; // ✅ OK (com strictFunctionTypes = ❌false)

De volta à nossa flag

Depois dessa super breve explicação sobre variância, vamos voltar à nossa flag strictFunctionTypes.

Como já disse, os parâmetros das funções do TypeScript são bivariantes. Mas isso está errado. Como já vimos antes, na maioria das vezes, os parâmetros das funções devem ser contravariantes, não bivariantes. Não consigo nem imaginar um bom exemplo de bivariância nos parâmetros de funções.

Portanto, quando eu disse que 'strictFunctionTypes' faz com que os parâmetros da função sejam verificados mais corretamente, o que eu quis dizer é que o TypeScript não tratará os parâmetros de funções como bivariantes se você habilitar essa flag. Depois de explicar essa flag, as próximas serão uma brisa.

strictPropertyInitialization

Descrição

A strictPropertyInitialization garante que você inicialize todas as propriedades de sua classe no construtor. Por exemplo, se criarmos uma classe chamada User e dissermos que ela tem um nome que é um string, precisamos definir esse nome no construtor.

TypeScript
// ❌ Errado
class User {
  name: string;
  //=> ❌ COMPILER ERROR: Property 'name' has no initializer and is not definitely assigned in the constructor.

  constructor() {}
}
TypeScript
// ✅ Solução 1
class User {
  name: string;

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

Se não conseguirmos colocá-lo no construtor, precisamos mudar seu tipo para dizer que o nome pode ser ou uma string ou undefined.

TypeScript
// ✅ Solução 2
class User {
  name: string | undefined;

  constructor() {}
}

Motivação

A motivação para esta flag é a mesma que para o strictNullChecks, precisamos lidar com o undefined, não apenas ignorá-lo e esperar pelo melhor.

useUnknownInCatchVariables

Descrição

A última flag de strict mode é 'useUnknownInCatchVariables'. Esta flag irá implicitamente digitar uma variável em uma cláusula catch como unknown, ao invés de any. Isto é muito mais seguro porque a utilização de unknown nos força a definir nosso tipo antes de qualquer operação.

TypeScript
// useUnknownInCatchVariables = ❌false

try {
  // Some code...
} catch (err) {
  //=> err: any
  console.log(err.message);
}
TypeScript
// useUnknownInCatchVariables = ✅true

try {
  // Some code...
} catch (err) {
  //=> err: unknown
  console.log(err.message);
  //=> ❌ COMPILER ERROR: Object is of type 'unknown'
}
TypeScript
// useUnknownInCatchVariables = ✅true

try {
  // Some code...
} catch (err) {
  //=> err: unknown
  if (err instanceof Error) {
    console.log(err.message); //=> err: Error
  }
}

👉 Olhe esse vídeo de um minuto em que eu explico as diferenças entre any e unknown.

Motivação

Por exemplo, é bastante comum esperar que nossa variável de catch seja uma instância de Error, mas nem sempre é o caso. O JavaScript nos permite lançar o que quisermos. Podemos jogar um string em vez de um Error.

Isso se torna problemático quando tentamos acessar propriedades que só existem em um Error, como .message.

TypeScript
// useUnknownInCatchVariables = ❌false

try {
  throw Error('error message');
} catch (err) {
  console.log(err.message);
  //=> LOG: 'error message'
}
TypeScript
// useUnknownInCatchVariables = ❌false

try {
  throw 'error message';
} catch (err) {
  console.log(err.message);
  //=> LOG: undefined
}

O TypeScript permitirá que você faça o que quiser, porque, por padrão, as variáveis de catch são definidas como any. Mas se ativarmos essa flag, o código irá quebrar, porque o TypeScript irá definir as variáveis de catch como unknown, forçando-nos a verificar se nossa variável é de fato uma instância de Error antes de acessar a propriedade .message.

TypeScript
// useUnknownInCatchVariables = ✅true

try {
  throw 'error message';
} catch (err) {
  //=> err: unknown
  console.log(err.message);
  //=> ❌ COMPILER ERROR: Object is of type 'unknown'
}
TypeScript
// useUnknownInCatchVariables = ✅true

try {
  throw 'error message';
} catch (err) {
  //=> err: unknown
  if (err instanceof Error) {
    console.log(err.message); //=> err: Error
  }

  if (typeof err === 'string') {
    console.log(err); //=> err: string
  }
}

LinkNo Unused Code Flags

Agora vamos ver as flags de código não utilizado. São 4 nessa categoria:

  1. noUnusedLocals
  2. noUnusedParameters
  3. allowUnusedLabels = false
  4. allowUnreachableCode = false

Você verá que essas flags são mais parecidas com checagens de linting do que checagens de compilação.

noUnusedLocals

Descrição

A primeira, noUnusedLocals, emitirá um erro se houver variáveis locais não utilizadas.

TypeScript
// noUnusedLocals = ❌false

const getUserName = (): string => {
  const age = 23;
  return 'Joe';
};
TypeScript
// noUnusedLocals = ✅true

const getUserName = (): string => {
  const age = 23;
  //=> 💡 COMPILATION ERROR: 'age' is declared but its value is never read.
  return 'Joe';
};

Motivação

Ela não necessariamente apontará um bug em seu código, mas apontará um código desnecessário, que você pode remover e reduzir o seu bundle size.

TypeScript
// noUnusedLocals = ✅true

const getUserName = (): string => {
  return 'Joe';
};

noUnusedParameters

Descrição

noUnusedParameters faz a mesma coisa, mas para parâmetros de funções em vez de variáveis locais. Ele emitirá um erro se houver parâmetros não utilizados em funções.

TypeScript
// noUnusedParameters = ❌false

const getUserName = (age: number): string => {
  return 'Joe';
};
TypeScript
// noUnusedParameters = ✅true

const getUserName = (age: number): string => {
  //=> 💡 COMPILER ERROR: 'age' is declared but its value is never read.
  return 'Joe';
};

Motivação

A motivação é a mesma, remover o código desnecessário.

TypeScript
// noUnusedParameters = ✅true

const getUserName = (): string => {
  return 'Joe';
};

👉 Uma dica: se você realmente quiser declarar algum parâmetro de função não utilizado, você pode fazer isso prefixando-o com um sublinhado _. Em nosso exemplo, poderíamos mudar a idade para _idade e o TypeScript ficaria satisfeito.

TypeScript
// noUnusedParameters = ✅true

const getUserName = (age: number): string => {
  //=> 💡 COMPILER ERROR: 'age' is declared but its value is never read.
  return 'Joe';
};
TypeScript
// noUnusedParameters = ✅true

const getUserName = (_age: number): void => {};

allowUnusedLabels = false

Descrição

O JavaScript é uma linguagem de programação multiparadigma, imperativa, funcional e orientado a objetos. Sendo uma linguagem imperativa, ela suporta labels, que são como pontos de verificação em seu código, para os quais você pode pular.

TypeScript
// allowUnusedLabels = ✅true

let str = '';

label: for (let i = 0; i < 5; i++) {
  if (i === 1) {
    continue label;
  }
  str = str + i;
}

console.log(str);
//=> 🔉 LOG: "0234"

Labels são raramente usadas em JavaScript, e a sintaxe para declarar uma etiqueta é muito próxima da sintaxe para declarar um objeto literal. Assim, na maioria das vezes, os desenvolvedores declaram acidentalmente uma label pensando que estão criando um objeto literal.

TypeScript
// allowUnusedLabels = ✅true

const isUserAgeValid = (age: number) => {
  if (age > 20) {
    valid: true;
  }
};

Motivação

Para previnir isso, podemos definir allowUnusedLabels como false, o que criará erros de compilação se tivermos labels não utilizadas.

TypeScript
// allowUnusedLabels = ❌false

const isUserAgeValid = (age: number) => {
  if (age > 20) {
    valid: true;
    //=> 💡 COMPILER ERROR: Unused label
  }
};

allowUnreachableCode = false

Descrição

Outra coisa da qual podemos nos livrar com segurança é o código inalcançável. Se nunca vai ser executado, não há motivo para mantê-lo. O código que vem depois de uma declaração de return, por exemplo, é inalcançável.

TypeScript
// allowUnreachableCode = ✅true

const fn = (n: number): boolean => {
  if (n > 5) {
    return true;
  } else {
    return false;
  }

  return true; // Inalcançável
};

Motivação

Ao definir allowUnreachableCode como false, o compilador do TypeScript criará um erro se tivermos um código inalcançável.

TypeScript
// allowUnreachableCode = ❌false

const fn = (n: number): boolean => {
  if (n > 5) {
    return true;
  } else {
    return false;
  }

  return true;
  //=> 💡 COMPILER ERROR: Código inálcançavel detectado
};

LinkNo Implicit Code

A próxima categoria é "No Implicit Code". Há apenas 2 flags aqui:

  1. noImplicitOverride
  2. noImplicitReturns

👉 Aliás, noImplicitAny e noImplicitThis também poderiam estar aqui, mas elas já estão na categoria de strict mode.

noImplicitOverride

Descrição

Se você usa muita herança de objetos, primeiro: POR QUÊ? Segundo: O TypeScript 4.3 introduziu a palavra-chave override, esta palavra-chave tem o objetivo de tornar mais segura a substituição de seu membro.

Motivação

Voltando para nossa analogia com User e Admin. Vamos dizer que User têm um método chamado greet e você faz um override desse método na classe Admin.

TypeScript
class User {
  greet() {
    return 'Eu sou um usuário';
  }
}

class Admin extends User {
  greet() {
    return 'Eu não sou um usuário comum';
  }
}

Tudo muito bem, até que User decida renomear greet para saySomething. Então sua classe 'Admin' estará fora da sincronia.

TypeScript
class User {
  saySomething() {
    return 'Eu sou um usuário';
  }
}

class Admin extends User {
  greet() {
    return 'Eu não sou um usuário comum';
  }
}

Mas não tenha medo, porque, com a palavra-chave override, o TypeScript irá criar um erro de compilação, fazendo com que você saiba que está tentando ignorar um método que não está declarado na classe base User.

TypeScript
class User {
  saySomething() {
    return 'Eu sou um usuário';
  }
}

class Admin extends User {
  override greet() {
    //=> ⚠️ COMPILER ERROR: This member cannot have an 'override' modifier because it is not declared in the base class 'User'.
    return 'Eu não sou um usuário comum';
  }
}

Junto com a palavra-chave override, temos a flag noImplicitOverride. Na qual exige que todos os membros sobrescritos utilizem a palavra-chave override.

TypeScript
// noImplicitOverride = ❌false
class User {
  saySomething() {
    return 'Eu sou um usuário';
  }
}

class Admin extends User {
  saySomething() {
    return 'Eu não sou um usuário comum';
  }
}
TypeScript
// noImplicitOverride = ✅true
class User {
  saySomething() {
    return 'Eu sou um usuário';
  }
}

class Admin extends User {
  saySomething() {
    //=> ⚠️ COMPILER ERROR: This member must have an 'override' modifier because it overrides a member in the base class 'User'.
    return 'Eu não sou um usuário comum';
  }
}

Além de tornar suas a sobrescrita de membros mais seguras, há o benefício adicional de torná-las explícitas.

TypeScript
// noImplicitOverride = ✅true
class User {
  saySomething() {
    return 'Eu sou um usuário';
  }
}

class Admin extends User {
  override saySomething() {
    return 'Eu não sou um usuário comum';
  }
}

noImplicitReturns

Descrição

Por falar em explícito, também temos a flag noImplicitReturns, que verificará todos os caminhos de código em uma função para garantir que eles retornem um valor.

Motivação

Por exemplo, digamos que você tenha uma função getNameByUserID que recebe o ID de um usuário e retorna seu nome. Se o ID for 1, retorna Bob e se for 2, retorna Will.

TypeScript
// noImplicitReturns = ❌false
const getNameByUserID = (id: number) => {
  if (id === 1) return 'Bob';
  if (id === 2) return 'Will';
};

Há um problema óbvio aqui. O que aconteceria se solicitássemos o nome de um usuário com ID 3?

Habilitar noImplicitReturns nos daria um erro de compilação dizendo que nem todos os caminhos de código retornam um valor.

TypeScript
// noImplicitReturns = ✅true
const getNameByUserID = (id: number) => {
  //=> ❌ COMPILER ERROR: Not all code paths return a value.
  if (id === 1) return 'Bob';
  if (id === 2) return 'Will';
};

Para lidar com isso, temos duas alternativas:

1 - Lidar com casos em que a identificação não é 1 nem 2. Por exemplo, poderíamos retornar Joe por padrão.

TypeScript
// noImplicitReturns = ✅true
// ✅ Solução 1
const getNameByUserID = (id: number) => {
  if (id === 1) return 'Bob';
  if (id === 2) return 'Will';
  return 'Joe';
};

2 - Podemos manter nossa implementação como ela está, e digitar explicitamente nosso retorno de função como string | void.

TypeScript
// noImplicitReturns = ✅true
// ✅ Solução 2
const getNameByUserID = (id: number): string | void => {
  if (id === 1) return 'Bob';
  if (id === 2) return 'Will';
};

Isso relembra à motivação para a strictNullChecks. Temos que lidar com todos os casos possíveis. Essa flag deve estar sempre ativada.

LinkOutras

A última categoria é "Outras". Basicamente, flags úteis que eu não consegui encaixar nas categorias anteriores.

Há 5 flags aqui:

  1. noUncheckedIndexedAccess
  2. noPropertyAccessFromIndexSignature
  3. noFallthroughCasesInSwitch
  4. exactOptionalPropertyTypes
  5. forceConsistentCasingInFileNames

noUncheckedIndexedAccess

Descrição

Vamos começar com a `noUncheckedIndexedAccess'. O TypeScript nos permite index signatures para descrever objetos que têm keys desconhecidas, mas values conhecidos. Por exemplo, se você tem um objeto que mapeia IDs para usuários, você pode utilizar uma index signature para descrever isso.

TypeScript
// noUncheckedIndexedAccess = ❌false
interface User {
  id: string;
  name: string;
}

type UsersByID = {
  [userID: string]: User;
};

declare const usersMap: UsersByID;

Com isso no lugar, agora podemos acessar qualquer propriedade do userMap e obter um User em troca.

TypeScript
// noUncheckedIndexedAccess = ❌false
interface User {
  id: string;
  name: string;
}

type UsersByID = {
  [userID: string]: User;
};

declare const usersMap: UsersByID;

const example = usersMap.example;
//=> example: User

Motivação

Mas isso está obviamente errado. Nem todas as propriedades do nosso usersMap serão preenchidas. Portanto, o usersMap.example não deve ser do tipo User, deve ser do tipo User | undefined.

Isso parece ser um trabalho perfeito para 'strictNullChecks', mas não é. Para adicionar undefined ao acessar propriedades de index, precisamos da flag noUncheckedIndexedAccess.

TypeScript
// noUncheckedIndexedAccess = ✅true
interface User {
  id: string;
  name: string;
}

type UsersByID = {
  [userID: string]: User;
};

declare const usersMap: UsersByID;

const example = usersMap.example;
//=> example: User | undefined

noPropertyAccessFromIndexSignature

Descrição

No tópico de index properties, também temos a flag noPropertyAccessFromIndexSignature, que nos obriga a utilizar notação de colchete para acessar campos desconhecidos.

TypeScript
// noPropertyAccessFromIndexSignature = ❌false
interface GameSettings {
  // Known up-front properties
  speed: 'fast' | 'medium' | 'slow';
  quality: 'high' | 'low';
  [key: string]: string;
}

declare const settings: GameSettings;

settings.speed;
settings.quality;
settings.username;
settings['username'];
TypeScript
// noPropertyAccessFromIndexSignature = ✅true

interface GameSettings {
  // Known up-front properties
  speed: 'fast' | 'medium' | 'slow';
  quality: 'high' | 'low';
  [key: string]: string;
}

declare const settings: GameSettings;

settings.speed;
settings.quality;
settings.username;
//=> ⚠️ COMPILER ERROR: Property 'username' comes from an index signature, so it must be accessed with ['username'].
settings['username'];

Motivação

Essa é uma flag de linting. O benefício aqui é a consistência. Podemos criar o modelo mental de que o acesso a propriedades com notação de pontos sinaliza uma certeza de que a propriedade existe, enquanto que o uso de notação de parênteses sinaliza incerteza.

TypeScript
// Certainty
object.prop;

// Uncertainty
object['prop'];

noFallthroughCasesInSwitch

Descrição

Outra flag muito útil é noFallthroughCasesInSwitch. Se declararmos um case de switch, sem break ou return, nosso código executará as declarações desse case, assim como as declarações de qualquer case que se siga ao case correspondente, até chegar em um break, um return ou o final da declaração de switch.

TypeScript
// noFallthroughCasesInSwitch = ❌ false

const evenOrOdd = (value: number): void => {
  switch (value % 2) {
    case 0:
      console.log('Even');
    case 1:
      console.log('Odd');
      break;
  }
};

evenOrOdd(2);
//=> 🔉 LOG: "Even"
//=> 🔉 LOG: "Odd"

evenOrOdd(1);
//=> 🔉 LOG: "Odd"

Mesmo se você for um desenvolvedor sênior, é muito fácil esquecer acidentalmente uma declaração de break.

Motivação

Com noFallthroughCasesInSwitch ativado, o TypeScript emitirá erros de compilação para qualquer switch case não vazio que não tenha uma declaração de break ou return. Protegendo-nos contra bugs de casos de queda(fallthrough cases) acidentais.

TypeScript
// noFallthroughCasesInSwitch = ✅true

const evenOrOdd = (value: number): void => {
  switch (value % 2) {
    case 0:
      //=> ⚠️ COMPILER ERROR: Fallthrough case in switch.
      console.log('Even');
    case 1:
      console.log('Odd');
      break;
  }
};

evenOrOdd(2);
//=> 🔉 LOG: "Even"
//=> 🔉 LOG: "Odd"

evenOrOdd(1);
//=> 🔉 LOG: "Odd"

forceConsistentCasingInFileNames

Descrição

Outro erro fácil é confiar em nomes de arquivos não sensíveis a letras maiúsculas e minúsculas.

Por exemplo, se seu sistema operacional não diferencia caracteres minúsculos e maiúsculos em nomes de arquivos, você pode acessar um arquivo chamado User.ts digitando-o com um "U" maiúsculo ou com tudo em minúsculo, ele funcionará da mesma maneira. Mas pode não funcionar para o resto de sua equipe.

TypeScript
export interface User {
  name: string;
  email: string;
}
TypeScript
// forceConsistentCasingInFileNames = ❌false

import { User } from './User';
// ✅ Case-insensitive operating systems
// ✅ Case-sensitive operating systems

import { User } from './user';
// ✅ Case-insensitive operating systems
// ❌ Case-sensitive operating systems (the filename is User.ts)

Por padrão, o TypeScript segue as regras de sensibilidade do sistema de arquivos em que ele está rodando. Mas podemos mudar isso ativando a flag forceConsistentCasingInFileNames.

Motivação

Quando esta opção é ativada, o TypeScript irá criar erros de compilação se seu código tentar acessar um arquivo sem corresponder exatamente ao nome do arquivo.

Obtemos consistência e evitamos erros com sistemas operacionais sensíveis a letras maiúsculas e minúsculas.

TypeScript
// forceConsistentCasingInFileNames = ✅true

import { User } from './User';
// ✅ Case-insensitive operating systems
// ✅ Case-sensitive operating systems

import { User } from './user';
//=> ⚠️ COMPILER ERROR: File name 'user.ts' differs from already included file name 'User.ts' only in casing.
// ✅ Case-insensitive operating systems
// ❌ Case-sensitive operating systems (the filename is User.ts)

exactOptionalPropertyTypes

Descrição

A última flag que gostaria de mencionar é a ExactOptionalPropertyTypes. Em JavaScript, se você tem um objeto e tenta acessar uma propriedade que não existe nele, você recebe undefined. Isso é porque essa propriedade não foi definida.

Por exemplo, declare um objeto vazio chamado test e tente ver se property existe em test. Vamos obter false.

TypeScript
const test = {};
'property' in test; //=> false

Ok, nós entendemos isso. Mas também podemos definir explicitamente uma propriedade como undefined, e isso é um pouco diferente porque agora, se tentarmos ver se property existe em test, obteremos true.

TypeScript
const test = { property: undefined };
'property' in test; //=> true

Isso tudo é para dizer que há uma diferença entre uma propriedade ser undefined porque não foi definida e ser undefined porque nós a definimos como undefined.

Por padrão, o TypeScript ignora essa diferença, mas podemos mudar esse comportamento ao habilitar exactOptionalPropertyTypes.

TypeScript
// exactOptionalPropertyTypes = ❌false

interface Test {
  property?: string;
}

const test1: Test = {};
'property' in test1; //=> false

const test2: Test = { property: undefined };
'property' in test2; //=> true

Motivação

Com esta flag ativada, o TypeScript se torna consciente dessas duas formas de se ter uma propriedade undefined.

TypeScript
// exactOptionalPropertyTypes = ✅true

interface Test {
  property?: string;
}

const test1: Test = {};
'property' in test1; //=> false

const test2: Test = { property: undefined };
//=> ⚠️ COMPILER ERROR: Type '{ property: undefined; }' is not assignable to type 'Test' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
'property' in test2; //=> true

Quando utilizamos o operador de propriedade opcional ?, indicamos que uma propriedade pode ser undefined por não estar definida. Mas isso não nos permitirá definir explicitamente essa propriedade como undefined.

Se quisermos definir explicitamente uma propriedade como undefined, precisaremos dizer que ela pode ser undefined.

TypeScript
// exactOptionalPropertyTypes = ✅true

interface Test {
  property?: string | undefined;
}

const test1: Test = {};
'property' in test1; //=> false

const test2: Test = { property: undefined };
'property' in test2; //=> true

LinkConclusão

Isso é tudo. Se você quiser mergulhar mais fundo no TypeScript, tenho uma série sobre TypeScript narrowing. Você pode assistir a série completa, de graça, no meu canal.

Como sempre, as referências estão logo abaixo. Se você gostou do conteúdo, considere se inscrever na minha newsletter. E se sua empresa está procurando por desenvolvedores web remotos, considere entrar em contato comigo e minha equipe na seção "contato". tenha um ótimo dia e nos vemos em breve.

LinkRelacionado

TypeScript Narrowing Series

LinkReferências

  1. Flag strict do TypeScript Documentação do TypeScript
  2. TypeScript CLI compiler Documentação do TypeScript
  3. Strict Mode do JavaScript MDN
  4. Especificação do Strict Mode do ECMAScript Especificação do ECMAScript
  5. Como o Strict Mode do TypeScript na verdade corrige o TypeScript Eran Shabi (@eranshabi no Twitter)
  6. Porque os parametros de funções são bivariantes no TypeScript? FAQ oficial do TypeScript
  7. Contravariância e Covariância Matt Handler no Originate
  8. Covariância vs Contravariância nas linguagens de programação Code Radiance
  9. Paradigmas do JavaScript MDN
  10. Labels no JavaScript MDN
  11. Flags override e --noImplicitOverride Notas de lançamento do TypeScript 4.3
  12. Property Accessors no JavaScript - Notações de ponto e colchete MDN
  13. JavaScript Switch MDN

Assine a nossa Newsletter e seja avisado quando eu lançar um curso, postar um vídeo ou escrever um artigo.

Campo obrigatório
Campo obrigatório