Se você estiver criando aplicativos da Web complexos, o TypeScript provavelmente é sua linguagem de programação preferida. O TypeScript é muito apreciado por seu forte sistema de tipos e recursos de análise estática, tornando-o uma ferramenta poderosa para garantir que seu código seja robusto e livre de erros.
Ele também acelera o processo de desenvolvimento por meio da integração com editores de código, permitindo que os desenvolvedores naveguem pelo código com mais eficiência e obtenham dicas e preenchimento automático mais precisos, além de possibilitar a refatoração segura de grandes quantidades de código.
O compilador é o coração do TypeScript, responsável por verificar a correção do tipo e transformar o código TypeScript em JavaScript. No entanto, para utilizar totalmente o poder do TypeScript, é importante configurar o compilador corretamente.
Cada projeto TypeScript tem um ou mais arquivos tsconfig.json
que contêm todas as opções de configuração do compilador.
A configuração do tsconfig é uma etapa crucial para obter segurança de tipo ideal e experiência do desenvolvedor em seus projetos TypeScript. Ao reservar um tempo para considerar cuidadosamente todos os principais fatores envolvidos, você pode acelerar o processo de desenvolvimento e garantir que seu código seja robusto e livre de erros.
A configuração padrão no tsconfig pode fazer com que os desenvolvedores percam a maioria dos benefícios do TypeScript. Isso ocorre porque ele não habilita muitos recursos poderosos de verificação de tipo. Por configuração "padrão", quero dizer uma configuração em que nenhuma opção do compilador de verificação de tipo é definida.
Por exemplo:
{ "compilerOptions": { "target": "esnext", "module": "esnext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, }, "include": ["src"] }
A ausência de várias opções de configuração de chave pode resultar em qualidade de código inferior por dois motivos principais. Em primeiro lugar, o compilador do TypeScript pode lidar incorretamente com tipos null
e undefined
em vários casos.
Em segundo lugar, o tipo any
pode aparecer incontrolavelmente em sua base de código, levando à verificação de tipo desativada em torno desse tipo.
Felizmente, esses problemas são fáceis de corrigir ajustando algumas opções na configuração.
{ "compilerOptions": { "strict": true } }
O modo estrito é uma opção de configuração essencial que fornece garantias mais fortes de correção do programa, permitindo uma ampla gama de comportamentos de verificação de tipo.
Habilitar o modo estrito no arquivo tsconfig é uma etapa crucial para alcançar a máxima segurança de tipo e uma melhor experiência do desenvolvedor.
Requer um pouco de esforço extra na configuração do tsconfig, mas pode ajudar muito a melhorar a qualidade do seu projeto.
A opção de compilador strict
ativa todas as opções de família de modo estrito, que incluem noImplicitAny
, strictNullChecks
, strictFunctionTypes
, entre outros.
Essas opções também podem ser configuradas separadamente, mas não é recomendável desativar nenhuma delas. Vejamos exemplos para ver o porquê.
{ "compilerOptions": { "noImplicitAny": true } }
O tipo any
é uma brecha perigosa no sistema de tipo estático e usá-lo desabilita todas as regras de verificação de tipo. Como resultado, todos os benefícios do TypeScript são perdidos: bugs são perdidos, as dicas do editor de código param de funcionar corretamente e assim por diante.
O uso any
é permitido apenas em casos extremos ou para necessidades de prototipagem. Apesar de nossos melhores esforços, o tipo any
pode às vezes entrar em uma base de código implicitamente.
Por padrão, o compilador nos perdoa muitos erros em troca do aparecimento de any
em uma base de código. Especificamente, TypeScript nos permite não especificar o tipo de variáveis, mesmo quando o tipo não pode ser inferido automaticamente.
O problema é que podemos acidentalmente esquecer de especificar o tipo de uma variável, por exemplo, para um argumento de função. Em vez de mostrar um erro, o TypeScript inferirá automaticamente o tipo da variável como any
.
function parse(str) { // ^? any return str.split(''); } // TypeError: str.split is not a function const res1 = parse(42); const res2 = parse('hello'); // ^? any
Habilitar a opção do compilador noImplicitAny
fará com que o compilador destaque todos os lugares onde o tipo de uma variável é inferido automaticamente como any
. Em nosso exemplo, o TypeScript solicitará que especifiquemos o tipo do argumento da função.
function parse(str) { // ^ Error: Parameter 'str' implicitly has an 'any' type. return str.split(''); }
Quando especificamos o tipo, o TypeScript detecta rapidamente o erro de passar um número para um parâmetro de string. O valor de retorno da função, armazenado na variável res2
, também terá o tipo correto.
function parse(str: string) { return str.split(''); } const res1 = parse(42); // ^ Error: Argument of type 'number' is not // assignable to parameter of type 'string' const res2 = parse('hello'); // ^? string[]
{ "compilerOptions": { "useUnknownInCatchVariables": true } }
A configuração de useUnknownInCatchVariables
permite a manipulação segura de exceções em blocos try-catch. Por padrão, o TypeScript assume que o tipo de erro em um bloco catch é any
, o que nos permite fazer qualquer coisa com o erro.
Por exemplo, poderíamos passar o erro detectado como está para uma função de registro que aceita uma instância de Error
.
function logError(err: Error) { // ... } try { return JSON.parse(userInput); } catch (err) { // ^? any logError(err); }
No entanto, na realidade, não há garantias sobre o tipo de erro e só podemos determinar seu verdadeiro tipo em tempo de execução quando o erro ocorre. Se a função de registro receber algo que não seja um Error
, isso resultará em um erro de tempo de execução.
Portanto, a opção useUnknownInCatchVariables
alterna o tipo de erro de any
para unknown
para nos lembrar de verificar o tipo de erro antes de fazer qualquer coisa com ele.
try { return JSON.parse(userInput); } catch (err) { // ^? unknown // Now we need to check the type of the value if (err instanceof Error) { logError(err); } else { logError(new Error('Unknown Error')); } }
Agora, o TypeScript solicitará que verifiquemos o tipo da variável err
antes de passá-la para a função logError
, resultando em um código mais correto e seguro. Infelizmente, esta opção não ajuda com erros de digitação em funções promise.catch()
ou funções de retorno de chamada.
Mas discutiremos maneiras de lidar com any
desses casos no próximo artigo.
{ "compilerOptions": { "strictBindCallApply": true } }
Outra opção corrige a aparência de any
chamada em função via call
e apply
. Este é um caso menos comum do que os dois primeiros, mas ainda é importante considerar. Por padrão, o TypeScript não verifica os tipos nessas construções.
Por exemplo, podemos passar qualquer coisa como argumento para uma função e, no final, sempre receberemos o tipo any
.
function parse(value: string) { return parseInt(value, 10); } const n1 = parse.call(undefined, '10'); // ^? any const n2 = parse.call(undefined, false); // ^? any
Habilitar a opção strictBindCallApply
torna o TypeScript mais inteligente, portanto, o tipo de retorno será inferido corretamente como number
. E ao tentar passar um argumento do tipo errado, o TypeScript apontará para o erro.
function parse(value: string) { return parseInt(value, 10); } const n1 = parse.call(undefined, '10'); // ^? number const n2 = parse.call(undefined, false); // ^ Argument of type 'boolean' is not // assignable to parameter of type 'string'.
{ "compilerOptions": { "noImplicitThis": true } }
A próxima opção que pode ajudar a evitar o aparecimento de any
em seu projeto corrige a manipulação do contexto de execução em chamadas de função. A natureza dinâmica do JavaScript torna difícil determinar estaticamente o tipo de contexto dentro de uma função.
Por padrão, o TypeScript usa o tipo any
para o contexto nesses casos e não fornece nenhum aviso.
class Person { private name: string; constructor(name: string) { this.name = name; } getName() { return function () { return this.name; // ^ 'this' implicitly has type 'any' because // it does not have a type annotation. }; } }
Habilitar a opção de compilador noImplicitThis
nos solicitará especificar explicitamente o tipo de contexto para uma função. Desta forma, no exemplo acima, podemos pegar o erro de acesso ao contexto da função ao invés do campo name
da classe Person
.
{ "compilerOptions": { "strictNullChecks": true } }
Em seguida, várias opções incluídas no modo strict
não resultam na exibição de any
tipo na base de código. No entanto, eles tornam o comportamento do compilador TS mais rígido e permitem que mais erros sejam encontrados durante o desenvolvimento.
A primeira dessas opções corrige o tratamento de null
e undefined
no TypeScript. Por padrão, o TypeScript assume que null
e undefined
são valores válidos para qualquer tipo, o que pode resultar em erros de tempo de execução inesperados.
Habilitar a opção de compilador strictNullChecks
força o desenvolvedor a lidar explicitamente com casos em que null
e undefined
podem ocorrer.
Por exemplo, considere o seguinte código:
const users = [ { name: 'Oby', age: 12 }, { name: 'Heera', age: 32 }, ]; const loggedInUser = users.find(u => u.name === 'Max'); // ^? { name: string; age: number; } console.log(loggedInUser.age); // ^ TypeError: Cannot read properties of undefined
Este código irá compilar sem erros, mas pode gerar um erro de tempo de execução se o usuário com o nome “Max” não existir no sistema e users.find()
retornar undefined
. Para evitar isso, podemos habilitar a opção de compilador strictNullChecks
.
Agora, o TypeScript nos forçará a lidar explicitamente com a possibilidade de null
ou undefined
ser retornado por users.find()
.
const loggedInUser = users.find(u => u.name === 'Max'); // ^? { name: string; age: number; } | undefined if (loggedInUser) { console.log(loggedInUser.age); }
Ao manipular explicitamente a possibilidade de null
e undefiined
, podemos evitar erros de tempo de execução e garantir que nosso código seja mais robusto e livre de erros.
{ "compilerOptions": { "strictFunctionTypes": true } }
Habilitar strictFunctionTypes
torna o compilador do TypeScript mais inteligente. Antes da versão 2.6, o TypeScript não verificava a contravariância dos argumentos da função. Isso levará a erros de tempo de execução se a função for chamada com um argumento do tipo errado.
Por exemplo, mesmo que um tipo de função seja capaz de lidar com strings e números, podemos atribuir a esse tipo uma função que só pode lidar com strings. Ainda podemos passar um número para essa função, mas receberemos um erro de tempo de execução.
function greet(x: string) { console.log("Hello, " + x.toLowerCase()); } type StringOrNumberFn = (y: string | number) => void; // Incorrect Assignment const func: StringOrNumberFn = greet; // TypeError: x.toLowerCase is not a function func(10);
Felizmente, habilitar a opção strictFunctionTypes
corrige esse comportamento, e o compilador pode detectar esses erros em tempo de compilação, mostrando-nos uma mensagem detalhada da incompatibilidade de tipo nas funções.
const func: StringOrNumberFn = greet; // ^ Type '(x: string) => void' is not assignable to type 'StringOrNumberFn'. // Types of parameters 'x' and 'y' are incompatible. // Type 'string | number' is not assignable to type 'string'. // Type 'number' is not assignable to type 'string'.
{ "compilerOptions": { "strictPropertyInitialization": true } }
Por último, mas não menos importante, a opção strictPropertyInitialization
permite a verificação da inicialização de propriedade de classe obrigatória para tipos que não incluem undefined
como um valor.
Por exemplo, no código a seguir, o desenvolvedor esqueceu de inicializar a propriedade email
. Por padrão, o TypeScript não detecta esse erro e pode ocorrer um problema no tempo de execução.
class UserAccount { name: string; email: string; constructor(name: string) { this.name = name; // Forgot to assign a value to this.email } }
No entanto, quando a opção strictPropertyInitialization
estiver habilitada, o TypeScript destacará esse problema para nós.
email: string; // ^ Error: Property 'email' has no initializer and // is not definitely assigned in the constructor.
{ "compilerOptions": { "noUncheckedIndexedAccess": true } }
A opção noUncheckedIndexedAccess
não faz parte do modo strict
, mas é outra opção que pode ajudar a melhorar a qualidade do código em seu projeto. Ele permite que a verificação de expressões de acesso ao índice tenham um tipo de retorno null
ou undefined
, o que pode evitar erros de tempo de execução.
Considere o exemplo a seguir, onde temos um objeto para armazenar valores em cache. Em seguida, obtemos o valor de uma das chaves. Obviamente, não temos garantia de que o valor da chave desejada realmente exista no cache.
Por padrão, o TypeScript assumiria que o valor existe e tem o tipo string
. Isso pode levar a um erro de tempo de execução.
const cache: Record<string, string> = {}; const value = cache['key']; // ^? string console.log(value.toUpperCase()); // ^ TypeError: Cannot read properties of undefined
Habilitar a opção noUncheckedIndexedAccess
no TypeScript requer a verificação das expressões de acesso ao índice para o tipo de retorno undefined
, o que pode nos ajudar a evitar erros de tempo de execução. Isso também se aplica ao acesso a elementos em uma matriz.
const cache: Record<string, string> = {}; const value = cache['key']; // ^? string | undefined if (value) { console.log(value.toUpperCase()); }
Com base nas opções discutidas, é altamente recomendável habilitar as opções strict
e noUncheckedIndexedAccess
no arquivo tsconfig.json
de seu projeto para segurança de tipo ideal.
{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, } }
Se você já ativou a opção strict
, considere remover as seguintes opções para evitar duplicar a opção strict: true
:
noImplicitAny
useUnknownInCatchVariables
strictBindCallApply
noImplicitThis
strictFunctionTypes
strictNullChecks
strictPropertyInitialization
Também é recomendável remover as seguintes opções que podem enfraquecer o sistema de tipos ou causar erros de tempo de execução:
keyofStringsOnly
noStrictGenericChecks
suppressImplicitAnyIndexErrors
suppressExcessPropertyErrors
Ao considerar e configurar cuidadosamente essas opções, você pode obter segurança de tipo ideal e uma melhor experiência do desenvolvedor em seus projetos TypeScript.
O TypeScript percorreu um longo caminho em sua evolução, melhorando constantemente seu compilador e sistema de tipos. No entanto, para manter a compatibilidade com versões anteriores, a configuração do TypeScript tornou-se mais complexa, com muitas opções que podem afetar significativamente a qualidade da verificação de tipos.
Ao considerar e configurar cuidadosamente essas opções, você pode obter segurança de tipo ideal e uma melhor experiência do desenvolvedor em seus projetos TypeScript. É importante saber quais opções ativar e remover de uma configuração de projeto.
Compreender as consequências de desabilitar certas opções permitirá que você tome decisões informadas para cada uma delas.
É importante ter em mente que a digitação estrita pode ter consequências. Para lidar efetivamente com a natureza dinâmica do JavaScript, você precisará ter um bom entendimento do TypeScript além de simplesmente especificar "número" ou "string" após uma variável.
Você precisará estar familiarizado com construções mais complexas e com o primeiro ecossistema de bibliotecas e ferramentas do TypeScript para resolver com mais eficácia os problemas relacionados a tipos que surgirão.
Como resultado, escrever código pode exigir um pouco mais de esforço, mas com base na minha experiência, esse esforço vale a pena para projetos de longo prazo.
Espero que você tenha aprendido algo novo com este artigo. Esta é a primeira parte de uma série. No próximo artigo, discutiremos como obter melhor segurança de tipos e qualidade de código aprimorando os tipos na biblioteca padrão do TypeScript. Fique ligado, e obrigado por ler!