User-Defined Type Guards in TypeScript
The TypeScript documentation defines a Type Guard as
some expression that performs a runtime check that guarantees the type in some scope.
In practical terms, I think of a type guard as a function that tells TypeScript it is safe to assign a certain type to a value based on whether that function returns true of false. Type guards can help in situations you would otherwise have to resort to a type cast. Type guards are similar to a cast in the sense that the programmer is asserting they know something TypeScript can't deduce on its own, but they are safer since they come with a runtime check that should validate that the type assignment is safe.
An Example
Let's say we have two interfaces
interface A {
a: string;
b: number;
}
interface C {
c: boolean;
d: string;
}
and for some reason, we have a place in our code where the type of a value is unknown and we want to validate the data meets our expectation before passing the data along to the rest of our program. I find this situation often arises when I'm operating on data I don't have full control over, perhaps extracting data from a url or web storage, and I want to be really careful with it. Our goal is to verify that value
below is of type A or type C and then pass it to the appropriate function.
let value: unknown;
function callWithA(a: A) {}
function callWithC(c: C) {}
// Goal
if (isA(value)) {
callWithA(value);
} else if (isC(value)) {
callWithC(value);
}
isA
and isC
are the type guards that make this possible:
function isA(value: unknown): value is A {
if (typeof value !== "object" || !value) {
return false;
}
return (
typeof (value as Record<string, unknown>).a === "string" &&
typeof (value as Record<string, unknown>).b === "number"
);
}
function isC(value: unknown): value is C {
if (typeof value !== "object" || !value) {
return false;
}
return (
typeof (value as Record<string, unknown>).c === "boolean" &&
typeof (value as Record<string, unknown>).d === "string"
);
}
Notice the value is A
and value is C
as the return type annotation of these functions. This syntax is what tells TypeScript to assign the type to value
in blocks where isA
/isC
return true.
Another note, we are using a cast to Record<string, unknown>
in these type guards because the typeof value !== "object" || !value
refines value
from unknown
to object
. object
doesn't allow arbitrary property access, so the cast allows us to access fields a, b, c, and d. This cast isn't particularly unsafe, since we're just calling typeof on the property. I find it pretty typical to need to perform similar casts in type guards, since you are telling TypeScript something it otherwise could not figure out.
Filtering Arrays with Type Guards
Another place type guards can oftentimes come in handy is as the argument to the array filter function. Taking our interfaces and type guards from the above example,
const mixedArray: (A | C)[] = [];
// A[]
const arrayA = mixedArray.filter(isA);
// C[]
const arrayC = mixedArray.filter(isC);
One of the first places most people will probably encounter type guards is when trying to filter out null
/undefined
out of an array. Passing Boolean
or !!x
to the filter function is a very common pattern in JavaScript.
let arr: (A | null)[];
// (A | null)[]
let filteredArr1 = arr.filter(Boolean);
// (A | null)[]
let filteredArr2 = arr.filter((x) => !!x);
Unfortunately, this doesn't quite work in TypeScript, at least for now. The resulting array still is typed as having null values. Instead, a type guard can be used for this case.
export function isPresent<T>(t: T | undefined | null | void): t is T {
return t !== undefined && t !== null;
}
// A[]
let filteredArr3 = arr.filter(isPresent);