r/javascript • u/noeldemartin • 9d ago
JavaScript Magic Objects
https://noeldemartin.com/blog/programming-patterns-magic-objectsI just published a blog post to my website about a pattern I've been using for a while that I think you will find interesting (and probably very divisive!): Magic Objects.
class Parrot extends MagicObject {
__get(property) {
return `${property}! ${property}!`;
}
}
const parrot = new Parrot();
console.log(parrot.foo); // "foo! foo!"
console.log(parrot.bar); // "bar! bar!"
Basically, I'm borrowing the idea of Magic Methods from PHP and porting them to JavaScript. You've probably seen something like this done with proxies before, but I still like to use some OOP in my JavaScript, and I also came up with a way to make it work nicely with TypeScript.
It may seem a bit over the top at the beginning, but I've been using this for a while and I've found it very useful.
Let me know what you think!
13
u/krileon 9d ago
You're borrowing a feature from PHP that us PHP developers are actively avoiding and trying to get rid of, lol. Magic = Bad.
-2
u/noeldemartin 9d ago
Well, I guess you're one of the PHP developers who don't like Laravel, then :). Which is fine, as I say in the post it's a matter of opinion/preference.
But for the PHP developers who like Laravel (like me), this is actually very similar to what Laravel's ORM is doing.
I guess you also won't enjoy one other pattern I'll share in my blog, Laravel facades in JavaScript 🙈.
7
u/krileon 9d ago
I love Laravel. You don't have to use its magic. I don't use Facades. I use proper dependency injection. I use proper typed methods and properties. Some parts of Eloquent you can't get away from, but can certainly limit the magic to the minimum.
0
u/noeldemartin 9d ago
You use Laravel without Eloquent, then? How do you interact with the database, using the query builder directly?
3
u/krileon 9d ago
Of course I use Eloquent. There's nothing I can really do about it having magic. At the very least though I add statically typed getter/setter functions to reduce the magic as much as possible. Other times I've replaced it entirely with Doctrine, which was a decent chunk of work but in my experience was worth it as I prefer object mapping over active record.
35
u/javascript 9d ago
Proxy Objects (getters and setters) have been around for a while but I've yet to come across a use case where they were the right choice. They tend to work more as an escape hatch.
11
u/jhartikainen 9d ago
It seems potentially valuable in some kind of framework/infra code. Svelte uses them for its
$staterunes for example. But yeah, like generators, I've not really found any use for them myself.5
u/Dragon_yum 8d ago
I worked with JS/ts for nearly a decade and only came across one time where a generator was the right call
5
u/javascript 9d ago
Oh ya generators haha. I was so bewildered by the `function*` syntax when I first learned it. What a wild land grab for such a niche feature.
3
u/alex-weej 9d ago
It's not that niche! Could have been a keyword though.
1
u/javascript 9d ago
What have you used generators for?
4
u/Kwantuum 8d ago
Generators were used to transpile async/await for years.
I've used generators on a number of occasions. Generally I avoid it unless it's a great fit for the problem because they remain a relatively obscure feature for most people. They're a great way to get support for for..of on custom containers, among other things.
The other problem with generators and iterators in general is the lack of standard library support for manipulating them. No standard iterator mapping/filtering really reduces their usefulness.
1
u/vincentdesmet 9d ago
yeah, i’ve used them as wrappers around CDKTF to intercept and convert attribute getters into creating Terraform remote state lookups
fun, but confusing (leaky abstraction) for DX
1
6
u/ic6man 9d ago
I used a proxy object around a pre-existing API implementation to provide retry and consistent error handling + short circuit.
I don’t know that I would call that an escape hatch. It’s just a nicer way of building on top of something you don’t have access to modify yourself.
4
u/javascript 9d ago
I would absolutely call that an escape hatch. And a very useful one to have available!
2
u/lookarious 9d ago
Libraries like MobX uses proxies, its useful, but they are not good as in other languages, for example there is no way to use custom setter for whole class because JS is dynamically typed.
1
u/hyrumwhite 8d ago
I like using them to proxy web worker messages, so invoking a web worker method is just doing myWorkerProxy.doBigJob() with type support derived from the actual handler object in the worker.
1
0
u/TheThingCreator 9d ago
In my opinion they are not useful in normal development, they are the right choice if you're building a framework and need reactivity basically. Even "magic methods" don't sound like the right choice unless you're making a framework. Using these kinds of things just adds ambiguity to your code but they are highly useful for framework development.
2
u/javascript 9d ago
As a former library developer myself, agreed. Sometimes to make a truly generic interface, you need niche features. Glad they're available! But not a good default haha
1
u/noeldemartin 9d ago
Yes, I agree. In fact, I've only used them in 2 projects so far: an ORM and a Vue framework. Both libraries.
However, with the right Typescript declarations, it's not that ambiguous from user land. For example, in my ORM I define the fields with Zod, and using an IDE you can go straight to the Zod definition every time you use the field. They aren't declared as "any".
In any case, it's definitely a "sharp knife" and not something I use too often. But I specially like the hack of returning a Proxy from the constructor :). I haven't seen this used anywhere else, but IMO it enables some pretty powerful patterns.
1
u/TheThingCreator 9d ago
Ya I used the proxies to make a reactivity module for an app, it works very nice but ya things can get pretty pointy
4
u/CodeAndBiscuits 9d ago
Setting aside the whole PHP source question, what I'm struggling to figure out is what problem you're actually solving here. Less of a "should you?" And more of a "why would you?"
I know I'm biased here on this because I so often work as one of the first or early devs on a project that will ultimately be handed off, but even if I wasn't I think I would stand by the opinion that code should be "unsurprising" to a developer who is an expert in the language, even if they are new to the project. There's so much work already in learning and comprehending a code base that you're stepping into. Why invent unusual techniques that add more barriers to that process?
1
u/noeldemartin 8d ago
Yes, in general I agree with that idea, but as I mentioned in the blog post and other people have mentioned in the comments, this is something I would only use a few times and it's mostly for framework/libraries, not for user land.
In particular, the main reason I started using it was to implement my ORM. Users of that library don't have to use the magic methods, but it is essential to achieve the kind of DX I envisioned when I was working on that library. Sure, you can also do that without using proxies, but IMO you would end up with a complicated typescript anyways (in the library), and the DX would be worse.
Just a matter of opinion, though. I'm sure it's possible to do an ORM without this. But I had a vision for an API I wanted for user-land, and worked backwards to achieve it :).
1
u/me0here 7d ago
You might like Signals better; don't have to mess with Class as much
1
u/noeldemartin 6d ago
Yes, I'm aware of signals, and Vue already has them. But I still prefer this pattern because I actually like OOP in some situations. With this, I can define methods for a given class that take advantage of the dynamic properties, etc.
Basically, this pattern is all about DX and trying to achieve an API I had in mind when I was working on my JavaScript ORM. I know there are other ways to achieve the same functionality with different APIs, but I like defining classes and methods for this.
-6
41
u/smartgenius1 9d ago
Magic methods are generally discouraged in the PHP community, and are widely considered a bad decision. Why port them to JS?