TypeScript: Check for object properties and narrow down type
TypeScript’s control flow analysis lets you narrow down from a broader type to a more narrow type:
function print(msg: any) {
if(typeof msg === 'string') {
// We know msg is a string
console.log(msg.toUpperCase()) // 👍
} else if (typeof msg === 'number') {
// I know msg is a number
console.log(msg.toFixed(2)) // 👍
}
}
This is a type-safety check in JavaScript, and TypeScript benefits from that. However, there are some cases where TypeScript at the time of this writing needs a little bit more assistance from us.
Let’s assume you have a JavaScript object where you don’t know if a certain property exists. The object might be any
or unknown
. In JavaScript, you would check for properties like that:
if(typeof obj === 'object' && 'prop' in obj) {
//it's safe to access obj.prop
console.assert(typeof obj.prop !== 'undefined')
// But TS doesn't know :-(
}
if(typeof obj === 'object' && obj.hasOwnProperty('prop')) {
//it's safe to access obj.prop
console.assert(typeof obj.prop !== 'undefined')
// But TS doesn't know :-(
}
At the moment, TypeScript isn’t able to extend the type of obj
with a prop
. Even though this works with JavaScript.
We can, however, write a little helper function to get correct typings:
function hasOwnProperty<X extends {}, Y extends PropertyKey>
(obj: X, prop: Y): obj is X & Record<Y, unknown> {
return obj.hasOwnProperty(prop)
}
If you don’t want to know how this works, copy it and be happy. If you want to know more, let’s check out what’s happening:
- Our
hasOwnProperty
function has two generics:X extends {}
makes sure we use this method only on objectsY extends PropertyKey
makes sure that the key is eitherstring | number | symbol
.PropertyKey
is a builtin type.
- There’s no need to explicitly define the generics, they’re getting inferred by usage.
(obj: X, prop: Y)
: We want to check ifprop
is a property key ofobj
- The return type is a type predicate. If the method returns
true
, we can retype any of our parameters. In this case, we say ourobj
is the original object, with an intersection type ofRecord<Y, unknown>
, the last piece adds the newly found property toobj
and sets it tounknown
.
In use, hasOwnProperty
works like that:
// person is an object
if(typeof person === 'object'
// person = { } & Record<'name', unknown>
// = { } & { name: 'unknown'}
&& hasOwnProperty(person, 'name')
// yes! name now exists in person 👍
&& typeof person.name === 'string'
) {
// do something with person.name, which is a string
}
That’s it! A lovely little helper to make TypeScript understand your code better. Here’s a playground for you to fiddle around.