Custom Type Guards - TypeScript Narrowing #3
TypeScript Narrowing #3
Hey, welcome to another article in our TypeScript Narrowing series. In this article, I'll explain:
- Type predicates
 - How to create your own guards
 - How to create a guard by exclusion
 
This is the third article in our series, if you haven't watched the previous ones, I highly recommend that you do, they lay out solid fundamentals for narrowing. I'll leave a link for them in the references.
I'm Lucas Paganini, and on this site, we release web development tutorials. If that's something you're interested in, leave a like and 
 Type Predicates
In the last article, we explored the fundamental type guard operators. Now I'd like to show you type guard functions.
For example, if you need to check if a variable called value is a string, you could use the typeof operator. But what you could also do, is to create a function called isString() that receives an argument and returns true if the given argument is a string.
const isString = (value: any): boolean => typeof value === 'string';TypeScriptconst isString = (value: any): boolean => typeof value === 'string';Remember our formatErrorMessage() function from the last article?
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;
}Let's remove the typeof operator from it and use isString() instead.
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;
}It's the same code, we've just isolated the guard in a function, right? No. It breaks. TypeScript is not narrowing the type to string, the guard is not working.
Here's the thing, isString() is returning a boolean and we know what that boolean means.
const isString = (value: any): boolean => typeof value === 'string';TypeScriptconst isString = (value: any): boolean => typeof value === 'string';It means that the argument is a string. But TypeScript doesn't know what that boolean means, so let's teach it.
Instead of saying that our function returns a boolean, we need to say that our function returns the answer to the question: "is this argument a string?".
Given that the name of our argument is value, we do that with the following syntax: value is string.
const isString = (value: any): value is string => typeof value === 'string';TypeScriptconst isString = (value: any): value is string => typeof value === 'string';Now TypeScript understands that isString() is a type guard and our formatErrorMessage() function compiles correctly.
The return type of our isString() function is not just a boolean anymore, it's a "Type Predicate".
So to make a custom type guard, you just define a function that returns a type predicate.
All type predicates take the form of { parameter } is { Type }.
 The unknown Type
A quick tip before we continue:
Instead of using the type any in our custom guard parameter, our code would be safer if we used the unknown type.
const isString = (value: unknown): value is string => typeof value === 'string';TypeScriptconst isString = (value: unknown): value is string => typeof value === 'string';I made a one-minute video explaining the differences between any and unknown, the link is in the references.
 Custom Guards
Let's exercise our knowledge by converting all the checks in our formatErrorMessage() function to custom guards.
We already have a guard for strings, now we need guards for Warning, Error and falsy types.
Error Guard
The guard for Error is pretty straightforward, we just isolate the instanceof operator check in a function.
const isError = (value: unknown): value is Error => value instanceof Error;TypeScriptconst isError = (value: unknown): value is Error => value instanceof Error;Warning Guard
But the Warning guard, on the other hand, is not that simple.
TypeScript allowed us to use the in operator because there was a limited amount of types that our value parameter could be, and they were all objects.
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;
}But if we create a function and say that our parameter is unknown, then it could be anything. Including primitive types, and that would throw an Error because we can only use the in operator in objects.
interface Warning {
  text: string;
}
const isWarning = (value: unknown): value is Warning => 'text' in value; // Compilation errorTypeScriptinterface Warning {
  text: string;
}
const isWarning = (value: unknown): value is Warning => 'text' in value; // Compilation errorThe solution is to make sure our parameter is a valid object before using the in operator. And we also need to make sure that it's not 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
For the falsy values guard, we first need to define a type with values that are considered falsy.
type Falsy = false | 0 | -0 | 0n | '' | null | undefined;TypeScripttype Falsy = false | 0 | -0 | 0n | '' | null | undefined;I'm not including NaN here because there is no NaN type in TypeScript.
type Falsy = false | 0 | -0 | 0n | '' | null | undefined | ~~NaN~~;TypeScripttype Falsy = false | 0 | -0 | 0n | '' | null | undefined | ~~NaN~~;The type of NaN is number and not all numbers are falsy, so that's why we're not dealing with NaN.
typeof NaN;
//=> numberTypeScripttypeof NaN;
//=> numberThere is a proposal to add NaN as a type – and also integer, float and Infinity. I think that's nice, it would be helpful to have those types.
// Proposal
type number = integer | float | NaN | Infinity;TypeScript// Proposal
type number = integer | float | NaN | Infinity;I'll leave a link for the proposal in the references.
Anyway, now that we have our Falsy type, we can create a falsy values guard.
Remember, a value is falsy if it's considered false when converted to a boolean. So, to check if our value is falsy, we can use abstract equality to see if it gets converted to 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() with Custom Guards
That's it, we now have all the custom guards that we need for our formatErrorMessage() function.
// 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 by Exclusion
Before we wrap this up, I want to show you something.
There is a limited list of falsy values, right?
1. `false`
2. `0` `-0` `0n` representations of zero
3. ```` `""` `''` empty string
4. `null`
5. `undefined`
6. `NaN` not a numbermarkdown1. `false`
2. `0` `-0` `0n` representations of zero
3. ```` `""` `''` empty string
4. `null`
5. `undefined`
6. `NaN` not a numberBut truthy values, on the other hand, are infinity. All values that are not falsy, are truthy.
So, how would make a type guard for truthy values?
Truthy Guard
The trick is to exclude the falsy types.
Instead of checking if our value is truthy, we check that it's _not_ 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`
}I use this trick a lot, and we'll see it again in future articles.
 Conclusion
References and other links are below.
If you haven't already, please like, 
And if your company is looking for remote web developers, I and my team are currently available for new projects. You can contact us on lucaspaganini.com.
Have a great day, and I'll see you soon.
 Related content
- 1min JS - Falsy and Truthy
 - 1min TS - Unknown vs Any
 - TypeScript Narrowing Series
 - TypeScript Narrowing Part 1 - What is a Type Guard
 - TypeScript Narrowing Part 2 - Type Guard Operators