Ben Carp
Ben Carp

Reputation: 26518

Why does Typescript treat `object.hasOwnProperty("key")` as essentially different from `"key" in object`

declare const action: { total: number } | { };
declare const defavlt: 200;

const total = (action.hasOwnProperty("total")) ? action.total : defavlt;

results in the following TS error for action.total:

Property 'total' does not exist on type '{ type: "NEW_CONVERSATION_LIST" | "UPDATE_CONVERSATION_LIST_ADD_BOTTOM" | "UPDATE_CONVERSATION_LIST_ADD_TOP"; list: IDRArray<RestConversationMember>; total: number | undefined; } | ... 13 more ... | { ...; }'.
  Property 'total' does not exist on type '{ type: "UPDATE_URL_STATE"; updateObj: IMessagingUrlState; }'.ts(2339)

Whereas

const total = ("total" in action) ? action.total : defavlt

works. Is there a rational for TS treating both cases differently?

Upvotes: 17

Views: 11928

Answers (4)

jcalz
jcalz

Reputation: 327944

In the issue microsoft/TypeScript#10485, it was suggested for the in operator to act as a type guard which can be used to filter unions; this was implemented in microsoft/TypeScript#15256 and released with TypeScript 2.7.

This was not done for Object.prototype.hasOwnProperty(); if you really feel strongly about this, you might want to file a suggestion for it, noting that a similar suggestion (microsoft/TypeScript#18282) was declined because it was asking for the more controversial narrowing of the key and not the object... and some people have wanted both (microsoft/TypeScript#20363). And there's no guarantee the suggestion would be accepted.

Luckily for you, though, you don't have to wait for this to be implemented upstream. Unlike an operator like in, the hasProperty() method is just a library signature that can be altered to act as a user-defined type guard function. What's more, you don't even have to touch the standard library definition; you can use declaration merging to augment the Object interface with your own signature for hasOwnProperty():

// declare global { // need this declaration if in a module
interface Object {
  hasOwnProperty<K extends PropertyKey>(key: K): this is Record<K, unknown>;
}
// } // need this declaration if in a module

This definition says that when you check obj.hasOwnProperty("someLiteralKey"), a true result implies that obj is assignable to {someLiteralKey: unknown}, while a false result does not. This definition might not be perfect and there are probably quite a few edge cases (e.g., what should obj.hasOwnProperty(Math.random()<0.5?"foo":"bar") imply? what should obj.hasOwnProperty("foo"+"bar") imply? they will do weird things) but it works for your example:

const totalIn = ("total" in action) ? action.total : defavlt; // okay
const totalOwnProp = (action.hasOwnProperty("total")) ? action.total : defavlt; // okay

Link to code

Upvotes: 24

Rusty Dawson
Rusty Dawson

Reputation: 1

In case anyone else comes here looking for a complete answer, it's possible to type the method and not lose out on any typing information

interface Object {
  hasOwnProperty<T, K extends keyof T>(this: T, key: K): this is T & {[Key in K]: T[Key]};
}

Upvotes: 0

Nolle
Nolle

Reputation: 261

For me, an object merge works like this:

const total = (action.hasOwnProperty("total")) ? 
        {...{total: defavlt}, ...action}.total : defavlt

or the short way:

const total = {...{total: defavlt}, ...action}.total

Upvotes: 0

S&#225;mal Rasmussen
S&#225;mal Rasmussen

Reputation: 3495

You can implement your own wrapper function around hasOwnProperty that does type narrowing.

This way you don't have to fiddle with the built in types and do type merging if you don't want to pollute the built in types.

function hasOwnProperty<T, K extends PropertyKey>(
    obj: T,
    prop: K
): obj is T & Record<K, unknown> {
    return Object.prototype.hasOwnProperty.call(obj, prop);
}

I found this solution here: TypeScript type narrowing not working when looping

Upvotes: 3

Related Questions