Prototypes and inheritance
Every object has a prototype, which is either another object or null. Reading a property the object does not have continues to its prototype, and from there to its prototype, until something has it or the chain ends.
That is the entirety of JavaScript's inheritance mechanism. There is no second one. Classes are built on this rather than beside it, which is why understanding it first makes the classes page read as a shorthand instead of a separate feature.
Two asymmetries do most of the work in this entry. Reading walks the whole chain; writing only ever touches the first object. And the properties an object inherits are real when you read them and invisible to everything that lists.
Create programs/prototypes-and-inheritance.test.ts for this reference and keep it open. Start it with the assertion helpers used on this page:
import {
assert,
assertEquals,
assertFalse,
assertStrictEquals,
} from "@std/assert";
Follow the page as you add and revise the runnable examples below that import.
an object inherits what its prototype has
Deno.test("an object inherits what its prototype has", () => {
const proto = { protoProp: "a" };
type Inheriting = { protoProp: string; objProp: string };
const obj = Object.assign(Object.create(proto) as Inheriting, {
objProp: "b",
});
assertStrictEquals(obj.objProp, "b");
assertStrictEquals(obj.protoProp, "a");
assertEquals(Object.keys(obj), ["objProp"]);
assertStrictEquals(Object.getPrototypeOf(obj), proto);
});
Check programs/prototypes-and-inheritance.test.ts
running 1 test from ./programs/prototypes-and-inheritance.test.ts
an object inherits what its prototype has ... ok (1ms)
ok | 1 passed | 0 failed (1ms)
Object.create(proto) makes an object whose prototype is proto. Reading protoProp finds it one level up, and Object.keys does not mention it, because listing operations consider own properties only, the full set of which the objects as dictionaries page mapped in four listing operations, four answers. A property that is not the object's own is exactly as readable as one that is. That is the feature, and it is also why an object can look emptier than it behaves.
the checker does not model an ad-hoc prototype
Worth knowing before you go far with this. Build the same chain with the literal key and read the inherited property:
Deno.test("the checker does not model an ad-hoc prototype", () => {
const proto = { protoProp: "a" };
const viaLiteral = { __proto__: proto, objProp: "b" };
assertStrictEquals(viaLiteral.protoProp, "a");
assertStrictEquals(Object.getPrototypeOf(viaLiteral), proto);
});
Check programs/prototypes-and-inheritance.test.ts
TS2339 [ERROR]: Property 'protoProp' does not exist on type '{ __proto__: { protoProp: string; }; objProp: string; }'.
assertStrictEquals(viaLiteral.protoProp, "a");
~~~~~~~~~
at file:///programs/prototypes-and-inheritance.test.ts:27:35
error: Type checking failed.
TypeScript treats __proto__: in a literal as an ordinary property named __proto__, so the inherited members are invisible to it even though the run-time chain is genuinely there, which the last assertion proves once the read is pinned:
Deno.test("the checker does not model an ad-hoc prototype", () => {
const proto = { protoProp: "a" };
const viaLiteral = { __proto__: proto, objProp: "b" };
// @ts-expect-error: Property 'protoProp' does not exist on type '{ __proto__: { protoProp: string; }; objProp: string; }'.
assertStrictEquals(viaLiteral.protoProp, "a");
assertStrictEquals(Object.getPrototypeOf(viaLiteral), proto);
});
the checker does not model an ad-hoc prototype ... ok (0ms)
This is the same blindness the iterator helpers page hit in your own iterable can supply the methods, where a hand-assigned Iterator.prototype was "missing" fourteen methods it inherited. Object.create is no better on this axis: it returns any, which is permissive rather than helpful, and the casts in this file are the examples saying what they know. The type system models inheritance through interface extends and class extends, not through prototypes set by hand, and that is the honest argument for classes: inheritance the checker understands.
own and inherited are different questions
Deno.test("own and inherited are different questions", () => {
const proto = { protoProp: "a" };
const obj = { __proto__: proto };
assert("protoProp" in obj);
assertFalse(Object.hasOwn(obj, "protoProp"));
assert(Object.hasOwn(proto, "protoProp"));
});
own and inherited are different questions ... ok (0ms)
in reports what you can read, so it walks the chain and says yes. Object.hasOwn reports what the object itself holds, and says no. Most of the time you want the second, which is the point the dictionaries page made at length in a plain object is never empty: a plain object inherits toString and several others, so in says yes about keys nobody added.
every chain ends at null
Deno.test("every chain ends at null", () => {
const chain: string[] = [];
let step: unknown = { a: 1 };
while (step !== null) {
chain.push(
step === Object.prototype ? "Object.prototype" : "the object",
);
step = Object.getPrototypeOf(step);
}
assertEquals(chain, ["the object", "Object.prototype"]);
const bare = Object.create(null);
assertStrictEquals(Object.getPrototypeOf(bare), null);
});
every chain ends at null ... ok (0ms)
A plain object's chain has two links: itself and Object.prototype. That is where toString, valueOf, and hasOwnProperty come from, and Object.prototype's own prototype is null, which is what stops both the walk in the loop and every property lookup that finds nothing. Object.create(null) opts out entirely, a chain of one, which is what made it the dictionaries page's fix in a null prototype inherits nothing to trip over.
only the first object in the chain is ever mutated
The write below looks like it changes the inherited property. Predict what the prototype holds afterwards:
Deno.test("only the first object in the chain is ever mutated", () => {
const proto = { protoProp: "a" };
const obj = { __proto__: proto } as { protoProp?: string };
assertEquals(Object.keys(obj), []);
obj.protoProp = "x";
assertEquals(Object.keys(obj), ["protoProp"]);
assertStrictEquals(proto.protoProp, "x");
assertStrictEquals(obj.protoProp, "x");
});
Check programs/prototypes-and-inheritance.test.ts
running 5 tests from ./programs/prototypes-and-inheritance.test.ts
...
only the first object in the chain is ever mutated ... FAILED (10ms)
ERRORS
only the first object in the chain is ever mutated => ./programs/prototypes-and-inheritance.test.ts:59:11
error: AssertionError: Values are not strictly equal.
[Diff] Actual / Expected
- a
+ x
FAILURES
only the first object in the chain is ever mutated => ./programs/prototypes-and-inheritance.test.ts:59:11
FAILED | 4 passed | 1 failed (11ms)
error: Test failed
The prototype still holds "a". This is the answer's asymmetry in three lines: reading walks the chain, writing only ever touches the first object, so the assignment created an own protoProp that shadows the inherited one from then on. Object.keys went from [] to ["protoProp"], the prototype keeps its value, and every other object sharing that prototype is unaffected. Correct the prediction to "a":
only the first object in the chain is ever mutated ... ok (0ms)
That is almost always what you want, and it is also why shared state on a prototype behaves so strangely: a prototype holding a mutable array is shared by every object below it, and the first one to reassign the property quietly stops sharing while the others carry on. Prototypes are for shared behavior; keep data in own properties.
sharing one method is the whole point
Deno.test("sharing one method is the whole point", () => {
const personMethods = {
describe(this: { firstName: string }): string {
return `Person named ${this.firstName}`;
},
};
type Person = { firstName: string } & typeof personMethods;
function makePerson(firstName: string): Person {
return Object.assign(Object.create(personMethods) as Person, {
firstName,
});
}
const ada = makePerson("Ada");
const grace = makePerson("Grace");
assertStrictEquals(ada.describe(), "Person named Ada");
assertStrictEquals(grace.describe(), "Person named Grace");
assertStrictEquals(
Object.getPrototypeOf(ada),
Object.getPrototypeOf(grace),
);
assertEquals(Object.keys(ada), ["firstName"]);
});
sharing one method is the whole point ... ok (0ms)
Two objects, one describe. The method body reads this.firstName, and this is whichever object received the call, the mechanism from the value of this, so a single function serves both. This is worth dwelling on because it is exactly how classes are arranged internally: methods live on one shared prototype, per-object data lives in own properties on each instance, which is why Object.keys(ada) lists only firstName. The classes page confirms it in a class is two connected objects, and describe being one function object rather than two is the reason the mechanism exists at all.
instanceof asks about the chain, not construction
Here is the answer to a question the values and references page left open when a clone stopped being a Price. First, an object that no constructor ever touched, then a prediction about what happens when a constructor's prototype property is replaced:
Deno.test("instanceof asks about the chain, not construction", () => {
class Person {
firstName = "";
}
const neverConstructed = Object.create(Person.prototype);
assert(neverConstructed instanceof Person);
assertStrictEquals(
Object.getPrototypeOf(neverConstructed),
Person.prototype,
);
function Widget() {}
const widget = Object.create(Widget.prototype);
assert(widget instanceof Widget);
Widget.prototype = { replaced: true };
assertEquals(widget instanceof Widget, true);
});
Check programs/prototypes-and-inheritance.test.ts
running 7 tests from ./programs/prototypes-and-inheritance.test.ts
...
instanceof asks about the chain, not construction ... FAILED (8ms)
ERRORS
instanceof asks about the chain, not construction => ./programs/prototypes-and-inheritance.test.ts:98:11
error: AssertionError: Values are not equal.
[Diff] Actual / Expected
- false
+ true
FAILURES
instanceof asks about the chain, not construction => ./programs/prototypes-and-inheritance.test.ts:98:11
FAILED | 6 passed | 1 failed (9ms)
error: Test failed
Two findings, one operator. neverConstructed is instanceof Person even though nothing ever called Person, because value instanceof F walks value's prototype chain looking for the object F.prototype, and someone put that object in the chain. That is all the operator does; it is not asking "did this come from that constructor" and cannot ask it. And the failed prediction is the same fact from the other side: instanceof reads .prototype at the moment you ask, so replacing Widget.prototype changed the answer for an object that was not touched in between. Widget had to be a function declaration for that line to work at all, because a class's prototype property is not writable. Correct the prediction to false:
instanceof asks about the chain, not construction ... ok (0ms)
The same object, two different answers: instanceof is a question about a mutable property of a function, asked fresh each time.
a clone leaves the chain behind
Deno.test("a clone leaves the chain behind", () => {
class Tagged {
kind = "tagged";
}
const tagged = Object.create(Tagged.prototype);
const copy = structuredClone(tagged);
assert(tagged instanceof Tagged);
assertFalse(copy instanceof Tagged);
assertStrictEquals(Object.getPrototypeOf(copy), Object.prototype);
});
a clone leaves the chain behind ... ok (0ms)
The values and references page watched a cloned Price stop being a Price and called the missing piece a hidden link to its class. This is that link: a clone is a new object built with Object.prototype as its prototype, nothing about the original's chain travels with the data, so every instanceof against a custom constructor fails on the far side of any boundary that copies. What to do about it for error classes specifically is the designing error types page's what survives a boundary.
instanceof can be answered by anything
Deno.test("instanceof can be answered by anything", () => {
const Even = {
[Symbol.hasInstance](value: unknown): boolean {
return typeof value === "number" && value % 2 === 0;
},
};
assert((4 as unknown) instanceof Even);
assertFalse((3 as unknown) instanceof Even);
assertStrictEquals(typeof Even, "object");
});
instanceof can be answered by anything ... ok (0ms)
Even is a plain object with a [Symbol.hasInstance] method, one more well-known key from the symbols page, and instanceof asks that method when it is present. No chain, no function, no new; nothing here is a class, which the typeof pins.
Put the last three steps together and the conclusion is unavoidable: instanceof is not a type check. It is a configurable question about a chain, and it is reliable only within one program, for objects built locally, against constructors nobody has reassigned. Inside those limits it is genuinely useful, including for narrowing, where the unions and narrowing page counts it among the checks that narrow are ordinary JavaScript. Outside them, a discriminant field is what survives.
isPrototypeOf is the direct question
When you want to ask about the chain without a constructor in the way, isPrototypeOf is the method. Write it the obvious way:
Deno.test("isPrototypeOf is the direct question", () => {
const a = {};
const b = Object.create(a);
const c = Object.create(b);
assert(a.isPrototypeOf(c));
assertFalse(c.isPrototypeOf(a));
assertFalse(a.isPrototypeOf(a));
});
Run deno lint and it objects three times:
error[no-prototype-builtins]: Access to Object.prototype.isPrototypeOf is not allowed from target object
--> programs/prototypes-and-inheritance.test.ts:149:12
|
149 | assert(a.isPrototypeOf(c));
| ^^^^^^^^^^^^^^^^^^
docs: https://docs.deno.com/lint/rules/no-prototype-builtins
error[no-prototype-builtins]: Access to Object.prototype.isPrototypeOf is not allowed from target object
--> programs/prototypes-and-inheritance.test.ts:150:17
|
150 | assertFalse(c.isPrototypeOf(a));
| ^^^^^^^^^^^^^^^^^^
docs: https://docs.deno.com/lint/rules/no-prototype-builtins
error[no-prototype-builtins]: Access to Object.prototype.isPrototypeOf is not allowed from target object
--> programs/prototypes-and-inheritance.test.ts:151:17
|
151 | assertFalse(a.isPrototypeOf(a));
| ^^^^^^^^^^^^^^^^^^
docs: https://docs.deno.com/lint/rules/no-prototype-builtins
no-prototype-builtins is in the recommended set, and its reasons are this page's own material: calling a.isPrototypeOf(...) reaches the method through a's chain, and this entry has already built objects where that lookup finds nothing, because Object.create(null) inherits no methods at all, and objects where a shadowing own property could answer instead. The sanctioned spelling names the function directly:
Deno.test("isPrototypeOf is the direct question", () => {
const a = {};
const b = Object.create(a);
const c = Object.create(b);
assert(Object.prototype.isPrototypeOf.call(a, c));
assertFalse(Object.prototype.isPrototypeOf.call(c, a));
assertFalse(Object.prototype.isPrototypeOf.call(a, a));
});
isPrototypeOf is the direct question ... ok (0ms)
The .call(a, c) supplies a as the receiver explicitly, the move from the value of this's call, apply, and bind name the receiver. The answers themselves: it is transitive, so a counts as a prototype of c from two links up, not just one, and it is not reflexive, which is the one thing about it worth remembering.
__proto__ on Deno is an ordinary key
Deno.test("__proto__ on Deno is an ordinary key", () => {
const proto = { protoProp: "a" };
const table: Record<string, unknown> = {};
table["__proto__"] = proto;
assertEquals(Object.keys(table), ["__proto__"]);
assertStrictEquals(Object.getPrototypeOf(table), Object.prototype);
});
__proto__ on Deno is an ordinary key ... ok (1ms)
Deno replaces Object.prototype.__proto__ with a stub, so writing it creates an ordinary property and reading it gives undefined. The dictionaries page measured this in full in on Deno, __proto__ as a property is inert, including the one route that still works, the plain __proto__: key in a literal that this entry has been using throughout. It is a runtime difference and not a language change: on Node, ({}).__proto__ === Object.prototype is true and assigning to it really does change the prototype, so code meant to run in more than one place should not rely on Deno's version. Either way the advice is the same, and was before: use the Object.* functions, which work everywhere and say what they do.
set the prototype when you create the object
Deno.test("set the prototype when you create the object", () => {
const proto = { protoProp: "a" };
const viaCreate = Object.create(proto) as { protoProp: string };
const viaLiteral = { __proto__: proto };
assertStrictEquals(viaCreate.protoProp, "a");
assertStrictEquals(Object.getPrototypeOf(viaLiteral), proto);
const later = {};
Object.setPrototypeOf(later, proto);
assertStrictEquals(Object.getPrototypeOf(later), proto);
});
set the prototype when you create the object ... ok (0ms)
Object.create and the literal key are the two good ways, because they set the prototype at birth. Object.setPrototypeOf works, and it changes an object's shape after engines have optimised for the old one, so it is the last resort rather than an option.
In practice
- Set an object's prototype when creating it with
Object.createor the literal prototype key. KeepObject.setPrototypeOfas a last resort. - Use
Object.create(null)for a lookup table that should have no inherited keys. - Keep data in own properties and shared behavior on the prototype.
- Use
Object.hasOwnunless inherited properties are intentionally part of the question. - Prefer a class to a hand-built prototype chain so readers and the type checker can see the inheritance.
- Do not treat
instanceofas a general type check; it can fail across boundaries that copy data.