1

I'm trying to add, for example a isEmpty getter to the Object or Array, but I'm having trouble to do so because I think I'm not really understanding how to access properties through this:

declare global {
  interface Object {
    isEmpty: boolean;
  }
}

// 1. Doesn't work
Object.prototype.isEmpty = this.keys.length === 0;

// 2. Doesn't work either
Object.prototype.isEmpty = Object.keys(this).length === 0;

Is it really possible to make this work? How would it be with Array?

References:

Philippe Fanaro
  • 6,148
  • 6
  • 38
  • 76
  • 1
    Looks like a duplicate of [this](https://stackoverflow.com/q/10592753/2887218) more or less – jcalz Dec 19 '22 at 23:22
  • 1
    Does [this approach](https://tsplay.dev/WokMlW) work for you? If so I could write up an answer, although with big caveats about how it is almost universally discouraged to add novel functionality to native prototypes this way. – jcalz Dec 19 '22 at 23:25
  • I think that is working, thank you very much! Although I'm getting this from ESLint, as expected: `Object prototype is read only, properties should not be added`. Why exactly is this kind of thing discouraged? Some languages actually encourage this sort of extension patter (e.g. Dart). – Philippe Fanaro Dec 19 '22 at 23:32
  • https://eslint.org/docs/latest/rules/no-extend-native#rule-details `In JavaScript, you can extend any object, including builtin or “native” objects. Sometimes people change the behavior of these native objects in ways that break the assumptions made about them in other parts of the code.` – Dimava Dec 19 '22 at 23:42

2 Answers2

1

In this way it's easy to make extensions, and you don't have to worry about correct descriptors (making them non-enumerable, etc) and correct typing

class ObjectExtension extends Object {
    get isEmpty() {
        return Object.keys(this).length === 0;
    }
}

function extendSuperclassPrototype(classExtension: new (...a: any[]) => any): void {
    let desc = Object.getOwnPropertyDescriptors(classExtension.prototype);
    delete (desc as any).constructor;
    let superproto = Object.getPrototypeOf(classExtension).prototype;
    Object.defineProperties(Object.prototype, desc);
}

declare global {
    interface Object {
        isEmpty: ObjectExtension['isEmpty']
    }
}

extendSuperclassPrototype(ObjectExtension)

console.log(({a: 1}).isEmpty, ({}).isEmpty)
Dimava
  • 7,654
  • 1
  • 9
  • 24
1

⚠ WARNING! ⚠

IT IS WIDELY CONSIDERED BAD PRACTICE TO MODIFY NATIVE PROTOTYPES, mainly because it might change the code — specially its behavior — other programmers depend on.

See Why is extending native objects a bad practice? for more information. In what follows I'm going to show how to do it, but this is not meant to say that you should do it. A standalone isEmpty() function would work just as well, and is far less controversial.

Do check out @Dimava's answer as well, as he shows yet another way of doing this.


If you want to append a getter to an existing object, then you need to use the Object.defineProperty() method.

Your example could therefore look like:

// declare global { // <-- needed within modules
interface Object {
  isEmpty: boolean;
}
// }

Object.defineProperty(Object.prototype, "isEmpty", {
  configurable: true,
  get() {
    return Object.keys(this).length === 0;
  },
});

As for Arrays, you could simply do the same but with Object.defineProperty(Array.prototype, ...).

And you can verify that it works as desired:

const a = { z: 1 };
console.log(a.isEmpty) // false

const b = {};
console.log(b.isEmpty) // true

Playground link to code

Philippe Fanaro
  • 6,148
  • 6
  • 38
  • 76
jcalz
  • 264,269
  • 27
  • 359
  • 360
  • If the reason for not extending these native prototypes is changing behavior, then what I'm proposing is not a problem, I don't think. And that's simply because I'm not changing anything, I'm simply adding a derivative property that actually doesn't change the behavior under other programmers' feets. – Philippe Fanaro Dec 20 '22 at 00:11
  • Anyways, I'm torn apart between giving the answer to you or @Dimava, since he showed something I hadn't seen elsewhere (and he has way less points than you). Who do you think I should give it to? (Thank you very much for your answer anyways.) – Philippe Fanaro Dec 20 '22 at 00:12
  • 1
    Hey, don't ask me which one to accept . I'm obviously biased toward myself: I usually try to give links to sources for my information so that it's not just my opinion. – jcalz Dec 20 '22 at 00:14
  • 1
    As for adding a new property to `Object.prototype`, this is fine as long as nobody else comes along and tries to add a different `isEmpty` there. What if another developer makes `isEmpty` a *method* so you have to call `foo.isEmpty()`? It's far less controversial to make your own function (especially if it's module-scoped) rather than modify anything other code can access. Again, I'm not saying you *can't* do it, or even that you necessarily *shouldn't*. I'm just saying that it's widely considered bad practice. – jcalz Dec 20 '22 at 00:17
  • I think the actual bad practice here is not having a freaking compiler that would point to developers that there are conflicting extensions to native prototypes :P – Philippe Fanaro Dec 20 '22 at 00:19
  • 1
    I briefly edited your answer to include a pointer to our discussion and to Dimava's answer. – Philippe Fanaro Dec 20 '22 at 00:28