2

First the code snippet I try to get to work:

// taken from https://github.com/TheDavidDelta/scope-extensions-js and https://stackoverflow.com/a/65808350/2080707

declare global {
    /**
     * let calls the specified function block with `this` value as its argument and returns its result
     * @param fn - The function to be executed with `this` as argument
     * @returns `fn`'s result
     */
    interface Object {
        let<R>(fn: (val: this) => R): R;
    }
}

Object.defineProperty(Object.prototype, 'let', {
    value(fn) {
        return fn(this);
    },
    configurable: true,
    writable: true,
});

And now what I try to achieve:

({ a: 'foobar' }).let((obj) => console.debug(obj.a)) Of course this example is contrived and doesn't make much sense. But there are some cases where it does.

My problem is that let's argument is of type object and not of type {a:string}. For this I need to reference the object whichs prototype got extended and not object itself. Any pointer would be helpful :pray:

Edit: One could adapt https://github.com/HerbLuo/babel-plugin-kotlish-also to be able to support let as well. But it creates a lot of function overhead as well I guess.

velop
  • 3,102
  • 1
  • 27
  • 30
  • Ick, modifying `Object.prototype` is very naughty, but I guess you're not asking for advice about that. And do note that `{a: "foobar"}.let` is not valid syntax; you will need to wrap that in parentheses to `({a: "foobar"}).let`. Anyway, I'm looking around to see if I can find a reason why polymorphic `this` doesn't automatically work the way you want here. Does [this code](https://tsplay.dev/mAVb1W) meet your needs instead? If so I can write up an answer. If not, please [edit] the code example to show what the issue is. – jcalz Sep 22 '21 at 12:51
  • Indeed it is naugthy. But calling `let({a: 'foobar})((obj) => ...)` somehow just doesn't feel right. It is also possible to use babel to transform but I guess all the function mingeling does'nt make performance better. Thank you very much for your code snippet. It perfectly fits. I don't understand why `let` doesn't expect the object as first parameter as required by the types. But it works :) – velop Sep 23 '21 at 14:01
  • 1
    Okay I'll write up an answer when I get a chance. – jcalz Sep 23 '21 at 14:03

1 Answers1

3

At the outset, as you know, it is considered bad practice to extend native prototypes this way. I will not belabor the point here, but I would be remiss if I did not officially discourage using this technique in any production code.


With that out of the way: you are trying to use the polymorphic this type to represent the actual type of the object on which you call the let() method, which is almost certainly going to be narrower than Object.

Unfortunately, as you've seen, the compiler eagerly evaluates this to be Object when you call let(). I haven't found authoritative documentation that says that this happens or why (although I'm almost sure I've seen something like this before; I'll keep looking and update if I find it), but this comment in microsoft/TypeScript#40704 is evocative: this types are expensive. They are essentially implicit generic type parameters, and whenever you use one in a class or interface it imposes a compiler performance penalty. My suspicion is that native types like Object this are not treated as potentially extra generic because to do so would affect all object types everywhere and the penalty would be severe.


Luckily, we can work around it. Instead of using this as an implicit generic, we can make an explicit generic T and use a this parameter of type T in the let() method:

interface Object {
    let<R, T>(this: T, fn: (val: T) => R): R;
}
        
({ a: 'foobar' }).let((obj) => console.debug(obj.a)) // okay now

I first saw this sort of technique used in a comment on microsoft/TypeScript#5863, to work around a similar limitation whereby polymorphic this types are not available inside static class methods.

Note that the same comment from before in microsoft/TypeScript#40704 actually says that this types are expensive, but this parameters are cheap. So presumably this workaround is viable because of such "economic" factors.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360