A bundle ends up containing two copies of your library, so a Money object made by one copy fails value instanceof Money inside the other. How would you write a type check for Money that still recognises it — across duplicate copies and across realms?
answer
- class identity is what duplication destroys
- need a token both copies compute
- string-keyed registry, not a fresh symbol
- one well-known symbol customises the operator
- private names are per-class-evaluation
basics
~20 sStop keying the check on class identity. Tag instances with a property whose key is a registry symbol such as Symbol.for("acme.money"), since that key is identical in every copy and every realm, and test for it — optionally exposing it through static [Symbol.hasInstance] so instanceof keeps working.
solid answer
~50 s`instanceof` compares against one specific `Money.prototype` object, so two copies of the library — or two realms — each own a different one and the check fails. The durable answer is a *brand*: a marker whose identity does not depend on which copy created the value. `Symbol.for("acme.money")` is the usual choice, because the global symbol registry is shared, so both copies compute the same symbol; tag instances with a property under that key and test `Boolean(value?.[BRAND])`. You can keep callers' `instanceof` working by defining `static [Symbol.hasInstance](v)` on the class to run that brand test. What does *not* solve this is an ES2022 private-field check like `#brand in value`: it is unforgeable, but each copy declares its own private name, so it fails across copies exactly as `instanceof` does. And the real fix is upstream — dedupe the dependency so only one copy exists.
code
javascript · 18 linesconst BRAND = Symbol.for("acme.money");
class Money {
constructor(amount) {
this.amount = amount;
this[BRAND] = true;
}
static [Symbol.hasInstance](value) {
return Boolean(value?.[BRAND]);
}
}
// stands in for an instance built by a second copy of the library
const fromOtherCopy = { amount: 5, [Symbol.for("acme.money")]: true };
console.log(fromOtherCopy instanceof Money); // true
console.log(new Money(1) instanceof Money); // true
console.log({ amount: 5 } instanceof Money); // falsego deeper
Take away the core fact: instanceof compares against one particular prototype object, so if the class exists twice in memory the check says no. Recognise a marker property as the workaround rather than trying to fix it with a name comparison.
Explain how a brand is made shareable: Symbol.for pulls from a registry keyed by string, so independent copies derive the identical symbol, while Symbol() would mint a different one each time. Know that Symbol.hasInstance is what reroutes the instanceof operator.
Demonstrate the diagnosis under pressure — spot duplicate-copy symptoms beyond the failed check, weigh a forgeable registry brand against an unforgeable private-field check, and say why the packaging-level dedupe is the actual fix rather than the guard you shipped.
Own the API contract: whether your published types promise nominal identity at all, what you commit to by customising instanceof for every consumer and subclass, and how a portable brand interacts with serialisation, versioning and multiple realms over the library's life.
## Why the check fails `value instanceof Money` walks `value`'s prototype chain and asks whether any link is the *same object* as `Money.prototype`. A class expression evaluated twice creates two unrelated function objects with two unrelated prototype objects. So if the bundle contains two resolved copies of the package, there are two `Money` classes in memory. An instance of the first is a perfectly good Money, and it is not `instanceof` the second. The same reasoning, one level up, explains the cross-realm case: an iframe or a separate vm context has its own copy of everything. The unifying diagnosis: `instanceof` tests *object identity of a prototype*, and object identity is precisely the thing that duplication destroys. ## Brands: a marker that survives duplication A brand is a token that both copies can compute independently and agree on. The candidates, from strongest to weakest: **A global-registry symbol.** `Symbol.for(key)` looks the key up in a registry that is shared across realms, returning the identical symbol for the identical string. Both library copies call `Symbol.for("acme.money")` and get the same symbol, so a property under that key is readable by both. ```js const BRAND = Symbol.for("acme.money"); class Money { constructor(amount) { this.amount = amount; this[BRAND] = true; } static isMoney(value) { return Boolean(value?.[BRAND]); } } ``` Symbol keys do not collide with string keys, are skipped by `JSON.stringify`, `Object.keys` and `for...in`, and are namespaced by the string you chose. Put the brand on the prototype rather than on each instance if you want it non-enumerable and shared. **A string tag property**, such as `value?.$type === "acme/Money"`. Weaker — collides more easily, shows up in serialised output — but readable in a debugger and survives a JSON round trip, which is occasionally the point. **Duck typing** on shape (`typeof value?.toCents === "function"`). The loosest option; appropriate when you truly want structural compatibility rather than provenance. ## Keeping instanceof working for callers Callers will write `instanceof` whatever you tell them. `Symbol.hasInstance` lets you make that spelling correct: if the right-hand operand has a `Symbol.hasInstance` method, `instanceof` calls it with the left-hand value and coerces the result to a boolean instead of doing the chain walk. ```js class Money { static [Symbol.hasInstance](value) { return Boolean(value?.[BRAND]); } } ``` Now `otherCopysMoney instanceof Money` is true. Two cautions: the method is inherited too, so a subclass gets the loosened behaviour unless it overrides it; and you have decoupled `instanceof` from actual prototype membership, which will surprise a reader who did not expect a customised operator — document it. ## What does not fix it **ES2022 ergonomic brand checks.** `static isMoney(v) { return #brand in v; }` is genuinely unforgeable — nothing outside the class body can install a private name, so nothing can fake it. But each evaluation of the class body creates a *fresh* private name, so copy A's private name is not present on copy B's instances. It is the correct tool for "was this made by *this exact* class object", and the wrong tool for duplication. It also throws a TypeError if the operand is a primitive, so guard the object-ness first. **`value.constructor.name === "Money"`.** `constructor` is a writable ordinary property, and minifiers rename classes. Fragile twice over. **`Object.prototype.toString.call(value)`.** Reports `"[object Object]"` for every class instance unless you set a `Symbol.toStringTag` — and if you do set one, you have simply built a weaker, string-keyed brand. ## The upstream fix All of this is mitigation. Two copies of a library also means two module-level caches, two registries, two sets of module state, and `instanceof` is usually just the first symptom to surface. The durable answer is to make the package a single instance — dedupe or hoist the dependency, or declare it a peer dependency so consumers resolve one copy. Brand checks are what you ship so that the failure is graceful when that guarantee slips, and what you must ship anyway if values legitimately arrive from another realm.
- Why is Symbol.for("acme.money") usable here when a plain Symbol("acme.money") is not?`Symbol(desc)` mints a brand-new unique symbol every call; the description is only a label, so each library copy would hold a different key and neither could read the other's brand. `Symbol.for(key)` consults the global symbol registry, which is shared across realms, and returns the same symbol for the same string every time — exactly the shared identity the check needs.
- When is an ES2022 private-field brand check the right tool rather than a registry symbol?When you need unforgeability inside a single copy: `#brand in value` cannot be faked, because only code inside that class body can install the private name. Use it to guarantee a value really came from your constructor — for invariant-dependent code paths — and accept that it is deliberately narrow. It fails across duplicate copies and realms, and it throws on primitives, so guard the operand first.
- What is the risk of overriding Symbol.hasInstance on a public class?You have redefined an operator readers assume is a prototype-chain test, so anything relying on that assumption — including subclasses, which inherit your static method — now gets your logic instead. If the brand test is looser than real instance membership, `instanceof` can pass for objects that lack your methods entirely. Keep the implementation trivial, document it, and override it deliberately in subclasses.
saying these in an interview costs you the question
- Suggests comparing constructor.name, which minifiers rename
- Thinks a private-field brand check works across library copies
- Uses Symbol() instead of Symbol.for() for a shared brand
- Believes two copies of a class share one prototype
- Treats Symbol.toStringTag as tamper-proof provenance