RFC: Support __proto__ literal in object initializers
See original GitHub issueThis is a new issue to specifically propose and elaborate on a feature I raised in this comment on #30587.
Search Terms
- __proto__
- prototype
- object literal
- object spread
- object initializer
Suggestion
While accessing or mutating an existing object via the Object.protptype.__proto__
getter/setter is deprecated, to the best of my knowledge defining the prototype of a new object via the object initializer __proto__
literal is very much encouraged.
The rules for specifying the prototype of a new object via these semantics are very well-specified and safe. See the relevant section of the spec here.
Basic support
Given the following:
const foo = {
__proto__: { a: "a" },
b: "b"
};
Typescript currently thinks that foo
is the following shape:
type foo = {
["__proto__"]: { a: string };
b: string;
};
when in reality, it is:
type foo = {
a: string;
b: string;
};
TypeScript should be able to correctly detect the type of this object initialization.
Strict validity checks
Additionally, TypeScript should prevent invalid __proto__
assignments that are “ignored” by the spec, and require all values to be null
or an object. This should fail validation:
const invalid = { __proto__: "hello" }
Correct handling of computed properties
It’s important to note that per the spec, __proto__
literals are not the same as regular property assignments.
This object initialization, for example:
const foo = {
__proto__: { a: "a" },
b: "b",
["__proto__"]: { c: "c"},
};
creates an object of the following shape:
type foo = {
a: string;
b: string;
["__proto__"]: { c: "c"};
};
Given this, I would recommend that a __proto__
literal be forbidden in type/interface definitions, such that this is considered a syntax error:
type foo = {
__proto__: { a: string }
}
while this is an allowable way to specify a property named __proto__
on the type foo
.
type foo = {
["__proto__"]: string
}
Use-Cases & Examples
This feature allows TypeScript to correctly understand the shape of objects defined with standard JS semantics. While this pattern isn’t especially prevalent, it is an important feature of the language, and should be much more common in one particular use-case where TypeScript currently has a rather severe blind spot:
TypeScript currently PREVENTS the creation of safe indexed objects derived from existing indexed objects. For example:
Given this object, and the goal of “spreading” it into a new map:
// All safe map objects MUST have a `null` prototype.
const someMapObject: { [key: string]: boolean } = Object.create(null);
The following is UNSAFE, and probably the most common approach I see people using. TypeScript should catch this, and should issue a compile-time error. See bug #37963.
const unsafeSpreadMapObject: { [key: string]: boolean | undefined } = {
...someMapObject,
foo: false
};
console.log(typeof unsafeSpreadMapObject["constructor"]);
// => function
console.log(typeof safeSpreadMapObject["foo"]);
// => boolean
The following is also UNSAFE. While using Object.assign
and Object.create
is a perfectly valid alternative, the any
returned by Object.create
propagates through the statement and breaks type safety. (Perhaps the result of Object.create
should be unknown
instead of any
?)
const unsafeAssignMapObject: { [key: string]: boolean | undefined } = Object.assign(
Object.create(null),
{ foo: "this is not boolean" }
);
console.log(typeof safeSpreadMapObject["constructor"]);
// => undefined
console.log(typeof safeSpreadMapObject["foo"]);
// => string
This is the SAFE way to accomplish this while using object spreads, but TypeScript currently forbids it, since it lacks support for the __proto__
literal, and incorrectly believes a property of type null
is being defined:
const safeSpreadMapObject: { [key: string]: boolean | undefined } = {
__proto__: null,
...someMapObject,
foo: false
};
console.log(typeof safeSpreadMapObject["constructor"]);
// => undefined
console.log(typeof safeSpreadMapObject["foo"]);
// => boolean
Checklist
My suggestion meets these guidelines:
- 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, etc.)
- This feature would agree with the rest of TypeScript’s Design Goals.
References
Issue Analytics
- State:
- Created 3 years ago
- Reactions:23
- Comments:9 (7 by maintainers)
Top GitHub Comments
supported in PR #42359, behavior:
__proto__
for a nonnull | object
type is a compile error.{...Q, __proto__: T, ...W}
is treated as{...T, ...Q, ...W}
at the type level.Very concretely:
{ __proto__: null }
for a dictionary isn’t just common, it’s also good; having a dictionary without__proto__: null
is just incorrect. TypeScript considerslet dict: Record<string, string> = { __proto__: null }
to be a type error, which discourages the right thing and encourages the wrong thing. That’s bad. And fixing this particular thing doesn’t require getting the full semantics of prototypes into the type system; any of the three options mentioned in the previous comment would fix it, and would be a strict improvement on the current very-incorrect semantics.As mentioned above,
__proto__
in object literals is a standard part of ECMAScript. It is not deprecated or restricted to strict-only or any of that. TypeScript should not neglect it.