Type Guards Personalizados- TypeScript Narrowing #3
TypeScript Narrowing #3
Ei! Bem-vindo a outro artigo na nossa série de Narrowing em TypeScript. Neste artigo, vou explicar:
- Type predicates
- Como criar seus próprios guards
- Como criar um guard por exclusão
Este é o terceiro artigo da nossa série, se você não assistiu os anteriores, eu recomendo fortemente que você leia, eles abordam fundamentos sólidos para narrowing. Vou deixar um link para eles nas referências.
Sou Lucas Paganini, e neste site, lançamos tutoriais de desenvolvimento web. Se você está interessado nisso, deixe um like e
Type Predicates
No último artigo, exploramos os operadores type guard fundamentais. Agora, eu gostaria de te mostrar funções de type guard.
Por exemplo, se você precisa verificar se uma variável chamada value
é uma string
, você pode usar o operador typeof
. Mas o que você também pode fazer, é criar uma função chamada isString()
que recebe um argumento e retorna true
se o argumento fornecido for uma string
.
const isString = (value: any): boolean => typeof value === 'string';
TypeScriptconst isString = (value: any): boolean => typeof value === 'string';
Você se lembra da nossa função formatErrorMessage()
do último artigo?
const formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
if (!value) {
return prefix + 'Unknown';
}
// If it's a string, return the string with the prefix
if (typeof value === 'string') {
return prefix + value;
}
// If it's a Warning, return the Warning.text with the prefix
if ('text' in value) {
return prefix + value.text;
}
// If it's an Error, return the Error.message with the prefix
if (value instanceof Error) {
return prefix + value.message;
}
// We will never reach here
throw new Error(`Invalid value type`);
};
interface Warning {
text: string;
}
TypeScriptconst formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
if (!value) {
return prefix + 'Unknown';
}
// If it's a string, return the string with the prefix
if (typeof value === 'string') {
return prefix + value;
}
// If it's a Warning, return the Warning.text with the prefix
if ('text' in value) {
return prefix + value.text;
}
// If it's an Error, return the Error.message with the prefix
if (value instanceof Error) {
return prefix + value.message;
}
// We will never reach here
throw new Error(`Invalid value type`);
};
interface Warning {
text: string;
}
Vamos remover o operador typeof
dela e usar isString()
no lugar.
const formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
if (!value) {
return prefix + 'Unknown';
}
// If it's a string, return the string with the prefix
if (isString(value)) {
return prefix + value;
}
// If it's a Warning, return the Warning.text with the prefix
if ('text' in value) {
return prefix + value.text;
}
// If it's an Error, return the Error.message with the prefix
if (value instanceof Error) {
return prefix + value.message;
}
// We will never reach here
throw new Error(`Invalid value type`);
};
interface Warning {
text: string;
}
TypeScriptconst formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
if (!value) {
return prefix + 'Unknown';
}
// If it's a string, return the string with the prefix
if (isString(value)) {
return prefix + value;
}
// If it's a Warning, return the Warning.text with the prefix
if ('text' in value) {
return prefix + value.text;
}
// If it's an Error, return the Error.message with the prefix
if (value instanceof Error) {
return prefix + value.message;
}
// We will never reach here
throw new Error(`Invalid value type`);
};
interface Warning {
text: string;
}
É o mesmo código, somente isolamos o guard em uma função, certo? Não. O código quebra. TypeScript não está estreitando o tipo para string
, o guard não está funcionando.
O problema é o seguinte: isString()
está retornando um boolean, e nós sabemos o que esse boolean
significa.
const isString = (value: any): boolean => typeof value === 'string';
TypeScriptconst isString = (value: any): boolean => typeof value === 'string';
Significa que o argumento é uma string
. Mas o TypeScript não sabe o que esse boolean
significa, então precisamos informá-lo.
Em vez de dizer que nossa função retorna um boolean
, precisamos dizer que nossa função retorna a resposta à pergunta: "este argumento é uma string
?".
Dado que o nome do nosso argumento é value
, fazemos isso com a seguinte sintaxe: value is string
.
const isString = (value: any): value is string => typeof value === 'string';
TypeScriptconst isString = (value: any): value is string => typeof value === 'string';
Agora o TypeScript entende que isString()
é um type guard e nossa função formatErrorMessage()
compila corretamente.
O retorno da nossa função isString()
não é apenas um boolean
, é um "Type Predicate".
Então, para fazer um type guard personalizado, basta criarmos uma função que retorna um type predicate.
Todos os type predicates assumem a forma de { parameter } é { Type }
.
O Tipo unknown
Uma dica rápida antes de continuarmos:
Em vez de usar o tipo any
em nosso type guard personalizado, nosso código seria mais seguro se usássemos o tipo unknown
.
const isString = (value: unknown): value is string => typeof value === 'string';
TypeScriptconst isString = (value: unknown): value is string => typeof value === 'string';
Fiz um vídeo de um minuto explicando as diferenças entre any
e unknown
, o link está nas referências
Guards personalizados
Vamos exercitar o nosso conhecimento convertendo todas as verificações em nossa função formatErrorMessage()
para type guards personalizados.
Já temos uma guard para strings
, agora precisamos de guards para Warning
, Error
e Falsy.
Error
Guard
O guard para Error
é bem simples, apenas isolamos a verificação com o instanceof
em uma função.
const isError = (value: unknown): value is Error => value instanceof Error;
TypeScriptconst isError = (value: unknown): value is Error => value instanceof Error;
Warning
Guard
Mas o guard para Warning
, por outro lado, não é tão simples.
TypeScript nos permitiu usar o operador in
porque havia uma quantidade limitada de tipos que o nosso parâmetro value
poderia ser, e todos eram objetos.
const formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
if (!value) {
return prefix + 'Unknown';
}
// If it's a string, return the string with the prefix
if (isString(value)) {
return prefix + value;
}
// If it's a Warning, return the Warning.text with the prefix
if ('text' in value) {
return prefix + value.text;
}
// If it's an Error, return the Error.message with the prefix
if (isError(value)) {
return prefix + value.message;
}
// We will never reach here
throw new Error(`Invalid value type`);
};
interface Warning {
text: string;
}
TypeScriptconst formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
if (!value) {
return prefix + 'Unknown';
}
// If it's a string, return the string with the prefix
if (isString(value)) {
return prefix + value;
}
// If it's a Warning, return the Warning.text with the prefix
if ('text' in value) {
return prefix + value.text;
}
// If it's an Error, return the Error.message with the prefix
if (isError(value)) {
return prefix + value.message;
}
// We will never reach here
throw new Error(`Invalid value type`);
};
interface Warning {
text: string;
}
Mas se criarmos uma função e dissermos que nosso parâmetro é unknown
, então ele pode ser qualquer coisa, incluindo tipos primitivos. E isso causaria um Error
porque só podemos usar o operador in
em objetos.
interface Warning {
text: string;
}
const isWarning = (value: unknown): value is Warning => 'text' in value; // Compilation error
TypeScriptinterface Warning {
text: string;
}
const isWarning = (value: unknown): value is Warning => 'text' in value; // Compilation error
A solução é garantir que o nosso parâmetro seja um objeto válido antes de usar o operador in
. E também precisamos ter certeza de que ele não é null
.
interface Warning {
text: string;
}
const isWarning = (value: unknown): value is Warning =>
typeof value === 'object' && value !== null && 'text' in value;
TypeScriptinterface Warning {
text: string;
}
const isWarning = (value: unknown): value is Warning =>
typeof value === 'object' && value !== null && 'text' in value;
Falsy Guard
Para o guard de valores falsy, primeiro precisamos definir um tipo com valores considerados falsy.
type Falsy = false | 0 | -0 | 0n | '' | null | undefined;
TypeScripttype Falsy = false | 0 | -0 | 0n | '' | null | undefined;
Não estou incluindo NaN
aqui porque não há um tipo NaN
em TypeScript.
type Falsy = false | 0 | -0 | 0n | '' | null | undefined | ~~NaN~~;
TypeScripttype Falsy = false | 0 | -0 | 0n | '' | null | undefined | ~~NaN~~;
NaN
é do tipo number
e nem todos os números são falsy, por isso não estamos lidando com NaN
.
typeof NaN;
//=> number
TypeScripttypeof NaN;
//=> number
Existe uma proposta para adicionar NaN
como um tipo e também integer
, float
e Infinity
. Eu considero uma boa proposta, seria muito útil ter esses tipos disponíveis.
// Proposal
type number = integer | float | NaN | Infinity;
TypeScript// Proposal
type number = integer | float | NaN | Infinity;
Vou deixar um link para a proposta nas referências.
De qualquer forma, agora que temos nosso tipo Falsy
, podemos criar um guard para valores falsy.
Lembre-se, um valor é falsy se for considerado false
quando convertido para boolean
. Então, para verificar se nosso valor é falsy, podemos usar igualdade abstrata para ver se ele é convertida para false
.
type Falsy = false | 0 | -0 | 0n | '' | null | undefined;
const isFalsy = (value: unknown): value is Falsy => value == false;
TypeScripttype Falsy = false | 0 | -0 | 0n | '' | null | undefined;
const isFalsy = (value: unknown): value is Falsy => value == false;
formatErrorMessage()
com Guards Personalizados
É isso, agora temos todos os guards personalizados que precisamos para nossa função formatErrorMessage()
// FUNCTION
const formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
if (isFalsy(value)) {
return prefix + 'Unknown';
}
// If it's a string, return the string with the prefix
if (isString(value)) {
return prefix + value;
}
// If it's a Warning, return the Warning.text with the prefix
if (isWarning(value)) {
return prefix + value.text;
}
// If it's an Error, return the Error.message with the prefix
if (isError(value)) {
return prefix + value.message;
}
// We will never reach here
throw new Error(`Invalid value type`);
};
TypeScript// FUNCTION
const formatErrorMessage = (
value: null | undefined | string | Error | Warning
): string => {
const prefix = 'Error: ';
// If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
if (isFalsy(value)) {
return prefix + 'Unknown';
}
// If it's a string, return the string with the prefix
if (isString(value)) {
return prefix + value;
}
// If it's a Warning, return the Warning.text with the prefix
if (isWarning(value)) {
return prefix + value.text;
}
// If it's an Error, return the Error.message with the prefix
if (isError(value)) {
return prefix + value.message;
}
// We will never reach here
throw new Error(`Invalid value type`);
};
// GUARDS
const isString = (value: unknown): value is string => typeof value === 'string';
const isError = (value: unknown): value is Error => value instanceof Error;
interface Warning {
text: string;
}
const isWarning = (value: unknown): value is Warning =>
typeof value === 'object' && value !== null && 'text' in value;
type Falsy = false | 0 | -0 | 0n | '' | null | undefined;
const isFalsy = (value: unknown): value is Falsy => value == false;
TypeScript// GUARDS
const isString = (value: unknown): value is string => typeof value === 'string';
const isError = (value: unknown): value is Error => value instanceof Error;
interface Warning {
text: string;
}
const isWarning = (value: unknown): value is Warning =>
typeof value === 'object' && value !== null && 'text' in value;
type Falsy = false | 0 | -0 | 0n | '' | null | undefined;
const isFalsy = (value: unknown): value is Falsy => value == false;
BONUS: Narrowing por Exclusão
Antes de encerrarmos, eu quero te mostrar uma coisa.
Existe uma lista limitada de valores falsy, certo?
1. `false`
2. `0` `-0` `0n` representações de zero
3. ```` `""` `''` string vazio
4. `null`
5. `undefined`
6. `NaN` not a number
markdown1. `false`
2. `0` `-0` `0n` representações de zero
3. ```` `""` `''` string vazio
4. `null`
5. `undefined`
6. `NaN` not a number
Mas os valores truthy, por outro lado, são infinitos. Todos os valores que não são falsy são truthy.
Então, como faríamos um guard para valores truthy?
Truthy Guard
O truque é excluir os types falsy.
Ao invés de verificar se nosso valor é truthy, verificamos se _não_ é falsy.
type Truthy<T> = Exclude<T, Falsy>;
const isTruthy = <T extends unknown>(value: T): value is Truthy<T> =>
value == true;
// Test
const x = 'abc' as null | string | 0;
if (isTruthy(x)) {
x.trim(); // `x: string`
}
TypeScripttype Truthy<T> = Exclude<T, Falsy>;
const isTruthy = <T extends unknown>(value: T): value is Truthy<T> =>
value == true;
// Test
const x = 'abc' as null | string | 0;
if (isTruthy(x)) {
x.trim(); // `x: string`
}
Eu uso muito esse truque e veremos ele novamente em vídeos futuros.
Conclusão
Referências e outros links estão abaixo.
Se ainda não o fez, deixe seu like e inscreva-se. Isso nos ajuda a crescer, o que resulta em mais conteúdo gratuito para você. Todos ganham.
E se sua empresa está procurando desenvolvedores web remotos, eu e a minha equipe estamos disponíveis para novos projetos. Você pode entrar em contato conosco em lucaspaganini.com.
Tenha um ótimo dia e nos vemos em breve.
Conteúdo Relacionado
- 1min JS - Falsy and Truthy
- 1min TS - Unknown vs Any
- TypeScript Narrowing Series
- TypeScript Narrowing Parte 1 - O que é um Type Guard
- TypeScript Narrowing Parte 2 - Type Guard Operators