-
Notifications
You must be signed in to change notification settings - Fork 13.3k
Description
🔍 Search Terms
prototype typing
prototype chain types
prototype inheritance
Object.create typing
inherited properties typing
✅ Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
⭐ Suggestion
Note
I haven't been able to find a proposal that generalized problems encountered by typing prototypes structurally (searching is:issue in:title prototypes). Sorry if this is a duplicate.
This proposal introduces structural prototypes to TypeScript: a way to model the prototype of a value or type within the existing structural type system instead of treating prototype lookup as invisible to typing.
Core idea:
- every type has a prototype type
- prototype information composes structurally
- property lookup may include properties inherited through the prototype chain
- ownership and inheritance remain distinct in the type system
This feature is intended to improve typing of APIs and patterns that rely on prototypes, including Object.getPrototypeOf, Object.setPrototypeOf, Object.create, inherited members, and reflection utilities.
It does not attempt to add nominal typing, exact typing, or identity-based reasoning. It keeps prototypes structural.
Motivation
JavaScript uses prototype inheritance at runtime, but TypeScript mostly types objects as if only their own declared properties existed. This creates a gap:
- values can expose inherited properties that are visible and usable at runtime
Object.getPrototypeOfand related APIs cannot be described precisely, currently relying on extensiveanyusage.- reflected information about ownership versus inheritance is mostly unavailable to the type system
- common operations such as reading inherited properties, walking prototypes, or setting prototypes are either under-typed or modeled loosely
Today TypeScript effectively flattens object behavior for most purposes while simultaneously pretending prototype structure is not part of the model. This proposal replaces that omission with an explicit structural model.
The intent is not to make prototypes nominal or identity-sensitive. The intent is to say: if JavaScript exposes prototype lookup structurally at runtime, TypeScript should be able to expose it structurally at compile time.
Goals
- Model prototypes under current structural typing principles.
- Make the type of
Object.getPrototypeOf(x)expressible. - Preserve the distinction between own and inherited properties.
- Allow inherited properties to participate in ordinary property reads.
- Support more accurate typing for prototype-manipulating APIs.
- Keep the model compositional under unions, intersections, and recursion.
Non-goals
- Nominal typing.
- Exact ownership tracking.
- Encoding prototype identity.
- Proving mutation stability of shared prototypes.
- Replacing or redefining JavaScript runtime behavior.
Proposed syntax
Two new type operators are introduced.
Unary prototype query
prototypeof TThis gives the prototype type associated with T.
Infix prototype attachment
T prototype PThis produces a type with the same own properties as T and prototype type P.
This operator is right-associative:
T prototype P prototype Qmeans:
T prototype (P prototype Q)not:
(T prototype P) prototype QThis matches JavaScript prototype chains.
Core model
Every type T has two components:
- its own structure, written normally as
T - its prototype structure, written
prototypeof T
These are both structural.
The type system distinguishes between:
- properties declared directly on
T - properties reachable through the prototype chain
The proposal introduces an implicit notion of apparent structure for property lookup.
Apparent structure
Property reads in JavaScript walk the prototype chain. To model that, define an apparent form:
type Apparent<T> = T & Apparent<Readonly<Omit<prototypeof T, keyof T>>>with recursive expansion continuing until no new keys are introduced (e.g. keyof prototypeof T extends never).
Note
Apparent<T> and other helper types are used in this proposal. This proposal is not suggesting their integration as utility types.
- own properties are visible first
- prototype properties are visible if not shadowed by own properties
- inherited properties are treated as readonly views (see "Readonly inherited properties")
- deeper prototypes contribute recursively
This is not a source-level utility type. It is a specification device describing lookup semantics.
Termination
The expansion stops when the next prototype layer contributes no new keys.
This fixed-point behavior handles:
- finite prototype chains
- self-reference such as
T prototype T - indirect cycles
- recursive prototype types
Defaults
Object types
For a plain object type declaration such as:
type Config = { port: number }the default prototype is:
prototypeof Config = object | nullThis means the type guarantees nothing about its prototype unless specified.
This is a deliberate abstraction boundary for object types.
object | null represents the "top type" of prototypes. All prototypes are be assignable to it.
A type T prototype P is only valid if P extends (is assignable to) object | null.
This means
type Proto<T,P> = T prototype P // invalid
type Proto<T,P extends object | null> = T prototype P // correctInferred values and literals
For values whose construction is known, the compiler may infer a more precise prototype.
For example:
const a = {}may be inferred as:
{} prototype Object(assuming Object = typeof Object.prototype, which is the case as of 5.9.3)
Likewise, other literals or known constructors may infer concrete built-in prototypes.
This separates: declared type-level abstraction from value-level inference of known constructors.
Prototype types
never
prototypeof T being never is appropriate for values such as null and undefined, for which Object.getPrototypeOf throws.
null
null is a valid prototype value in JavaScript representing the absence of a prototype. A type may have prototypeof T = null, representing an object created with Object.create(null), Object.getPrototypeOf(Object.getPrototypeOf({})), or equivalent.
object
object represents a prototype nothing is known about besides its existence.
Primitive types
For primitive types, primitive values may infer prototypes when appropriate. For instance, prototypeof number is Number.
Semantics of key operations
Property access
A property access t.x type-checks if x exists in Apparent<T>.
Indexed access
Indexed access walks prototypes implicitly.
T[K]is resolved against Apparent<T>, preserving shadowing and readonly inherited members.
This ensures consistency between value-level reads and type-level indexed access:
- if
t.xis readable, thenT["x"]is meaningful - inherited properties contribute to indexed access types
keyof
keyof remains own-only.
keyof Treturns only keys declared directly on T, not inherited keys.
Rationale:
- preserves locality
- avoids far-away prototype changes affecting unrelated
keyofs - keeps mapped types predictable
- makes prototype-aware traversal explicit rather than implicit
Prototype-aware key traversal can be expressed separately.
Mapped types
Mapped types operate on own keys only because they are driven by keyof.
This means mapped types do not implicitly preserve prototype structure unless explicitly reattached.
Assignability
Assignability distinguishes between readable and writable requirements.
- Own properties satisfy both read and write requirements.
- Inherited properties (from the prototype chain) are treated as readonly:
- they can satisfy readonly property requirements
- they cannot satisfy mutable (writable) property requirements
This reflects JavaScript behavior:
- reading walks the prototype chain
- writing to an inherited property creates a new own property instead of mutating the prototype
Example
declare function f(x: { readonly a: string; b: number }): void
const x = Object.assign(Object.create({ a: "hi" }), { b: 440 })
f(x) // validThis is valid because:
ais available through the prototype chain and only required asreadonlybis an own property
Invalid case
declare function f(x: { a: string; b: number }): void
const x = Object.assign(Object.create({ a: "hi" }), { b: 440 })
f(x) // errorThis is rejected because:
ais inherited- the target type requires it to be writable
- writing would create a new own property, which is not guaranteed by the source type
Readonly inherited properties
A property reached through the prototype chain is treated as readonly unless it is already an own property of the base type.
This is captured in the apparent form by applying Readonly to prototype contributions.
Rationale:
- reading inherited properties is normal JavaScript behavior
- writing through an inherited property often creates an own shadowing property instead of mutating the prototype
- making inherited members readonly avoids accidentally treating them as ordinary writable local properties
Example:
type T = {} prototype { a: number }Then T["a"] behaves as a readonly property view.
This is a type-system safety rule. It does not claim that JavaScript runtime assignment is impossible; it prevents accidental confusion in the type model.
Shadowing
Own properties shadow inherited properties by name.
Example:
type T = { a: string } prototype { a: number; b: boolean }Then:
T["a"]isstringT["b"]isreadonly boolean
The own declaration wins.
Union and intersection behavior
Prototype information remains structural.
Unions
type U = (A prototype P) | (B prototype Q)Then:
prototypeof U = P | Qand property lookup behaves as it normally does for unions.
Intersections
type I = (A prototype P) & (B prototype Q)This refines both own structure and prototype structure under normal structural rules.
The intended reading is not "multiple runtime prototypes", but "a type whose apparent contract satisfies both sides structurally".
Recursive and cyclic prototypes
This proposal allows recursive prototype types.
Examples:
type Crazy<T> = T prototype Crazy<T>and even degenerate forms such as:
T prototype TThese are valid if they stabilize under fixed-point apparent lookup.
For example, Crazy<T> contributes no keys beyond those already in T, so its apparent structure stabilizes to T.
Likewise, indirect prototype cycles are tolerated as long as no new properties are introduced indefinitely.
The checker use the same kinds of cycle detection, caching, and recursion-limiting techniques already used for recursive types.
Standard library typing examples
Object.getPrototypeOf
declare function getPrototypeOf<T extends {}>(obj: T): prototypeof TReflect.getPrototypeOf
declare function getPrototypeOf<T extends object>(target: T): prototypeof TObject.setPrototypeOf
declare function setPrototypeOf<T extends {}, P extends object | null>(obj: T, proto: P): T prototype PsetPrototypeOf could also return asserts obj is T prototype P because it mutates obj.
Passing a non-object proto or obj doesn't throw (unlike stricter Reflect.setPrototypeOf) but it is a noop.
Reflect.setPrototypeOf
declare function setPrototypeOf<T extends object, P extends object | null>(obj: T, proto: P): T prototype PObject.create
declare function create<P extends object | null>(proto: P): object prototype PAdditional overloads may model property descriptors and own members.
Examples
Basic attachment
type Parent = { host: string }
type Child = { port: number } prototype ParentThen:
keyof Childis"port"prototypeof ChildisParentChild["host"]isreadonly stringChild["port"]isnumber
Shadowing
type T = { a: string } prototype { a: number; b: boolean }Then:
T["a"]isstringT["b"]isreadonly boolean
Value inference
const x = Object.assign(Object.create({ a: 1 }), { b: 2 })A possible inferred type is:
{ b: number } prototype { a: number }Prototype-aware assignability
declare function takesAB(x: { a: string; b: number }): void
const x = Object.assign(Object.create({ a: "hi" }), { b: 440 })
takesAB(x) // validPrototype-aware readonly
type T = {} prototype { a: number }
declare let t: T
t.a // ok
t.a = 1 // error under inherited-readonly ruleRationale for own-only keyof
A major design choice is keeping keyof local.
If keyof walked prototype chains:
- it would no longer be a local operator
- mapped types would become prototype-sensitive by default
- nested prototype changes could invalidate distant code unexpectedly
- implementation and mental model complexity would increase significantly
By keeping keyof own-only:
- local structure remains simple
- prototype lookup is explicit where it matters
- mapped types remain predictable
- the only implicit prototype-walking operation is indexed access
This is a deliberate balance between fidelity and simplicity.
Soundness considerations
This proposal does not attempt to solve all mutation unsoundness in JavaScript.
In particular:
- shared mutable prototypes can still change behind the type system's back
- prototype writes from external aliases remain possible
- runtime assignment to an inherited property may create an own property shadow instead of mutating the prototype
These are already part of JavaScript's behavior and align with existing TypeScript tradeoffs around mutability and aliasing.
Structural prototypes do not create a new category of unsoundness; they expose an existing runtime mechanism in a structured way.
The inherited-readonly rule reduces one common source of confusion without claiming full immutability.
Why structural rather than nominal
A nominal design would attempt to preserve prototype identity or class lineage. That is not the direction of this proposal.
TypeScript already centers structural compatibility. Prototypes are therefore be modeled the same way:
- prototype information is a structural contract
- unions and intersections compose structurally
- apparent property lookup is structural
- no runtime identity assumptions are introduced
The proposal is intentionally minimal in this respect.
Alternatives considered
Ignore prototypes entirely
This is the status quo. It leaves reflection and prototype APIs under-typed and forces the checker to overlook a real source of properties.
Make keyof walk prototypes
Rejected because it spreads prototype sensitivity too broadly and makes many local operations non-local.
Preserve writable inherited properties
Rejected because it allows accidental, implicit property creation from mutation of prototype-origin members through flattened derived object aliases.
Model prototypes nominally
Rejected because it conflicts with TypeScript's structural design and greatly increases complexity.
Backwards compatibility
This proposal is largely additive.
Potential impacts include:
- more precise typing of inherited reads
- new readonly errors for inherited-only properties if value inference becomes prototype-aware
- improved typings for standard library reflection APIs
This design favors conservative defaults for declared object types to minimize disruption.
Related issues (non exhaustive)
This proposal overlaps with several existing TypeScript issues, but is not identical to any one of them. Most existing discussions focus on one symptom at a time: typing specific prototype APIs, inherited property behavior, or gaps between JavaScript's runtime object model and TypeScript's structural model.
- RFC: Support __proto__ literal in object initializers #38385: discusses gaps in typing around prototype-manipulating APIs and highlights the lack of a first-class way to represent prototype relationships.
- No way to type an object with null prototype #1108: early recognition that TypeScript’s structural model does not account for JavaScript’s prototype chain semantics, motivating a more faithful model.
- Object.getPrototypeOf returns
anyinstead ofobject | null#22875: focuses on inconsistencies between property access and type queries, which your proposal addresses by making lookup explicitly prototype-aware. - Module Namespace Exotic Objects are incorrectly typed as if they have an Object prototype #32646: explores limitations in modeling inherited vs own properties, directly aligning with your ownership vs inheritance distinction.
- Inference for Object.isPrototypeOf() #37141: raises broader concerns about accurately modeling JavaScript object behavior, which this proposal systematizes through structural prototypes.
Open questions
- How precisely should value inference attach concrete built-in prototypes? It is possible inferring too eagerly could break existing code, such behavior could be walled behind a new flag
strictPrototypeInference. - How should index signatures interact with inherited readonly lookup in detail?
- Which standard library APIs should be updated initially?
- Should there be syntax for prototype-aware key extraction in the standard library, or should such helpers remain user-defined?
Conclusion
Structural prototypes extend TypeScript's existing structural model to a part of JavaScript it currently mostly ignores.
The key idea is:
- keep prototypes structural
- keep ownership distinct from inheritance
- allow property lookup to reflect runtime prototype reads
- preserve locality for most existing type operators
This approach improves precision for prototype-heavy code without forcing TypeScript into nominal or exact typing.
It does not reinvent JavaScript's object model. It simply stops pretending prototypes are absent.
📃 Motivating Example
// A prototype-oriented helper library written in ordinary JavaScript style.
// A prototype object with shared behavior
const nodeProto = {
kind: 'node',
id: -1,
describe(this: { id: string }) {
return `node:${this.id}`;
},
};
type NodeProto = typeof nodeProto;
// "pseudo prototype" workaround for thi example
/**/
type Apparent<T> = T & Readonly<Omit<NodeProto, keyof T>>;
/*/
type Apparent<T> = T & Apparent<Readonly<Omit<prototypeof T, keyof T>>>
/*/
// Create a value with own data and shared prototype behavior
type UNode = {
id: string;
name: string;
};
/*
Today, TypeScript more or less loses the prototype information here.
A structural-prototype model could infer:
const user: UNode prototype UProto
*/
const user: Apparent<UNode> = Object.assign(Object.create(nodeProto), {
id: 'u1',
name: 'Ada',
} satisfies UNode);
// Inherited reads would be typed naturally
type C1 = Test<Same<typeof user.id, string>>; // own, shadows prototype
type C2 = Test<Same<typeof user.name, string>>; // own
type C3 = Test<Same<typeof user.kind, string>>; // readonly (inherited)
type C4 = Test<Same<typeof user.describe, (this: { id: string }) => string>>; // inherited method
// But inherited members are readonly views, which matches JS better:
// @ts-expect-error
user.kind = 'other';
// @ts-expect-error
user.describe = () => '';
// own keys stay local and predictable
type UserKeys = keyof typeof user;
// "id" | "name"
// while indexed access can still reflect inherited readable members
type C5 = Test<Same<(typeof user)['kind'], typeof user.kind>>;
type C6 = Test<Same<(typeof user)['describe'], typeof user.describe>>;
// Reflection APIs become precisely typed
/**/
type Proto = NodeProto;
/*/
type Proto = prototypeof typeof user;
/*/
const proto: Proto = Object.getPrototypeOf(user);
type C7 = Test<Same<typeof proto.kind, string>>;
type C8 = Test<Same<typeof proto.describe, (this: { id: string }) => string>>;
// Ownership-sensitive utilities become expressible
declare function hasOwn<T, K extends PropertyKey>(obj: T, key: K): K extends keyof T ? true : boolean;
// keyof only gets provably own keys. to retrieve keys from the prototype chain too, use keyof Apparent<T>
const c9 = hasOwn(user, 'name');
type C9 = Test<Same<typeof c9, true>>; // own
// boolean for this value at runtime, because we don't know
// knowing would require exact types, which is out of scope for this proposal
const c10 = hasOwn(user, 'kind');
type C10 = Test<Same<typeof c10, boolean>>;
// Assignability stays structural but respects writable vs inherited
declare function takesReadableNode(x: { readonly kind: string; id: string }): void;
takesReadableNode(user); // ok
declare function takesWritableNode(x: { kind: string; id: string }): void;
takesWritableNode(user); // error: kind is inherited, not own-writable
type Test<T extends true> = T;
type Same<Actual, Expected> = [Actual] extends [Expected] ? ([Expected] extends [Actual] ? true : false) : false;💻 Use Cases
-
What do you want to use this for?*
I want to use this to type ordinary JavaScript code that builds objects through prototypes rather than classes, especially patterns involvingObject.create,Object.assign,Object.getPrototypeOf, inherited property reads, and ownership-sensitive checks likehasOwn. The goal is to let TypeScript describe these values structurally, including both their own properties and the contract of their prototype, without introducing nominal classes or identity-based reasoning. -
What shortcomings exist with current approaches?
Current TypeScript models these patterns weakly because prototype relationships are mostly invisible to the type system. Objects created throughObject.createmay expose inherited properties and methods at runtime, but TypeScript cannot describe that structure precisely. As a result, APIs likeObject.getPrototypeOfand prototype-based object construction lose useful type information, inherited property reads are under-modeled, and the distinction between own and inherited properties is largely unavailable statically. TypeScript is strong at typing object literals and class instances, but much weaker at typing prototype-built objects even though JavaScript exposes them structurally. -
What workarounds are you using in the meantime?
The current workaround is to manually flatten prototype information into ordinary object types, duplicate inherited members onto the apparent object shape, or fall back to loose typings such asany, broad object types, assertions, or custom helper types. In practice this means either losing precision or writing types that do not reflect the actual runtime structure, especially forObject.create,Object.getPrototypeOf, and inherited members.