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
hasOwnPropertyfunction has two generics:X extends {}makes sure we use this method only on objectsY extends PropertyKeymakes sure that the key is eitherstring | number | symbol.PropertyKeyis 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 ifpropis 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 ourobjis the original object, with an intersection type ofRecord<Y, unknown>, the last piece adds the newly found property toobjand 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.