Reputation: 26518
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
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
Upvotes: 24
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
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
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