(Photo by Daniel Jensen on Unsplash)
This article was first published on my blogπ.
TL;DR:
Either:
const isValidObject = (myObject as ValidObject).id !== undefined;
Or, better, define a type guard:
function isValidObject(myObject: ValidObject | {}): myObject is ValidObject {
return (myObject as ValidObject).id !== undefined;
}
I'm publishing this tip mainly because it's the third time I run into this problem, and the third time I get lost on the internet trying to understand what it is I'm doing wrong. I hope next time I search for this, this post will come up! Read on if you want a better understandind of what the cheat-sheet code above does and where it comes from:
When writing regular JavaScript we are used to a certain degree of flexibility when it comes to objects. Take the following example:
// Imaginary call to an API that returns the venue with ID 1,
// or an empty object if there is no venue with that ID
const venue = getVenue(1);
// Let's check if a venue was found by verifying the existence of the `id` property
const weHaveVenue = venue.id !== undefined;
if (weHaveVenue) {
// do something
} else {
// do something else...
}
Pretty straightforward, right?
Well, the moment we use TypeScript, things don't work so smoothly anymore. Have a look a this implementation:
// Let's define the type of our imaginary API function first
type GetVenue = (
id: number
) => { id: number; name: string; location: string } | {};
// And then write a sample (and NOT real world production code) implementation
// faking an API call that might or might not find (and return) a venue
const getVenue: GetVenue = function(id) {
const state = id < 10 ? 200 : 404;
return state === 200
? {
id,
name: "Meetings Central",
location: "North Pole",
}
: {};
};
const venue = getVenue(1);
const weHaveVenue = venue.id !== undefined; // β Property 'id' does not exist on type '{}'.
if (weHaveVenue) {
// do something
} else {
// do something else...
}
I know what you are thinking: "Wait, I know that. That's exactly why I'm checking on the id!". But TypeScript needs a little more holding hands:
// Let's define two more types since we will have to reuse them in our code
type Venue = { id: number; name: string; location: string };
type NoVenue = {};
type GetVenue = (id: number) => Venue | NoVenue;
const getVenue: GetVenue = function(id) {
const state = id < 10 ? 200 : 404;
return state === 200
? {
id,
name: "Meetings Central",
location: "North Pole",
}
: {};
};
const venue = getVenue(1);
// By casting our `venue` to a `Venue` type, and then checking on the `id` property,
// we are basically telling TypeScript: "trust us, at runtime we're gonna be fine"
const weHaveVenue = (venue as Venue).id !== undefined; // β
if (weHaveVenue) {
// do something
} else {
// do something else...
}
Hurrah π
This may (and will) work well in several, simple cases. But what if further down we also want to use that venue
object? Let's say we need an upper-cased version of the venue name, and add one line of code to our if/else statement:
[...]
if (weHaveVenue) {
// do something with our venue object
const upperName = venue.name.toUpperCase(); // β Property 'name' does not exist on type 'NoVenue'.
} else {
// do something else...
}
Whoops π. Back at square one.
In this case we need to move our check in a custom type guard, which is fancy wording "a function that checks a type". Check out the full code:
type Venue = { id: number; name: string; location: string };
type NoVenue = {};
type GetVenue = (id: number) => Venue | NoVenue;
// We move our id check into a function whose return type is "value is Type"
function isVenue(venue: Venue | NoVenue): venue is Venue {
return (venue as Venue).id !== undefined;
}
const getVenue: GetVenue = function(id) {
const state = id < 10 ? 200 : 404;
return state === 200
? {
id,
name: "Meetings Central",
location: "North Pole",
}
: {};
};
const venue = getVenue(1);
// We can now call our type guard to be sure we are dealing with one type, and not the other
if (isVenue(venue)) {
// do something with our venue object
const upperName = venue.name.toUpperCase(); // β
} else {
// do something else...
}
To paraphrase the official TypeScript documentation:
Any time
isVenue
is called with some variable, TypeScript will narrow that variable to that specific type if the original type is compatible.
This brief excursion should've clarified a feature of TypeScript that may leave someone coming from JavaScript perplexed. At least, it troubled me a few times! I'd love to hear your comments: let's be friends on Twitter (@mjsarfatti, DMs are open) and on dev.to.
If you'd like to be notified of the next article, please do subscribe to my email list. No spam ever, cancel anytime, and never more than one email per week (actually probably much fewer).
Top comments (1)
Super advanced guide! Thank you!