TL;DR:
Either:
1const isValidObject = (myObject as ValidObject).id !== undefined;
Or, better, define a type guard:
1function isValidObject(myObject: ValidObject | {}): myObject is ValidObject {2 return (myObject as ValidObject).id !== undefined;3}
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:
1// Imaginary call to an API that returns the venue with ID 1,2// or an empty object if there is no venue with that ID3const venue = getVenue(1);45// Let's check if a venue was found by verifying the existence of the `id` property6const weHaveVenue = venue.id !== undefined;78if (weHaveVenue) {9 // do something10} else {11 // do something else...12}
Pretty straightforward, right?
Well, the moment we use TypeScript, things don’t work so smoothly anymore. Have a look a this implementation:
1// Let's define the type of our imaginary API function first2type GetVenue = (3 id: number4) => { id: number; name: string; location: string } | {};56// And then write a sample (and NOT real world production code) implementation7// faking an API call that might or might not find (and return) a venue8const getVenue: GetVenue = function(id) {9 const state = id < 10 ? 200 : 404;1011 return state === 20012 ? {13 id,14 name: "Meetings Central",15 location: "North Pole",16 }17 : {};18};1920const venue = getVenue(1);2122const weHaveVenue = venue.id !== undefined; // ❌ Property 'id' does not exist on type '{}'.2324if (weHaveVenue) {25 // do something26} else {27 // do something else...28}
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:
1// Let's define two more types since we will have to reuse them in our code2type Venue = { id: number; name: string; location: string };3type NoVenue = {};45type GetVenue = (id: number) => Venue | NoVenue;67const getVenue: GetVenue = function(id) {8 const state = id < 10 ? 200 : 404;910 return state === 20011 ? {12 id,13 name: "Meetings Central",14 location: "North Pole",15 }16 : {};17};1819const venue = getVenue(1);2021// By casting our `venue` to a `Venue` type, and then checking on the `id` property,22// we are basically telling TypeScript: "trust us, at runtime we're gonna be fine"23const weHaveVenue = (venue as Venue).id !== undefined; // ✅2425if (weHaveVenue) {26 // do something27} else {28 // do something else...29}
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:
1[...]23if (weHaveVenue) {4 // do something with our venue object5 const upperName = venue.name.toUpperCase(); // ❌ Property 'name' does not exist on type 'NoVenue'.6} else {7 // do something else...8}
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:
1type Venue = { id: number; name: string; location: string };2type NoVenue = {};3type GetVenue = (id: number) => Venue | NoVenue;45// We move our id check into a function whose return type is "value is Type"6function isVenue(venue: Venue | NoVenue): venue is Venue {7 return (venue as Venue).id !== undefined;8}910const getVenue: GetVenue = function(id) {11 const state = id < 10 ? 200 : 404;1213 return state === 20014 ? {15 id,16 name: "Meetings Central",17 location: "North Pole",18 }19 : {};20};2122const venue = getVenue(1);2324// We can now call our type guard to be sure we are dealing with one type, and not the other25if (isVenue(venue)) {26 // do something with our venue object27 const upperName = venue.name.toUpperCase(); // ✅28} else {29 // do something else...30}
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).