2

The default type for hasOwnProperty is hasOwnProperty(v: PropertyKey): boolean;. However, this prevents me from doing things like:

const obj = { a: 1 };

function foo(str: string) {
    if (obj.hasOwnProperty(str)) {
        console.log(obj[str]);
        // Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ a: number; }'.
    }
}

To override the type for obj.hasOwnProperty, I added:

interface Object {
  hasOwnProperty<T extends ObjectOf<any>>(this: T, key: any): key is keyof T;
}

This works for obj.hasOwnProperty(key), but not Object.prototype.hasOwnProperty.call(obj, key). How can I override hasOwnProperty's type when I call it using the second method?

Edit: to clarify, even with the override, the following doesn't work:

const obj = { a: 1 };

function foo(str: string) {
    if (Object.prototype.hasOwnProperty.call(obj, str)) {
        console.log(obj[str]);
        // Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ a: number; }'.
    }
}
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Leo Jiang
  • 24,497
  • 49
  • 154
  • 284
  • Can you explain a lite bit more what you want to achieve? or why you want to override hasOwnProperty? For me, this `Object.prototype.hasOwnProperty.call(obj, key) ` works like `obj.hasOwnProperty(key)` and I achieve same result. – Schwarz54 Jun 16 '21 at 12:16
  • `obj.hasOwnProperty` is using my overridden type, but `Object.prototype.hasOwnProperty.call` is still using the default type – Leo Jiang Jun 16 '21 at 12:23
  • 1
    @LeoJiang I have to admit, you are asking interesting questions :D – captain-yossarian from Ukraine Jun 25 '21 at 20:43

3 Answers3

2

FULL UPDATE

I was wrong, I have overriden only call method.

I'm sorry for providing you with bad example.

With my previous code, calling Object.prototype.[any prototype method].call would act like typeguard which is wrong.

Previous wrong code:

interface CallableFunction extends Function {

  call<T, Prop extends string, R>(this: (this: T, property: Prop) => R, thisArg: T, property: Prop): thisArg is T & Record<Prop, string>;
}

Above code means that Object.prototype.propertyIsEnumerable.call will act as a typeguard because I have typed only call.

WORKING EXAMPLE



type Tag = { [prop: `tag${number}`]: never }

interface Object {
  hasOwnProperty(v: PropertyKey): boolean & Tag
}

interface CallableFunction extends Function {
  call<
    T,
    Prop extends string,
    R extends boolean & Tag
  >(this: (this: T, property: Prop) => R, thisArg: T, property: Prop): thisArg is T & Record<Prop, string>;
}

declare const obj: { name?: string, surname?: number }

if (Object.prototype.hasOwnProperty.call(obj, 'name')) {
  const test = obj.name // string
}

if (Object.prototype.propertyIsEnumerable.call(obj, 'name')) {
  const test = obj.name // string | undefined
}

Playground

How does it work ?

I have created a brand type Tag and intersected it with hasOwnProperty return type.

What it gives to me?

call method is able to infer return type of hasOwnProperty. This is the only way to know which prototype method was called.

Then, I added constraint to R generic type. R is an infered return type from prototype method. In our case it is boolean & Tag. This is how call method is able to figure out that we have called hasOwnProperty.

0

Given that its hard to understand your motivation for doing this, here is my best guess at what you're trying to achieve:

interface Object {
    hasOwnProperty<T extends ObjectOf<any>>(this: T, key: any): key is keyof T;
}

interface ObjectConstructor {
    readonly prototype: Object;
}

if (obj.hasOwnProperty(key)) {
    console.log(obj[key]);
}

if ((Object as ObjectConstructor).prototype.hasOwnProperty.call(obj, key)) {
    console.log(obj[key]);
}

Without casting:

interface Object {
    hasOwnProperty<T extends ObjectOf<any>>(this: T, key: any): key is keyof T;
    readonly prototype: this;
}

if (obj.hasOwnProperty(key)) {
    console.log(obj[key]);
}

if (Object.prototype.hasOwnProperty.call(obj, key)) {
    console.log(obj[key]);
}
Dennis G.
  • 283
  • 1
  • 11
  • I added clarifications, this isn't really overriding Object.prototype.hasOwnProperty, it's just casting to a different type – Leo Jiang Jun 16 '21 at 12:40
  • @LeoJiang Thanks for the clarifications. Honestly, this was just a guess given that I can't reproduce the issue you're seeing. I don't get an implicit 'any' type like you're seeing and therefore can't play around and verify a solution. I used casting to follow the default TS object definition conventions. Adding version without casting. – Dennis G. Jun 16 '21 at 12:56
  • 1
    @LeoJiang After finally absorbing what you're trying to accomplish and exploring possible solutions, I'm throwing in the towel. Without understanding TS internals better, it seems like this won't be possible. See [this issue](https://github.com/microsoft/TypeScript/issues/5101) that started the whole discussion over there. Basically, since `Object.prototype.hasOwnProperty.call` cannot infer the return type of `Object.prototype.hasOwnProperty` to be a type guard (predicate), you are left with a boolean return and therefore no type narrowing. – Dennis G. Jun 16 '21 at 18:41
0

This is what I tried, a little bit dirty, but you get what you want whit this workaround.

const example = {
     eProperty:'hello',
     e2:1,
     e3:[123,432,'paco']
} 
foo(str: string) { 
   if(Object.prototype.hasOwnProperty.call(example, str)){
   let index = 0;
   for(let k in example){
     if(k === str){
       break;
     }else{
       index++;
     }    
   }
 // a is a array that index 0 is the key and index 1 is the value
    let a = Object.entries(example)[index];
    console.log(a[1]);
}
// logs hello
foo('eProperty');
Schwarz54
  • 964
  • 1
  • 9
  • 18