Fundamentos do Type Guards - TypeScript Narrowing #2
TypeScript Narrowing #2
Ei!
Bem-vindo ao segundo artigo da nossa série de narrowing em TypeScript, onde cobriremos desde fundamentos até casos de uso avançados.
Nosso objetivo
Neste artigo, eu quero mostrar a vocês os type guards fundamentais na prática. Para fazer isso, vamos construir uma função que formata mensagens de erro antes de mostrá-las ao usuário final.
const formatErrorMessage =
(value: ???): string => { ... }
TypeScriptconst formatErrorMessage =
(value: ???): string => { ... }
Nossa função deve ser capaz de receber vários tipos diferentes e retornar uma mensagem de erro formatada.
const formatErrorMessage =
(value: null | undefined | string | Error | Warning): string => { ... }
TypeScriptconst formatErrorMessage =
(value: null | undefined | string | Error | Warning): string => { ... }
Você está pronto? 🥁
O Operator Guard typeof
Começaremos oferecendo suporte a apenas dois tipos: string
e Error
.
const formatErrorMessage =
(value: string | Error): string => { ... }
TypeScriptconst formatErrorMessage =
(value: string | Error): string => { ... }
Para implementar essa função, precisamos estreitar a string
| Error
para apenas uma string e lidar com isso, então nós estreitamos para apenas um Error
e lidamos com ele.
const formatErrorMessage = (value: string | Error): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
// Se for um erro, retorne o Error.message com o prefixo
};
TypeScriptconst formatErrorMessage = (value: string | Error): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
// Se for um erro, retorne o Error.message com o prefixo
};
O primeiro type guard que vamos explorar é o typeof
operator. Este operador nos permite verificar se um determinado valor é um:
"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"
Então, vamos usá-lo em nossa função.
const formatErrorMessage = (value: string | Error): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value; // <- value: string
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message; // <- value: Error
};
TypeScriptconst formatErrorMessage = (value: string | Error): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value; // <- value: string
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message; // <- value: Error
};
O que está acontecendo aqui é que a instrução typeof value === 'string'
está atuando como um type guard para string
. TypeScript sabe que a única maneira do código dentro desse if executar é se o valor
for uma string
. Portanto, ele estreita o tipo para string
dentro do bloco if.
Já que estamos retornando
algo, value
não pode ser uma string
após a instrução if, portanto, o único tipo que resta é Error
.
O Operator Guard in
Às vezes, não temos tanta sorte. Por exemplo, vamos adicionar um novo tipo à nossa função, uma interface personalizada chamada Warning
.
const formatErrorMessage = (value: string | Error): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value; // <- value: string
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message; // <- value: Error
};
interface Warning {
text: string;
}
TypeScriptconst formatErrorMessage = (value: string | Error): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value; // <- value: string
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message; // <- value: Error
};
interface Warning {
text: string;
}
Agora o nosso código está quebrado.
const formatErrorMessage = (value: string | Error | Warning): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value; // <- value: string
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message; // <- value: Error | Warning
};
interface Warning {
text: string;
}
TypeScriptconst formatErrorMessage = (value: string | Error | Warning): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value; // <- value: string
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message; // <- value: Error | Warning
};
interface Warning {
text: string;
}
Antes, nossa variável value
só poderia ser uma instância da classe Error
após o if.
const formatErrorMessage = (value: string | Error): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value; // <- value: string
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message; // <- value: Error
};
interface Warning {
text: string;
}
TypeScriptconst formatErrorMessage = (value: string | Error): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value; // <- value: string
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message; // <- value: Error
};
interface Warning {
text: string;
}
Mas agora, pode ser um Error | Warning
, e a propriedade .message
não existe em um Warning
.
const formatErrorMessage = (value: string | Error | Warning): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value; // <- value: string
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message; // <- value: Error | Warning
};
interface Warning {
text: string;
}
TypeScriptconst formatErrorMessage = (value: string | Error | Warning): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value; // <- value: string
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message; // <- value: Error | Warning
};
interface Warning {
text: string;
}
O operador typeof
não vai nos ajudar aqui, porque typeof value
seria object
para ambos os casos.
const formatErrorMessage =
(value: string | Error | Warning): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value // <- value: string
}
// Se for um Warning, retorne o Warning.text com o prefixo
if (???) {
return prefix + value.text
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message // <- value: Error | Warning
}
interface Warning {
text: string
}
TypeScriptconst formatErrorMessage =
(value: string | Error | Warning): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value // <- value: string
}
// Se for um Warning, retorne o Warning.text com o prefixo
if (???) {
return prefix + value.text
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message // <- value: Error | Warning
}
interface Warning {
text: string
}
Uma das maneiras idiomáticas de lidar com essa situação em JavaScript seria verificar se value
tem a propriedade .text
. Se tiver, é um Warning
. Podemos fazer isso com o operador in
.
const formatErrorMessage = (value: string | Error | Warning): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value; // <- value: string
}
// Se for um Warning, retorne o Warning.text com o prefixo
if ('text' in value) {
return prefix + value.text; // <- value: Warning
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message; // <- value: Error
};
interface Warning {
text: string;
}
TypeScriptconst formatErrorMessage = (value: string | Error | Warning): string => {
const prefix = 'Error: ';
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value; // <- value: string
}
// Se for um Warning, retorne o Warning.text com o prefixo
if ('text' in value) {
return prefix + value.text; // <- value: Warning
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message; // <- value: Error
};
interface Warning {
text: string;
}
Este operador retorna verdadeiro
se o objeto fornecido possui a propriedade fornecida. Nesse caso, se value
tiver a propriedade .text
.
TypeScript sabe que o nosso if só será verdadeiro
se value
for um Warning
porque esse é o único tipo possível para value
que tem uma propriedade chamada .text
, portanto, ele estreita o tipo para Warning
dentro do bloco if.
Após o primeiro if, value
pode ser Warning | Error
. Após o segundo if, só pode ser Error
.
Estreitamento por igualidade
Também é muito comum oferecer suporte a argumentos opcionais, isso significa, permitir que value
seja null
ou undefined
.
const formatErrorMessage =
(value: null | undefined | string | Error | Warning): string => {
const prefix = 'Error: ';
// Se for null ou indefinido, retorna "Unknown" com o prefixo
if (???) {
return prefix + 'Unknown'
}
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value // <- value: string
}
// Se for um Warning, retorne o Warning.text com o prefixo
if ('text' in value) {
return prefix + value.text // <- value: Warning
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message // <- value: Error
}
interface Warning {
text: string
}
TypeScriptconst formatErrorMessage =
(value: null | undefined | string | Error | Warning): string => {
const prefix = 'Error: ';
// Se for null ou indefinido, retorna "Unknown" com o prefixo
if (???) {
return prefix + 'Unknown'
}
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value // <- value: string
}
// Se for um Warning, retorne o Warning.text com o prefixo
if ('text' in value) {
return prefix + value.text // <- value: Warning
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message // <- value: Error
}
interface Warning {
text: string
}
Nós poderíamos lidar com undefined
usando o operador typeof
, mas isso não funcionaria com null
.
A propósito, se você quer saber por que não funcionaria para null
e as diferenças entre null
e undefined
, Tenho um artigo muito curto e informativo explicando exatamente isso. Vou deixar um link para ele nas referências.
O que poderíamos fazer que funcionaria para null
e undefined
é usar operadores de igualdade, como ===
:
const formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// Se for null ou indefinido, retorna "Unknown" com o prefixo
if (value === null || value === undefined) {
return prefix + 'Unknown';
}
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value;
}
// Se for um Warning, retorne o Warning.text com o prefixo
if ('text' in value) {
return prefix + value.text;
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message;
};
interface Warning {
text: string;
}
TypeScriptconst formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// Se for null ou indefinido, retorna "Unknown" com o prefixo
if (value === null || value === undefined) {
return prefix + 'Unknown';
}
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value;
}
// Se for um Warning, retorne o Warning.text com o prefixo
if ('text' in value) {
return prefix + value.text;
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message;
};
interface Warning {
text: string;
}
Nosso if só será verdadeiro se o valor for igual a null
ou undefined
, então TypeScript estreita o nosso tipo a null | undefined
.
Isso é chamado estreitamento por igualdade e também funciona com outros operadores de comparação, tais como:
- Diferente
!==
- Igual amplo
==
- Diferente amplo
!=
Estreitamento por truthiness
Mas é o seguinte, estreitamento por igualdade não é a forma idiomática do JavaScript de verificar se há null | undefined
. A maneira idiomática de fazer isso é verificar se value é truthy.
Eu tenho um artigo curto explicando o que é truthy e falsy em JavaScript. Vou colocar o link nas referências. Seria bom se você pudesse lê-lo antes de prosseguirmos, para ter as definições de truthy e falsy frescas na mente. Vá em frente, estou esperando.
Agora que todos nós temos as definições de truthy e falsy frescas em nossas mentes, deixe-me lhe apresentar o estreitamento por truthiness.
Em vez de usar o estreitamento por igualdade para verificar se value é igual a null
ou undefined
, podemos apenas verificar se ele é falsy.
const formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// Se for falso (null, undefined, string vazia), retorne "Unknown" com o prefixo
if (!value) {
return prefix + 'Unknown';
}
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value;
}
// Se for um Warning, retorne o Warning.text com o prefixo
if ('text' in value) {
return prefix + value.text;
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message;
};
interface Warning {
text: string;
}
TypeScriptconst formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// Se for falso (null, undefined, string vazia), retorne "Unknown" com o prefixo
if (!value) {
return prefix + 'Unknown';
}
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value;
}
// Se for um Warning, retorne o Warning.text com o prefixo
if ('text' in value) {
return prefix + value.text;
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message;
};
interface Warning {
text: string;
}
Podemos fazer isso prefixando-o com um NOT lógico !
. Isso converterá value em um booleano e o inverterá. Se for falsy, será convertido para falso
e depois invertido para verdadeiro
.
Análise de Fluxo de Controle
Até agora, evitamos um guarda para verificar se value
é uma instância da classe Error
. Eu disse como estamos conseguindo fazer isso. Estamos tratando todos os tipos possíveis, de modo que só resta o tipo Error
no final.
Essa técnica é muito comum em JavaScript e também é uma forma de estreitamento. O termo correto para o que temos feito é "análise do fluxo de controle" (Control Flow Analysis).
A análise do fluxo de controle é a análise do nosso código com base no seu alcance.
TypeScript sabe que não podemos alcançar a primeira instrução if, se value
não for falsy.
const formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// Se for falso (null, undefined, string vazia), retorne "Unknown" com o prefixo
if (!value) {
return prefix + 'Unknown';
}
// Se for uma string, retorna a string com o prefixo
// if (typeof value === 'string') {
// return prefix + value
// }
// Se for um Warning, retorne o Warning.text com o prefixo
// if ('text' in value) {
// return prefix + value.text
// }
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message;
};
interface Warning {
text: string;
}
TypeScriptconst formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// Se for falso (null, undefined, string vazia), retorne "Unknown" com o prefixo
if (!value) {
return prefix + 'Unknown';
}
// Se for uma string, retorna a string com o prefixo
// if (typeof value === 'string') {
// return prefix + value
// }
// Se for um Warning, retorne o Warning.text com o prefixo
// if ('text' in value) {
// return prefix + value.text
// }
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message;
};
interface Warning {
text: string;
}
Não podemos alcançar o segundo if, se value
não for uma string
.
const formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// Se for falso (null, undefined, string vazia), retorne "Unknown" com o prefixo
if (!value) {
return prefix + 'Unknown';
}
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value;
}
// Se for um Warning, retorne o Warning.text com o prefixo
// if ('text' in value) {
// return prefix + value.text
// }
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message;
};
interface Warning {
text: string;
}
TypeScriptconst formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// Se for falso (null, undefined, string vazia), retorne "Unknown" com o prefixo
if (!value) {
return prefix + 'Unknown';
}
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value;
}
// Se for um Warning, retorne o Warning.text com o prefixo
// if ('text' in value) {
// return prefix + value.text
// }
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message;
};
interface Warning {
text: string;
}
Não podemos alcançar o terceiro, se não for um Warning
. Então, no final, só resta um tipo: só pode ser um Error
.
const formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// Se for falso (null, undefined, string vazia), retorne "Unknown" com o prefixo
if (!value) {
return prefix + 'Unknown';
}
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value;
}
// Se for um Warning, retorne o Warning.text com o prefixo
if ('text' in value) {
return prefix + value.text;
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message;
};
interface Warning {
text: string;
}
TypeScriptconst formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// Se for falso (null, undefined, string vazia), retorne "Unknown" com o prefixo
if (!value) {
return prefix + 'Unknown';
}
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value;
}
// Se for um Warning, retorne o Warning.text com o prefixo
if ('text' in value) {
return prefix + value.text;
}
// Se for um erro, retorne o Error.message com o prefixo
return prefix + value.message;
};
interface Warning {
text: string;
}
Esses tipos estão sendo estreitados porque o TypeScript está usando a análise do fluxo de controle.
O Operator Guard instanceof
Mas não precisamos depender da análise do fluxo de controle para estreitar o nosso tipo a Error
. Podemos fazer isso com um operador muito simples e idiomático do JavaScript. O operador instanceof
.
const formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// Se for falso (null, undefined, string vazia), retorne "Unknown" com o prefixo
if (!value) {
return prefix + 'Unknown';
}
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value;
}
// Se for um Warning, retorne o Warning.text com o prefixo
if ('text' in value) {
return prefix + value.text;
}
// Se for um erro, retorne o Error.message com o prefixo
if (value instanceof Error) {
return prefix + value.message;
}
// Nós nunca chegaremos aqui
throw new Error(`Invalid value type`);
};
interface Warning {
text: string;
}
TypeScriptconst formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// Se for falso (null, undefined, string vazia), retorne "Unknown" com o prefixo
if (!value) {
return prefix + 'Unknown';
}
// Se for uma string, retorna a string com o prefixo
if (typeof value === 'string') {
return prefix + value;
}
// Se for um Warning, retorne o Warning.text com o prefixo
if ('text' in value) {
return prefix + value.text;
}
// Se for um erro, retorne o Error.message com o prefixo
if (value instanceof Error) {
return prefix + value.message;
}
// Nós nunca chegaremos aqui
throw new Error(`Invalid value type`);
};
interface Warning {
text: string;
}
Aqui estamos verificando se o valor
é uma instância da classe Error
, portanto, o TypeScript estreita nosso tipo a Error
. Não sobrou nenhum tipo após a última instrução if, nunca alcançaremos nenhum código que venha depois disso.
Tipo never
(15s)
Se você está se perguntando o que o TypeScript considera ser o tipo value
depois de todos os nossos ifs, a resposta é never
.
never
(nunca) é um tipo especial que representa algo que é impossível,
algo que nunca deveria acontecer.
Conclusão
Esses foram os type guards fundamentais, eles são super úteis, mas só vão te levar até certo ponto. Nos próximos artigos, mostrarei como criar type guards customizados.
As referências estão abaixo.
E se sua empresa está precisando de desenvolvedores web remotos, você pode entrar em contato comigo e com a minha equipe em lucaspaganini.com.
Como sempre, tenha um ótimo dia e nos vemos em breve!
Conteúdo Relacionado
- 1min JS - Falsy and Truthy
- Null vs Undefined in JavaScript - Explained Visually
- TypeScript Narrowing Part 1 - What is a Type Guard