How To Do Anything in TypeScript With Type Guards
Cameron McHenry
Posted on November 3, 2021
This article was originally published on my blog: camchenry.com. If you enjoy this article, please consider joining my mailing list for more content like this one.
TypeScript is valuable because it enables us to write safe code. Because when every type in the code is known at compile time, we can compile the code with TypeScript and perform type checking, which ensures that the code will not crash or cause errors.
However, it is not always possible to know every type at compile time, such as when accepting arbitrary data from an external API. To check types at run-time or differentiate between different types, we to need narrow the types using a type guard.
What is narrowing?
In TypeScript, narrowing is the process of refining broad types into more narrow types. Narrowing is useful because it allows code to be liberal in the types that it accepts. Then, we can use type guards to narrow the type down to something more useful.
These are some common examples of narrowing:
-
unknown
orany
tostring
-
string | object | number
tostring
-
number | null | undefined
tonumber
-
string
to a custom type likeNonEmptyString
What is a type guard?
A type guard is a kind of conditional check that narrows a type. Type guards allow for run-time type checking by using expressions to see if a value is of a certain type or not.
So, what does a type guard look like? These are all examples of type guards:
typeof value === 'string'
'name' in data
value instanceof MouseEvent
!value
A type guard is a special kind of expression that changes the type of a variable. We will look at more examples of type guards in practice later.
The kinds of type guards (how to check a type)
Most type guards revolve around regular JavaScript operators, which are given extra abilities in TypeScript that make it possible to narrow types by writing typical JavaScript code. So, it is possible that you've used a type guard before without even realizing it!
Fundamentally, every type guard relies on checking that some expression evaluates to true or false.
As a result, the first kind of type guard which we will look at is a simple truthiness check. But we can utilize more complex type guards like in
, typeof
, and instanceof
that tell us much more information.
In addition to all of these built-in type guards, we can can go even further and create our own custom type guards that can check any type.
Boolean type guard (truthiness)
As stated previously, checking the truthiness of a value is the essence of all type guards.
However, a boolean type guard only checks the truthiness of a value, but gives us no additional information beyond that. Other more complex type guards can check more complex types or verify more properties, but the boolean type guard is the most basic type guard.
function getAvailableRooms(rooms: number | undefined) {
if (rooms) {
return `There are ${rooms} hotel rooms available to book.`;
}
return "Sorry, all rooms are currently booked.";
}
getAvailableRooms(undefined); // "Sorry, all rooms are currently booked."
getAvailableRooms(5); // "There are 5 hotel rooms available to book."
When using a boolean type guard, the value is implicitly casted to a boolean. This has a logical interpretation most of the time, but not always.
For example, if use a boolean type guard to check a type of number | undefined
, we might expect that it will only exclude the undefined
case. However, it will also rule out the case where the value is 0, which might not be what you expect in some cases. For more information on this common bug, check out Kent C. Dodd's article, "Use ternaries rather than && in JSX."
Equality type guard
In the boolean type guard, we checked the truthiness of an expression. In an equality type guard, we check the value of an expression.
This kind of type guard is useful when we know all of the possible values of a type. For example, if we have an enumeration of string or number values, or if we want to know that a value is not null
or undefined
.
Here is an example where we use an equality type guard to remove undefined
from the type of a variable:
function getGreeting(timeOfDay?: "morning" | "afternoon") {
if (timeOfDay === undefined) {
return `Hello!`;
}
// Now the type of `timeOfDay` is narrowed to `morning` | `afternoon`,
// so we can use string methods on it safely.
return `Good ${timeOfDay[0].toUpperCase()}${timeOfDay.slice(1)}!`;
}
getGreeting(); // "Hello!"
getGreeting("afternoon"); // "Good Afternoon!"
getGreeting("morning"); // "Good Morning!"
We can also use a switch
block to accomplish exactly the same thing:
function getGreeting(timeOfDay?: "morning" | "afternoon") {
switch (timeOfDay) {
case "afternoon":
case "morning":
return `Good ${timeOfDay[0].toUpperCase()}${timeOfDay.slice(1)}!`;
default:
return `Hello!`;
}
}
Using a switch
block like this might be preferable if you have a lot of possible values to check and which might share the same code.
Discriminated unions deserve their own article, but they are essentially a more powerful version of the equality type guard.
A discriminated union is a type that has multiple possible types, with a field that allows to us to discriminate (or differentiate) between them. In other words, when we check the value of a single field (like type
), it automatically includes a number of other properties.
typeof
type guard
In contrast to the previous example, where we checked the value of a variable (or expression), with a typeof
type guard, we check the type of a variable.
When there is a value which has several possible types, like string | number
, we can use typeof
to figure out which type it is.
For example, we can use typeof
to write a comparison function that compares two values to each other and returns the difference:
function compare(a: number | string, b: number | string): number {
if (typeof a === "number" && typeof b === "number") {
// Both a and b are numbers, so we can compare them directly.
return a - b;
}
if (typeof a === "string" && typeof b === "string") {
// We can use string methods on `a` and `b` safely.
return a.localeCompare(b);
}
throw new Error(
`Cannot compare unrelated types '${typeof a}' and '${typeof b}'`
);
}
compare("a", "b"); // => -1
compare("b", "a"); // => 1
compare(123, 321); // => -198
The biggest limitation of the typeof
guard is that it can only differentiate between types that JavaScript recognizes. The types that typeof
can check are:
boolean
string
number
bigint
object
symbol
function
undefined
instanceof
type guard
When we have a variable that is an instance of a class, we can use instanceof
to check whether if the variable has that type or not.
For example, the DOM APIs define many classes and subclasses which can be quickly checked using instanceof
:
function handleEvent(event: Event) {
if (event instanceof MouseEvent) {
// `event` now has type `MouseEvent`, so we can access mouse-specific properties
console.log(`A mouse event occurred at (${event.x}, ${event.y}`);
}
if (event instanceof KeyboardEvent) {
// `event` now has type `KeyboardEvent`, so we can access key-specific properties
console.log(`A keyboard event occurred: ${event.key} ${event.}`);
}
console.log("An event occurred: ", event.type);
}
This is useful when dealing with potentially generic DOM objects, because a single instanceof
check grants access to all of the properties and methods of the class.
This can also be used to differentiate between common objects in JavaScript, like Map
, Date
, Array
, or Set
. For example, we can create a function to create a lookup table which accepts many possible inputs:
// Creates a Map which returns some value given a string key
// (ignoring the fact that the Map constructor already accepts some of these)
function createLookupTable<Value>(
db: [string, Value][] | Map<string, Value> | Record<string, Value>
): Map<string, Value> {
// `db` has type `[string, Value][] | Map<string, Value> | Record<string, Value>`
if (db instanceof Array) {
// `db` now has type `[string, Value][]`
return new Map(db);
}
// `db` has type `Map<string, Value> | Record<string, Value>`
if (db instanceof Map) {
// `db` now has type `Map<string, Value>`
return db;
}
// `db` has type `Record<string, Value>`
return new Map(Object.entries(db));
}
createLookupTable([
["hat", 14.99],
["shirt", 24.95],
]);
// => Map (2) {"hat" => 14.99, "shirt" => 24.95}
createLookupTable(
new Map([
["hat", 14.99],
["shirt", 24.95],
])
);
// => Map (2) {"hat" => 14.99, "shirt" => 24.95}
createLookupTable({ hat: 14.99, shirt: 24.95 });
// => Map (2) {"hat" => 14.99, "shirt" => 24.95}
Here is another example using instanceof
to check if a type is a Date
or a string
and decide whether to construct a new Date
object or not:
function getDate(value: string | Date): Date {
if (value instanceof Date) {
return value;
}
return new Date(value);
}
getDate("2021-05-06 03:25:00");
// => Date: "2021-05-06T07:25:00.000Z"
getDate(new Date("2021-05-06 03:25:00"));
// => Date: "2021-05-06T07:25:00.000Z"
in
type guard
The in
type guard allows us to differentiate between multiple types by checking if an object has a specific property. In JavaScript, the in
operator, like all type guards, returns a boolean value that indicates if the object has the property or not. For example,
"data" in { name: "test", data: { color: "blue" } }; // => true
"data" in { name: "test", data: undefined }; // => true
"data" in { name: "test" }; // => false
In this way, we can use in
to differentiate objects that have different sets of properties. For example, we can use it to differentiate between different types of classes (in this case, events):
function handleEvent(event: MouseEvent | KeyboardEvent) {
if ("key" in event) {
// event now has type `KeyboardEvent`
console.log(`A keyboard event occurred: ${event.key}`);
} else {
// event now has type `MouseEvent`
console.log(`A mouse event occurred: ${event.button}`);
}
}
The important thing here is that key
is only defined for KeyboardEvent
, but not for MouseEvent
. If the property we check exists in multiple cases, the narrowing will not work. For example, the following code will not work:
type EventInput =
| { type: "mouse"; button: string }
| { type: "key"; key: string };
function handleEventInput(event: EventInput) {
// This type guard will NOT work:
if ("type" in event) {
// event still has type `EventInput`, so the type guard does not
// do any narrowing in this case
}
}
Though not always related to its use for narrowing types, the in
operator is also often used to check for browser support of certain features.
For example, the guard 'serviceWorker' in navigator
checks whether the browser supports service workers.
Assertion type guard (or assertion function)
In TypeScript 3.7, TypeScript added support for assertion functions. An assertion function is a function that assumes a condition is always true, and throws an error when it does not.
To create an assertion function, we need to add something called an "assertion signature," which is a formal declaration of what the function will assert. The assertion signature is additional information about a function (like a return type) that lets the TypeScript compiler narrow the type.
Let's look at an example:
function assertString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new TypeError(`Expected 'string', got: '${typeof value}'`);
}
}
const x = "123";
assertString(x);
// x now has type 'string', so it is safe to use string methods
x.toLowerCase();
Previously, we discussed how all type guards are based around a boolean check. That is still true in this case, but the actual usage is slightly different from other type guards.
With other type guards, we typically used something like if
or switch
to create different branches of execution. With an assertion function, the two branches are: continue as normal, or stop the script (throw an error).
Other than the difference of how an assertion type guard can throw an exception, assertion type guards are similar to other type guards. However, something that we must be careful about is accidentally creating a type guard which asserts the wrong condition.
This is one way that we can end up with a false sense of safety. Here is an example where the function asserts something, but the actual code asserts nothing.
function assertString(value: unknown): asserts value is string {
// This check does not match the assertion signature
if (typeof value === "boolean") {
throw new TypeError();
}
}
const x: unknown = 123;
assertString(x);
// We get a run-time exception here (!!!), which TypeScript should
// be able to prevent under normal circumstances:
x.toLowerCase();
// "TypeError: x.toLowerCase is not a function"
User-defined (custom) type guard
Most type guards have limitations to what they can check, such as only primitive types for typeof
, or only classes for instanceof
. But with user-defined type guards, there are no limitations on what we can check.
Custom type guards are the most powerful kind of type guard, because we can verify any type, including ones that we defined ourselves, as well as built-in types from JavaScript or the DOM. The main downside of custom type guards is that they are not predefined, so we have to write them ourselves.
There are a few built-in custom type guards though, such as Array.isArray
:
const data: unknown = ["a", "b", 123, false];
if (Array.isArray(data)) {
// data now has type "array", so it is safe to use array methods
data.sort();
}
In the next section, we will look at all of the different ways that we can define our own type guard functions.
Type guard functions
A type guard function is a function that returns a value and has a type predicate.
A type predicate is an additional declaration that is added to a function (like a return type) which gives additional information to TypeScript and allows it to narrow the type of a variable. For example, in the definition of Array.isArray
,
function isArray(arg: any): arg is any[];
the type predicate is arg is any[]
. In spoken word, the signature of this function might be: "isArray
takes one argument of type any
and checks if it is an array." In general, type predicates take the form: variable is type
.
For a function to be eligible as a type guard, it must:
- Return a boolean value
- Have a type predicate
The type predicate replaces the return type, because a function with a type predicate must always return a boolean value.
Examples of type guard functions
Check if a value is a string
This example is essentially a reusable form of the built-in typeof
type guard.
function isString(value: unknown): value is string {
return typeof value === "string";
}
Check if a value is defined (not null or undefined)
A common use case for type guards is to refine the type of something like Type | null
or Type | undefined
down to just Type
, effectively eliminating the null or undefined case. We can do this by accepting a generic type which can be null or undefined, and adding a type predicate to remove null | undefined
from the type.
function isDefined<Value>(value: Value | undefined | null): value is Value {
return value !== null && value !== undefined;
}
Then, it can be used like this:
const x: string | undefined = 123;
if (isDefined(x)) {
// x is defined, so it is safe to use methods on x
x.toLowerCase();
}
Remove all values null
or undefined
values from array
Using the isDefined
type guard we just defined, we can use it with the built-in Array.filter
function, which has special support for type predicates. The Array.filter
function is defined like:
function filter<Filtered extends Item>(
predicate: (value: Item, index: number, array: Item[]) => value is Filtered
): Filtered[];
(The definition here has been altered slightly for improved understanding and readability). Essentially, every usage of Array.filter
is a type guard, except in most cases the type before and after calling Array.filter
is the same type.
But if the function passed to Array.filter
narrows the type (like a type guard), then the return type of Array.filter
changes. So we can use our isDefined
type guard to remove all null
and undefined
values from the array, as well as removing null
and undefined
types from the array items.
// 'values' is an array of strings, but can have null or undefined values
const values: (string | null | undefined)[] = [null, "a", "b", undefined];
// We can safely assign 'filtered' to an array of strings (string[])
// because `isDefined` changes the type of the variable 'values'
const filtered: string[] = values.filter(isDefined);
Check if a number is positive
A common use-case for creating our own types is so that we can ensure certain conditions are met. For example, we might want to ensure that an object has certain properties, a string is not empty, or a number is positive.
First, we need to create a custom PositiveNumber
type, and a type guard to check for it.
type PositiveNumber = number & { __type: "PositiveNumber" };
function isPositive(n: number): n is PositiveNumber {
return n >= 0;
}
To create a new type of number, we use a technique called "type branding." Essentially, we add a phantom property to the number type to differentiate it from all other types of numbers. In this case, I chose to use { __type: 'PositiveNumber' }
, but we could picked any arbitrary key/value, as long as it is unique and not already defined.
The important thing is that we cannot create PositiveNumber
by declaring a variable:
const x: PositiveNumber = 49;
// ERROR: Type 'number' is not assignable to type 'PositiveNumber
This may seem inconvenient, but it is exactly why it allows us to write safe code, because we must always check conditions with the type guard and prevents us from writing code like this:
const x: PositiveNumber = -100;
As an example of how we might use this type guard, we can write a square root function which accepts only positive numbers:
function squareRoot(n: PositiveNumber): PositiveNumber {
return Math.sqrt(n) as PositiveNumber;
}
Then, we can use the type guard to compute the square root:
const x = 49;
squareRoot(x);
// ERROR: ^^^ 'number' is not assignable to parameter of type 'PositiveNumber'
if (isPositive(x)) {
// OK: Now x has type 'PositiveNumber', so we can take the square root
squareRoot(x);
}
Check if a string is a GUID
Similar to the previous example, we can create a custom Guid
type that is based on the string
type and write a type guard to check for it.
type Guid = string & { __type: "Guid" };
const guidPattern =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
function isGuid(value: string): value is Guid {
return guidPattern.test(value);
}
As an example of how to use this type and type guard in practice, we will create a list of users that can be searched by GUID.
type User = {
id: Guid;
name: string;
};
const users: User[] = [
/* ... */
];
function getUserById(id: Guid) {
return users.find((user) => user.id === id);
}
const id = "abc123";
getUserById(id);
// ERROR: ^^ Argument of type 'string' is not assignable to parameter of type 'Guid'
if (isGuid(id)) {
// OK: id now has type `Guid`:
getUserById(id);
}
Check if a value is a valid React element (React.isValidElement
)
The isValidElement
function included with React checks if a value is a valid React element, which can be rendered by React.
function isValidElement<P>(
object: {} | null | undefined
): object is ReactElement<P>;
The implementation of this function is not relevant here, but it is a perfect example of a common type guard function that verifies a custom type that cannot be verified with other type guards.
Pros and cons of custom type guard functions
Custom type guard functions are powerful and sometimes be the only option in order to write type-safe code. However, they can be a tricky to write and are susceptible to mistakes.
The advantages of custom type guard functions are:
- Flexibility: can check any type, including custom types that we define
- Run-time type checking: allows type-checking at run-time, ensuring that safety is ensured both when code is compiled, and also when it is running
- Reusable: type guard functions allow us to combine multiple type guards into one and easily use them in multiple places
The disadvantages of a custom type guard function are:
- Manual: type guard functions have to be written manually (currently no automatic way to generate type guards)
- Performance: using type guard functions has a slight overhead to call the function and run the checks (negligible in practice)
- Fragile: custom type guards can be implemented incorrectly on accident, which may provide a false sense of security and safety
Where can a type guard be used?
Now that we know all about the available type guards, we will briefly look at where we can use type guards. There are a limited number of places that type guards can be used. The most common place they are used is in a if/else
block, like this:
if (typeof value === "string") {
// value has type 'string' in this block
} else {
// value does NOT have type 'string' in this block
}
Since we can use type guards in an if/else
block, then you might expect that we can also use them with the ternary operator, since it's a shorthand for an if/else
block. And you would be correct!
typeof value === 'string'
? /* value has type 'string' in this block */
: /* value does NOT have type 'string' in this block */
In addition, since TypeScript 4.4, we can use type guards with aliased conditions.
const isString = typeof value === "string";
if (isString) {
// value has type 'string' in this block
} else {
// value does NOT have type 'string' in this block
}
Beyond just if/else
, type guards can also be used in a while
block:
while (typeof value === "string") {
// value has type 'string' in this block
}
Finally, type guards are also compatible with a switch/case
block:
switch (typeof value) {
case "string":
// value has type 'string' in this block
break;
}
Conclusion
Type guards are conditional checks that allow types to be refined from one type to another, allowing us to write code that is type-safe and easy to write at the same time. Since TypeScript is a superset of JavaScript, many common operators like typeof
or instanceof
act as type guards. But, we can also use custom type guards to verify any condition and any type, given enough effort.
Summary
In general, I would recommend using the type guard that feels the most natural, which will come from experience. Don't write a custom type guard function when a simple typeof
check can suffice. However, it may be necessary to write a custom type guard.
To summarize the strengths of each type guard, here is a summary table.
Type guard | Usage |
---|---|
Boolean / truthiness | Rule out falsy values like null , undefined , '' , 0 , etc. |
Equality | Narrow multiple possible types down to a single type |
typeof |
Narrow a type to a primitive type (like string or number ) |
instanceof |
Check if a value is an instance of a specific class |
in |
Check if a property can be accessed |
Assertion function | Assert invariants that should always be true |
Custom type guard function | Check that a type meets some arbitrary conditions |
If this article was helpful, let me know on Twitter at @cammchenry! If you enjoy guides like this, consider signing up for my mailing list to be notified when new posts are published.
Good luck, and happy coding!
Posted on November 3, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 17, 2023