TypeScript Narrowing #3
See this and many other articles at lucaspaganini.com
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 subscribe to the newsletter.
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';
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;
}
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;
}
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';
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';
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';
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 string
s, 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;
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;
}
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 error
The 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;
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;
I'm not including NaN
here because there is no NaN
type in TypeScript.
type 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;
//=> number
There 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;
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;
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`);
};
// 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 number
`
But 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.
ts
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`
}
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, subscribe, and follow us on social media. This helps us grow, which results in more free content for you. It's a win-win.
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
Top comments (0)