{"id":27642011,"url":"https://github.com/bathos/private-mixin","last_synced_at":"2025-04-23T23:52:09.993Z","repository":{"id":57149424,"uuid":"159646975","full_name":"bathos/private-mixin","owner":"bathos","description":"Augment objects with contracts that can include private fields","archived":false,"fork":false,"pushed_at":"2019-01-06T09:15:18.000Z","size":44,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-23T23:52:05.698Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bathos.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-11-29T10:15:34.000Z","updated_at":"2019-01-06T09:15:20.000Z","dependencies_parsed_at":"2022-09-03T18:11:58.666Z","dependency_job_id":null,"html_url":"https://github.com/bathos/private-mixin","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bathos%2Fprivate-mixin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bathos%2Fprivate-mixin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bathos%2Fprivate-mixin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bathos%2Fprivate-mixin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bathos","download_url":"https://codeload.github.com/bathos/private-mixin/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250535081,"owners_count":21446506,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-04-23T23:52:09.390Z","updated_at":"2025-04-23T23:52:09.967Z","avatar_url":"https://github.com/bathos.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# private-mixin\n\nThis module is a small tool for defining mixins that implement contracts that\nuse [private fields].\n\nThe private fields proposal is at stage 3 at the time of writing — it is not yet\nofficially part of ECMAScript. It has been implemented in V8 and is available in\nnode by using the `--harmony-private-fields` V8 flag; it’s also available in\nChrome Canary.\n\n**This lib is intended mainly as a proof of concept.** It is not super advisable\nto enable experimental features in ordinary applications. The proposal could\nstill change. In addition, if we’re patient, there will likely be superior\nsolutions in 2298 when the decorators proposal reaches stage 3.\n\n\u003c!-- MarkdownTOC autolink=true --\u003e\n\n- [Background](#background)\n  - [Utility of private fields](#utility-of-private-fields)\n  - [Relationship to the WeakMap pattern](#relationship-to-the-weakmap-pattern)\n  - [Challenges of private fields](#challenges-of-private-fields)\n  - [Solutions for these problems](#solutions-for-these-problems)\n- [Usage](#usage)\n  - [Defining a mixin](#defining-a-mixin)\n  - [Using a mixin with a class](#using-a-mixin-with-a-class)\n  - [Using a mixin with other objects](#using-a-mixin-with-other-objects)\n  - [Using mixin contracts at-scope](#using-mixin-contracts-at-scope)\n- [How it works](#how-it-works)\n\n\u003c!-- /MarkdownTOC --\u003e\n\n## Background\n\nBefore getting into [usage](#usage), I want to provide background on what this\nis all about and why I was exploring this space. I’m mainly hoping the code here\nwill be interesting to other people looking at these problems.\n\n### Utility of private fields\n\nPrivate fields provide a first-class syntactic solution for associating state\nwith class instances without exposing it as public properties. This can make it\neasier to establish invariants about mutable state; usually it’s less about\npreventing the outside from seeing and more about preventing the outside from\nchanging.\n\nPrivate instance state is also a key concept when implementing host APIs or\nhost-like patterns, where instance data properties are unused and the entire API\nis realized at the constructor/prototype level. Relatedly, private state enables\nobjects and methods to exhibit “branding” behaviors, like intrinsic and host\nobjects often do.\n\n### Relationship to the WeakMap pattern\n\nPrivate fields follow a similar model to the “WeakMap pattern.” In the WeakMap\npattern, keys are public object instances and values are private state. This,\ntoo, allows implementing host-like APIs, branding, and nearly genuine privacy.\nThat “nearly” qualifier concerns the fact that `globalThis.WeakMap`,\n`WeakMap.prototype.set`, and so on are globally mutable. A determined agent that\nis able to evaluate code before your modules could patch these and spy. This\nisn’t generally something people worry about, but it’s a notable difference.\nBecause private fields are syntactic and the implied “hidden WeakMap” is fully\nabstracted, the API cannot be tainted or forged.\n\nThere is another critical difference between the WeakMap pattern and private\nfields. The WeakMap pattern relies on an existing form of privacy in ES, scope.\nScopes are very flexible; it is easy to control exactly what can have access to\na scope’s information. Typically the scope in question would be module scope.\nThis means with the WeakMap pattern, one can share a single private contract\nacross multiple classes, or even objects created without classes, behaving like\n“slots” in intrinsic and platform APIs. This isn’t true for private fields,\nwhose analog for scope is “a syntactic class body”. Another difference is that\nattempting to access a private field which has not been installed on a given\nobject will always throw, while in the WeakMap pattern, whether to throw is a\nchoice.\n\nFrom here on I’ll sometimes conflate “slot” and “field.” That they are different\nis ultimately not observable except when it comes to cross-realm behaviors,\nthough making the latter behave like the former is not straightforward.\n\n### Challenges of private fields\n\nExamples of APIs that need private state but which cannot easily be implemented\nusing private fields are easy to find. Among ES intrinsics, common slots are\nshared by both `%TypedArray%` and `DataView`. They don’t obtain these from a\ncommon ancestor. The related `ArrayBuffer.isView` method also needs awareness of\nthis common slot, and isn’t even an instance method. The implication is that\nknowledge of these slots is shared by a (hypothetical) scope — it clearly isn’t\nlocal to each of these object definitions when the spec refers to the\n[[ViewedArrayBuffer]] slot.\n\nPrivate fields wouldn’t work out of the box to implement an API like this\nbecause they must always be declared by a single class body and are always\nassociated with a single specific constructor.\n\nMany APIs that implement an object graph, like the DOM, are challenging to\nimplement using private fields, but easy (if boilerplate-heavy) to implement\nusing WeakMaps. This is on account of the same scoping issue.\n\nLess importantly, brand checks that produce consistent error messages in\nhigh-fidelity WebIDL API implementations surprisingly require more rather than\nless boilerplate if branding is achieved via private fields rather than WeakMap.\n\n### Solutions for these problems\n\nNaturally, we can continue using WeakMap when private fields are a poor fit. The\nproposal today is well-suited for usage at the application level, but often may\nbe unsuitable for library code. However, in the future it’s expected that the\ndecorators proposal will provide a hook that will make working with private\nfields easier in these contexts. It may alleviate these pain points (and make\nthe pattern used by this library moot).\n\nAll that said, it is possible to share private fields using ordinary scope for\nmanaging privacy even without decorators or reified field keys. It’s just not\nsuper obvious how, and without a tool for abstracting the dance away a bit, it’s\nvery noisy to achieve. That’s the functionality this library provides: it lets\nyou use private fields when implementing shared contracts with some of the hoop\njumping tucked away.\n\n## Usage\n\n### Defining a mixin\n\nThe library exports a constructor, `Mixin`. This constructor takes a single\nargument, a class. That class must extend `Mixin.Super`.\n\n```js\n// answer-mixin.mjs\n\nimport Mixin from 'private-mixin';\n\nexport default new Mixin(class extends Mixin.Super {\n  #theAnswer = 42;\n  #punctuation;\n\n  constructor(instance, punctuation) {\n    super(instance);\n    this.#punctuation = punctuation;\n  }\n\n  theAnswer() {\n    return `${ this.#theAnswer++ }${ this.#punctuation }`;\n  }\n});\n```\n\nAn explicit constructor is only necessary if the mixin needs to take arguments\nas part of its API. If there is an explicit constructor, it must pass the first\nargument along when it calls `super()`.\n\n### Using a mixin with a class\n\nThe `Mixin` instance can be used to apply the mixin to a class or a single\nobject. Using the mixin to augment a class looks like this:\n\n```js\nimport answerMixin from './answer-mixin.mjs';\n\nclass Earth {\n  constructor() {\n    answerMixin.super(this, '!');\n  }\n}\n\nanswerMixin.extend(Earth);\n```\n\nIf we create an instance of `Earth` now...\n\n```js\nconst earth = new Earth;\n\nearth.theAnswer(); // \"42!\"\nearth.theAnswer(); // \"43!\"\n```\n\nWe passed the class to augment to `Mixin.prototype.extend`. This copies any\nunique properties of the original mixin (like \"theAnswer\") to the target. But\nthere’s a second part to the API, too. The super-like `Mixin.prototype.super`\nfunction should be called by the mixee in its own constructor. It takes the\ninstance as the first argument plus any additional arguments that should be\npassed to the mixin’s constructor. Afterwards, the instance will have been\noutfitted with the mixin’s private slots, and any methods that came with the\nmixin that rely on those slots will work.\n\n### Using a mixin with other objects\n\nThere is another method, `Mixin.prototype.extendObject`, which can be used to\naugment objects directly. These could be ad hoc objects or they could be\nprototypes meant for used with `Object.create`. Because there is no\n`constructor` in this case, static properties cannot be copied over.\n\n```js\nfunction createEarth() {\n  const earth = {};\n  answerMixin.extendObject(earth);\n  answerMixin.super(earth, '?');\n  return earth;\n}\n\ncreateEarth().theAnswer(); // \"42?\"\n```\n\n### Using mixin contracts at-scope\n\nThe final part of this is the most important. So far we’ve dealt with the idea\nthat these pieces of functionality can be defined commonly and use the same\nfield keys, but the other issue we described earlier is sharing the associated\nfunctionality with other module internals (which have knowledge of the contract\nbut may not themselves be implementers of it). This is handled by\n`Mixin.prototype.api`.\n\n```js\nanswerMixin.api.theAnswer(earth); // \"44!\"\n```\n\nEach of the prototype methods will be “inverted” on `mixin.api` so that the\nreceiver is the first argument. For accessors that have both `get` and `set`,\narity determines which behavior is applied.\n\nStatic methods don’t need to be reflected on `api` because they already work\nlike this.\n\nIt’s up to you what the visibility of any API is. You don’t need to export a\nmixin, and you don’t need to use `extend` or `extendObject` at all:\n\n```js\nfunction createEarth2() {\n  const earth = {};\n  answerMixin.super(earth, '?');\n  return earth;\n}\n\nconst earth2 = createEarth2();\n\nearth2.theAnswer; // undefined\nanswerMixin.api.theAnswer(earth2); // \"42?\"\n```\n\n## How it works\n\nPrivate fields always belong, effectively, to a given constructor’s internal\n[[Construct]] method. When this is called, either immediately or, if applicable,\nat `super()`, the declared fields (or slots, if you prefer) are added to the\nnew `this`.\n\nYou can’t easily invoke [[Construct]] with a specific value of `this` the way\nyou can invoke [[Call]] with a specific `this`. That makes sense — the creation\nof that `this` is a fundamental part of what it’s doing. It walks down the\nconstruction chain (evaluating anything in constructor bodies prior to a\n`super()` call). When it reaches the bottom, it creates a new object that\ninherits from the prototype of `new.target`. Then it walks back up. Right before\nit resumes evaluating a given constructor body is when any fields associated\nwith that constructor get applied.\n\nWhile it isn’t super easy to supply a specific `this` to an arbitrary\nconstructor, it isn’t impossible if the constructor is one that calls `super()`.\nIf that constructor’s own [[Prototype]] is a constructor that returns a new\nobject, that object supplants whatever would have otherwise been the instance —\nand therefore the `this` value of the next constructor up.\n\nWhen the “walk” reenters the subclass constructor, any associated slots will\nbe allocated to its `this`. So what `Mixin` is mainly about is provisioning the\ncorrect object via `Mixin.Super`. It’s really just an identity function! In\neffect, it’s very much like inserting an extra constructor into the chain\nwithout actually mutating the real prototype chain. Though it may seem odd, it\nfits within the existing instantiation model and doesn’t rely on anything\nmagical, though the API that results (partly on account of eschewing magic) is\nless than ideal.\n\nThe module is small and there are tests, so you can check out the source to get\na more complete picture.\n\n[private fields]: https://tc39.github.io/proposal-class-fields/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbathos%2Fprivate-mixin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbathos%2Fprivate-mixin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbathos%2Fprivate-mixin/lists"}