20 flags de TypeScript que você deveria estar usando
Faça isso se você usa TypeScript!
Introduçã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.
Lidando 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.
Nossas recomendações de opção de compilador
Sem mais delongas, estas são as 20 opções de compilação TypeScript que recomendamos:
{
"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
}
}
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:
- Strict mode
- No unused code
- No implicit code
- Others
Ficando 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 é
Strict Mode Flags
Vamos começar pelas flags de strict mode. Como você pode ver, existem 9 flags na categoria de strict mode:
strict
alwaysStrict
noImplicitAny
noImplicitThis
strictNullChecks
strictBindCallApply
strictFunctionTypes
strictPropertyInitialization
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.
{
"compilerOptions": {
"strict": true,
}
}
TypeScript{
"compilerOptions": {
"strict": true,
}
}
{
"compilerOptions": {
"alwaysStrict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"useUnknownInCatchVariables": 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.
{
"compilerOptions": {
"alwaysStrict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"useUnknownInCatchVariables": true,
"someFlagFromTheFuture": true,
"someOtherFlagFromTheFuture": true,
"yetAnotherFlagFromTheFuture": true,
}
}
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.
'use strict';
var str = 'Hello World';
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.
'use strict';
mistypeVariable = 17;
//=> ⚠️ RUNTIME ERROR: mistypeVariable is not defined
JavaScript'use strict';
mistypeVariable = 17;
//=> ⚠️ RUNTIME ERROR: mistypeVariable is not defined
// 'use strict';
mistypeVariable = 17;
//=> ✅ OK, definindo window.mistypeVariable como 17
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.
mistypeVariable = 17;
//=> ❌ COMPILER ERROR: Cannot find name 'mistypeVariable'
TypeScriptmistypeVariable = 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.
function explicit(explicitAny: any) {}
//=> ✅ OK: Estamos definindo explícitamente como 'any'
function implicit(implicitAny) {}
//=> ❌ COMPILER ERROR: Parameter 'implicitAny' implicitly has an 'any' type
TypeScriptfunction 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á.
// noImplicityAny: false ❌
function splitByDots(value) {
//=> value: any
//=> returns: any
return value.split('.');
}
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.
// 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?"
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
.
// 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 😔
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.
// 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) {
//=> ❌ 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(...)
// 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'?
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
.
// noImplicityAny: true ✅
function splitByDots(value: any) {
//=> value: any
//=> returns: any
return value.split('.');
}
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:
class User {
name: string;
constructor(name: string) {
this.name = name;
}
getName() {
return function () {
return this.name;
};
}
}
const user = new User('Tom');
user.getName()();
TypeScriptclass 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.
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
TypeScriptclass 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.
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()();
TypeScriptclass 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?
const users = [{ name: 'Bob', age: 13 }];
const user = users.find((u) => u.age < 10);
console.log(user.name);
//=> ❌ COMPILER ERROR: Object is possibly 'undefined'.
TypeScriptconst 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.
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'.
TypeScriptconst 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:
interface User {
name: string;
}
interface Admin extends User {
permissions: Array<string>;
}
declare let admin: Admin;
declare let user: User;
TypeScriptinterface 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
.
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
TypeScriptinterface 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
.
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)
TypeScriptinterface 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.
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)
TypeScriptinterface 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.
// ❌ Errado
class User {
name: string;
//=> ❌ COMPILER ERROR: Property 'name' has no initializer and is not definitely assigned in the constructor.
constructor() {}
}
TypeScript// ❌ Errado
class User {
name: string;
//=> ❌ COMPILER ERROR: Property 'name' has no initializer and is not definitely assigned in the constructor.
constructor() {}
}
// ✅ Solução 1
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
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
.
// ✅ Solução 2
class User {
name: string | undefined;
constructor() {}
}
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.
// useUnknownInCatchVariables = ❌false
try {
// Some code...
} catch (err) {
//=> err: any
console.log(err.message);
}
TypeScript// useUnknownInCatchVariables = ❌false
try {
// Some code...
} catch (err) {
//=> err: any
console.log(err.message);
}
// 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
console.log(err.message);
//=> ❌ COMPILER ERROR: Object is of type 'unknown'
}
// useUnknownInCatchVariables = ✅true
try {
// Some code...
} catch (err) {
//=> err: unknown
if (err instanceof Error) {
console.log(err.message); //=> err: Error
}
}
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
.
// useUnknownInCatchVariables = ❌false
try {
throw Error('error message');
} catch (err) {
console.log(err.message);
//=> LOG: 'error message'
}
TypeScript// useUnknownInCatchVariables = ❌false
try {
throw Error('error message');
} catch (err) {
console.log(err.message);
//=> LOG: 'error message'
}
// useUnknownInCatchVariables = ❌false
try {
throw 'error message';
} catch (err) {
console.log(err.message);
//=> LOG: undefined
}
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
.
// 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
console.log(err.message);
//=> ❌ COMPILER ERROR: Object is of type 'unknown'
}
// 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
}
}
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
}
}
No Unused Code Flags
Agora vamos ver as flags de código não utilizado. São 4 nessa categoria:
noUnusedLocals
noUnusedParameters
allowUnusedLabels = false
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.
// noUnusedLocals = ❌false
const getUserName = (): string => {
const age = 23;
return 'Joe';
};
TypeScript// noUnusedLocals = ❌false
const getUserName = (): string => {
const age = 23;
return 'Joe';
};
// noUnusedLocals = ✅true
const getUserName = (): string => {
const age = 23;
//=> 💡 COMPILATION ERROR: 'age' is declared but its value is never read.
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.
// noUnusedLocals = ✅true
const getUserName = (): string => {
return 'Joe';
};
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.
// noUnusedParameters = ❌false
const getUserName = (age: number): string => {
return 'Joe';
};
TypeScript// noUnusedParameters = ❌false
const getUserName = (age: number): string => {
return 'Joe';
};
// 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): 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.
// noUnusedParameters = ✅true
const getUserName = (): string => {
return 'Joe';
};
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.
// 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): string => {
//=> 💡 COMPILER ERROR: 'age' is declared but its value is never read.
return 'Joe';
};
// noUnusedParameters = ✅true
const getUserName = (_age: number): void => {};
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.
// 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"
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.
// allowUnusedLabels = ✅true
const isUserAgeValid = (age: number) => {
if (age > 20) {
valid: true;
}
};
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.
// allowUnusedLabels = ❌false
const isUserAgeValid = (age: number) => {
if (age > 20) {
valid: true;
//=> 💡 COMPILER ERROR: Unused label
}
};
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.
// allowUnreachableCode = ✅true
const fn = (n: number): boolean => {
if (n > 5) {
return true;
} else {
return false;
}
return true; // 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.
// 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
};
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
};
No Implicit Code
A próxima categoria é "No Implicit Code". Há apenas 2 flags aqui:
noImplicitOverride
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
.
class User {
greet() {
return 'Eu sou um usuário';
}
}
class Admin extends User {
greet() {
return 'Eu não sou um usuário comum';
}
}
TypeScriptclass 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.
class User {
saySomething() {
return 'Eu sou um usuário';
}
}
class Admin extends User {
greet() {
return 'Eu não sou um usuário comum';
}
}
TypeScriptclass 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
.
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';
}
}
TypeScriptclass 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
.
// 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 = ❌false
class User {
saySomething() {
return 'Eu sou um usuário';
}
}
class Admin extends User {
saySomething() {
return 'Eu não sou um usuário comum';
}
}
// 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';
}
}
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.
// 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';
}
}
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.
// noImplicitReturns = ❌false
const getNameByUserID = (id: number) => {
if (id === 1) return 'Bob';
if (id === 2) return '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.
// 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';
};
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.
// noImplicitReturns = ✅true
// ✅ Solução 1
const getNameByUserID = (id: number) => {
if (id === 1) return 'Bob';
if (id === 2) return 'Will';
return 'Joe';
};
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
.
// noImplicitReturns = ✅true
// ✅ Solução 2
const getNameByUserID = (id: number): string | void => {
if (id === 1) return 'Bob';
if (id === 2) return 'Will';
};
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.
Outras
A última categoria é "Outras". Basicamente, flags úteis que eu não consegui encaixar nas categorias anteriores.
Há 5 flags aqui:
noUncheckedIndexedAccess
noPropertyAccessFromIndexSignature
noFallthroughCasesInSwitch
exactOptionalPropertyTypes
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.
// noUncheckedIndexedAccess = ❌false
interface User {
id: string;
name: string;
}
type UsersByID = {
[userID: string]: User;
};
declare const usersMap: UsersByID;
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.
// noUncheckedIndexedAccess = ❌false
interface User {
id: string;
name: string;
}
type UsersByID = {
[userID: string]: User;
};
declare const usersMap: UsersByID;
const example = usersMap.example;
//=> example: User
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
.
// noUncheckedIndexedAccess = ✅true
interface User {
id: string;
name: string;
}
type UsersByID = {
[userID: string]: User;
};
declare const usersMap: UsersByID;
const example = usersMap.example;
//=> example: User | undefined
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.
// 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 = ❌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'];
// 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'];
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.
// Certainty
object.prop;
// Uncertainty
object['prop'];
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
.
// 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"
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.
// 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"
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.
export interface User {
name: string;
email: string;
}
TypeScriptexport interface User {
name: string;
email: string;
}
// 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)
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.
// 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)
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
.
const test = {};
'property' in test; //=> false
TypeScriptconst 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
.
const test = { property: undefined };
'property' in test; //=> true
TypeScriptconst 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
.
// exactOptionalPropertyTypes = ❌false
interface Test {
property?: string;
}
const test1: Test = {};
'property' in test1; //=> false
const test2: Test = { property: undefined };
'property' in test2; //=> true
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
.
// 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
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
.
// exactOptionalPropertyTypes = ✅true
interface Test {
property?: string | undefined;
}
const test1: Test = {};
'property' in test1; //=> false
const test2: Test = { property: undefined };
'property' in test2; //=> true
TypeScript// exactOptionalPropertyTypes = ✅true
interface Test {
property?: string | undefined;
}
const test1: Test = {};
'property' in test1; //=> false
const test2: Test = { property: undefined };
'property' in test2; //=> true
Conclusã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.
Relacionado
Referências
- Flag strict do TypeScript Documentação do TypeScript
- TypeScript CLI compiler Documentação do TypeScript
- Strict Mode do JavaScript MDN
- Especificação do Strict Mode do ECMAScript Especificação do ECMAScript
- Como o Strict Mode do TypeScript na verdade corrige o TypeScript Eran Shabi (@eranshabi no Twitter)
- Porque os parametros de funções são bivariantes no TypeScript? FAQ oficial do TypeScript
- Contravariância e Covariância Matt Handler no Originate
- Covariância vs Contravariância nas linguagens de programação Code Radiance
- Paradigmas do JavaScript MDN
- Labels no JavaScript MDN
- Flags
override
e--noImplicitOverride
Notas de lançamento do TypeScript 4.3 - Property Accessors no JavaScript - Notações de ponto e colchete MDN
- JavaScript Switch MDN