12
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?

Ben Carp
  • 24,214
  • 9
  • 60
  • 72
  • 1
    Afaik not. The difference I know, `in` does take the prototype into account whereas `hasOwnProperty` doesn't. Furthermore I have problems reproducing your kind of issue. Please see: https://www.typescriptlang.org/play/#code/JYOwLgpgTgZghgYwgAgIILMA9iZBvAWAChlkwsw4AbAfgC5kQBXAWwCNoBuYgX2OIQ4AzmGSJMOBugm4AvPl78igkCLIVqAeRAp5ACnHYQAOgAWcIZoDuIAApQsAB2hgAnnoBE5SlQ8BKP2QaMQwjY29qZAYABm4iAWFRCKoAFSssZH0vDV9kUBCZQODDHHCcqORYpWoXTxSc7QgGD2QAanUfRrbkFoBaXuR6nzSsZu7kkb9OIA – r3dst0rm Dec 05 '19 at 12:34
  • @r3dst0rm, I'm aware that hasOwnProperty doesn't take into account the prototype. Interesting that your demo doesn't reproduce the issue. Probably since it's TS version 3.7.2, while our project uses TS 3.5.3. – Ben Carp Dec 05 '19 at 12:47
  • @r3dst0rm, downgrading your demo to 3.5.1 didn't reproduce the issue. Unfortunately I can't paste a screenshot. Here's a link https://1drv.ms/u/s!AoVymsDeSYhfg41n5lkItLwLsiMqfQ?e=g84Jy0 – Ben Carp Dec 05 '19 at 12:53
  • That's a bummer. What you could try is to create a function with a type guard that checks for the existence: `const hasOwnProp = (obj: T, prop: string): obj is T => obj && obj.hasOwnProperty(prop);` and use it like: `hasOwnProp(action, "total") ? action.total : default;` – r3dst0rm Dec 05 '19 at 12:58
  • Please consider editing the above code to constitute a [mcve] as described in the guidelines for [ask]. The type of `action` is relevant missing information. – jcalz Dec 05 '19 at 16:41
  • @jcalz, THX. Do you find this better? – Ben Carp Dec 05 '19 at 19:36
  • 1
    Not really, sorry. [Observe](http://www.typescriptlang.org/play//#code/JYOwLgpgTgZghgYwgAgIILMA9iZBvAWAChlSwsw4AbAfgC5k4QBPZYgX2JgFcQNtcAcwhgAKhWoAKRJhwN0skAEpCJUlBHcouafxwA6ABZwAzgHkA7iAAKULAAdoYZpIBE5SlVdKlNRnpB9D2pkBgATCHhuKjAOYiA); it doesn't give the error you're describing, and `default` is a reserved word which cannot be used as an identifier. I've already answered the question below with a code link at the bottom, using `declare const action: { total: number } | { specialK: number };` as the type, which does reproduce the issue. Good luck! – jcalz Dec 05 '19 at 19:45

3 Answers3

19

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

Okay, hope that helps; good luck!

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 2
    This is a bit suspicious when testing a key of type `string` instead of a string literal; it infers `Record` so after narrowing, you won't get a type error on any string key, including strings you didn't test with `hasOwnProperty`. I think that's what you're hinting at with the sentence about edge-cases, but I think this should be spelled out explicitly. – kaya3 Dec 05 '19 at 20:10
  • 1
    yeah there are plenty of edge cases here, many of which could be addressed by increasingly complex signatures, but I don't know if that's worth it. – jcalz Dec 05 '19 at 20:17
3

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

Sámal Rasmussen
  • 2,887
  • 35
  • 36
  • The only problem with this approach is that it makes property value `unknown`. – Gajus Oct 19 '21 at 22:28
  • `!hasOwnProperty()` will give you weird type errors https://www.typescriptlang.org/play?ssl=1&ssc=1&pln=2&pc=1#code/GYVwdgxgLglg9mABACwIYGcA8AVANIgaUQFMAPKYsAE3UQAUAnOAB2IagE8DiOA+ACgBQiRHABGAKwBciPMMTMmzGQUEBKGeImIYtbIgBkiAErEIcBlUwF84ANZg4AdzC9EAb3kNiUEAyQA8pJmUAB0inBQkRysoWjoAS6MLGycoRCoADaZ-Fr4EcxqANyCAL6CgqCQsAgKIOjIycw4McQC8loy7ogA2nY8MuhQDDBgAOYAujLYrT0TiKW48gWDw6NjSyIAblkgxNOt6h7yMMCI-ACE8bmS+UpqasciIlo9BfMAvL0TJSLlL5I3koJuF6sh+DtMntil4fH4kFoSuVKuBoPAkMwwU0AHIWAC2WRarHaAOkHl6-Q4qxG4ymslm80WyyU1PWm0QkL2B1YR08IlO5wuQQkIXCTCinFi8USYCaqQ46SyOTyCnujz5z1e70QXzmvwWHUB71BDQhu2IMJE3l8-lEkiRgiAA – Boris Verkhovskiy Nov 14 '22 at 08:23
  • @BorisVerkhovskiy your example breaks because you try to use string as Prop. Doing `!hasOwnProperty()` with a string prop will eliminate all keys from the obj leaving just the never type in its place. – Sámal Rasmussen Nov 26 '22 at 22:30
0

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
Nolle
  • 201
  • 2
  • 5
  • do you think this answers the question "Why does Typescript treat `object.hasOwnProperty("key")` as essentially different from `"key" in object`" ? – Ahmed Sbai Jun 30 '23 at 07:51
  • You're right. This is just another way to avoid the TS2339 error, but it doesn't answer the question. – Nolle Jul 01 '23 at 07:30