Skip to content

RFC: Support __proto__ literal in object initializers #38385

Open

Description

This 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: string};
};

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

Activity

  1. changed the title [-]Support `__proto__` declaration in literal object initializers[/-] [+]Support __proto__ literal in object initializers[/+] on May 7, 2020
  2. changed the title [-]Support __proto__ literal in object initializers[/-] [+]RFC: Support __proto__ literal in object initializers[/+] on May 8, 2020
  3. dhoulb commented on May 24, 2020

    @dhoulb

    Just ran across this and was surprised __proto__ in initialisers wasn't already supported.

    { __proto__: myPrototype } is much cleaner than Object.create(myPrototype), especially when you want to merge in additional properties at the same time.

    I acknowledge the confusion that __proto__ in initialisers is pretty clear, but obj.__proto__ is confusing and should probably be discouraged, but I'd still love to see it supported.

  4. hahanein commented on Sep 15, 2020

    @hahanein

    Also just ran accross this issue.

    FWIW: According to Axel Rauschmayer (@rauschma) [0]:

    I recommend to avoid the pseudo-property proto: As we will see later, not all objects have it.

    However, proto in object literals is different. There, it is a built-in feature and always available.

    [...]

    • The best way to set a prototype is when creating an object – via proto in an object literal or via:
    Object.create(proto: Object) : Object 
  5. ExE-Boss commented on Sep 15, 2020

    @ExE-Boss
    Contributor

    Also, the __proto__ initialiser in object literals is in the process of being moved out of Annex B: tc39/ecma262#2125.

  6. Jack-Works commented on Jan 15, 2021

    @Jack-Works
    Contributor

    supported in PR #42359, behavior:

    __proto__ for a non null | object type is a compile error. {...Q, __proto__: T, ...W} is treated as {...T, ...Q, ...W} at the type level.

  7. Josh-Cena commented on May 4, 2022

    @Josh-Cena
    Contributor

    Nathan Shively-Sanders (@sandersn) asked us to post thoughts in the issue (from #48816), so I'm going to throw in my 2c.

    My idea is:

    It's standard ECMAScript syntax, so even if we don't bring in the whole prototypical semantics, it should at least be special-cased to not mess with valid things like const map: Record<string, string> = { __proto__: null }

    I see several levels at which TypeScript as a language can support __proto__.

    1. (Most ideal) It should have the syntax and corresponding semantics to describe the entire prototype chain. This is related to Support for Object.hasOwn (lib.d.ts and narrowing) #44253 (although I'm surprised no-one brought it up there): Object.hasOwn would have slightly different semantics from "key" in obj, especially if TS wants to support narrowing class instances.
    2. (1) is obviously very hard to achieve given TypeScript's current architecture, so taking one step back, I think Support __proto__ in object literal (type-check only) #48816 is a good compromise, where it not only recognizes __proto__, but also gives its semantics enough love. (From my quick scanning, it types { __proto: p, ...obj } as typeof obj & typeof p.) Note, however, that such compromise is ultimately incorrect, and may hinder us from doing future meaningful work around prototype semantics.
    3. At the very, very minimum, TypeScript should ignore __proto__ (requested in Ignore __proto__ #13933), because after all, this is a special ECMAScript syntax, and is not totally obscure. Imagine TS thinking that super in object literals is just a random identifier. Even today's world where it's typed as any is better than that. Using { __proto__: null } to make a map is still common practice, and it should not prevent the object from being typed as Record<string, string>.

    I simply don't think "we should not think about it" is an adequate way to approach this, especially when there's a PR in place. I understand it's a difficult decision to make, and I would personally go with approach (2) (as it's currently implemented), but be noted about its potential footgun. Expressing class inheritance has the same footgun in TS today, after all.

  8. bakkot commented on May 4, 2022

    @bakkot
    Contributor

    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 considers let 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.

  9. Josh-Cena commented on May 5, 2022

    @Josh-Cena
    Contributor

    I was hesitant to call it "good", because if you actually want to work seriously with maps, you probably should use an actual Map anyways...

  10. bakkot commented on May 5, 2022

    @bakkot
    Contributor

    Nah, { __proto__: null } is a perfectly fine way to work with a string-keyed map.

  11. ExE-Boss commented on May 11, 2022

    @ExE-Boss
    Contributor

    TypeScript considers let dict: Record<string, string> = { __proto__: null } to be a type error, which discourages the right thing and encourages the wrong thing.

    I currently do:

    let dict: Record<string, string> = {
    	// the non-null assertion causes this to become `never`,
    	// which is assignable to a `string`:
    	__proto__: null!,
    };

    The other option is to use as any.

  12. aduh95 commented on Nov 28, 2023

    @aduh95

    Another place where this issue is a pain:

    Object.defineProperty({}, 'someKey', {
      __proto__: null,
      configurable: true,
    });

    This perfectly valid JS code is flagged as invalid by TypeScript (https://www.typescriptlang.org/play?#code/PIIwVgpgxgLgdAEwgMwJYDsIAUBOB7ABwhxgE8AKAbwF8AaAAgHIBnPAWwgGkJTGHKAUPXoB9EQXww8YgFz10AVwA2S2kPpQ86NAHMFOAIYglEOTBwKIa6gEoA3AKA).

  13. dmchurch commented on Mar 19, 2024

    @dmchurch

    Since this is still marked as "Awaiting More Feedback", I'll chime in with the chorus to say that this is seriously hindering my development efforts. Can we please get an update from Microsoft on this 4-year-old issue? At the very least, Ryan Cavanaugh (@RyanCavanaugh) can you explain what further feedback is needed, or remove the tag?

  14. Josh-Cena commented on Mar 20, 2024

    @Josh-Cena
    Contributor

    If this puts more weight on decision making: as MDN's JS docs writer, in recent years I've added a lot more documentation about __proto__, highlighting its appropriateness in object initializers. It is the very first syntax introduced in the Inheritance and the prototype chain guide, even before new and Class.prototype. It is also used a lot more than Object.create throughout our code examples. My hope is that it gets preferred over Object.create in most use cases, especially if there are other known keys, because it's much more statically analyzable.

  15. ljharb commented on Mar 20, 2024

    @ljharb
    Contributor

    It’s also safer, in that Object.create can be tampered with, but literal proto syntax can’t be.

  16. mike-marcacci commented on May 6, 2024

    @mike-marcacci
    Author

    Ryan Cavanaugh (@RyanCavanaugh) and team I hate to be a nag, but is there anything in particular about this that is still "awaiting more feedback" as the label suggests?

    This is a pretty straightforward suggestion that simply brings TypeScript in line with correct ECMA semantics, and there have been no concrete objections in the 4 years it's been open.

    This continues to promote unsafe programming patterns (creating map objects without __proto__: null) that appear in TypeScript tutorials all across the web, and many of our internal codebases are speckled with "TODO" comments explaining some workaround and linking to this issue.

  17. mkermani144 commented on Jul 21, 2024

    @mkermani144

    I'm using such wrappers to bypass the limitation:

    const withProto = <Proto extends object, Base extends object>(
      proto: Proto,
      base: Base,
    ) =>
      ({
        __proto__: proto,
        ...base,
      }) as unknown as Proto & Base;

    Clearly a pain point.

  18. HansBrende commented on Jan 4, 2025

    @HansBrende
    Contributor

    Another thing to think about is that objects created via {__proto__: null} should not implicitly inherit Object properties.

    E.g., ({__proto__: null}).toString() will be a TypeError at runtime, and therefore should not be allowed by TypeScript.

    (In fact, to be even more precise, an object literal should implicitly inherit Object properties if and only if __proto__ is missing, or __proto__ itself inherits Object properties. E.g., ({__proto__: {__proto__: null}}).toString() will also be a TypeError.)

  19. DominoPivot commented on Aug 4, 2025

    @DominoPivot

    The previous suggestions fail to consider how properties of an object shadow those of its prototype, which means they don't need to have compatible types at all.

    Here's a utility function that can recursively infer the types for an entire chain of objects with __proto__, and sample code using it:

    type WithProto<T> =
        T extends { "__proto__": infer P }
        ? Omit<T, "__proto__"> & Omit<WithProto<P>, keyof T>
        : T;
    
    function withProto<T> (value: T) {
        return value as { [K in keyof WithProto<T>]: WithProto<T>[K] };
    }
    
    const zeroVector = Object.freeze({
        x: 0,
        y: 0,
    });
    
    const horizontal = withProto({
        __proto__: zeroVector,
        x: 1,
    });
    // { x: number, readonly y: 0 }
    
    const diagonal = withProto({
        __proto__: { __proto__: zeroVector, x: 1 },
        y: 1,
    });
    // { x: number, y: number }

    PS: I did experiment with explicit inheritance of typeof Object.prototype in the absence of a __proto__, and mapping those properties to never on a __proto__: null, but that just pollutes autocompletion and type hints with said properties.

  20. 0f-0b commented on Oct 14, 2026

    @0f-0b

    Chiming in since this is tagged “Awaiting More Feedback”: an advantage of null-prototype objects (compared to Maps) is that they natively support JSON serialization1.

    Footnotes

    1. Support for parsing is in the proposal process. ↩

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions