Code Monkey home page Code Monkey logo

proposal-object-freeze-seal-syntax's Introduction

Object.freeze and Object.seal syntax

Rationale

Object.freeze and Object.seal are both useful functions, but they aren't particularly ergonomic to use, especially when dealing with deeply nested objects or trying to do complex things like create a superset of a frozen object. Also even frozen objects can be modified via their prototype chain:

> const x = Object.freeze({});
undefined
> x.foo
undefined
> Object.prototype.foo = 3;
3
> x.foo
3

To prevent this you can use Object.freeze({ __proto__: null }) or Object.freeze(Object.assign(Object.create(null), {})) to ensure that frozen objects cannot be modified via their prototype chain.

> const x = Object.freeze({ __proto__: null });
undefined
> x.foo
undefined
> Object.prototype.foo = 3;
3
> x.foo
undefined

In addition, it would be useful to have these syntaxes in other places. It'd be useful to seal a destructuring expression, or freeze function arguments to create immutable bindings.

Sketches

Basic freezing of an object

const foo = {#
  a: {#
    b: {#
      c: {#
        d: {#
          e: [# "some string!" #]
        #}
      #}
    #}
  #}
#}
Click here to see the desugared version
const foo = Object.freeze({
  __proto__: null,
  a: Object.freeze({
    __proto__: null,
    b: Object.freeze({
      __proto__: null,
      c: Object.freeze({
        __proto__: null,
        d: Object.freeze({
          __proto__: null,
          e: Object.freeze([ "some string!" ])
        })
      })
    })
  })
})

Basic sealing of an object

const foo = {|
  a: {|
    b: {|
      c: {|
        d: {|
          e: [| "some string!" |]
        |}
      |}
    |}
  |}
|}
Click here to see the desugared version
const foo = Object.seal({
  __proto__: null,
  a: Object.seal({
    __proto__: null,
    b: Object.seal({
      __proto__: null,
      c: Object.seal({
        __proto__: null,
        d: Object.seal({
          __proto__: null,
          e: Object.seal(["some string!"])
        })
      })
    })
  })
})

Sealing a functions destructured options bag

function ajax({| url, headers, onSuccess |}) {
  fetch(url, { headers }).then(onSuccess)
}
ajax({ url: 'http://example.com', onsuccess: console.log })
// throws TypeError('cannot define property `onsuccess`. Object is not extensible')
Click here to see the desugared version
function ajax(_ref1) {
  const _ref2 = Object.seal({ url: undefined, headers: undefined, onSuccess: undefined })
  Object.assign(_ref2, _ref1)
  let url = _ref2.url
  let headers = _ref2.headers
  let onSuccess = _ref2.onSuccess

  fetch(url, { headers }).then(onSuccess)
}
ajax({ url: 'http://example.com', onsuccess: console.log })
// throws TypeError('cannot define property `onsuccess`. Object is not extensible')

Freezing a functions destructured options bag

function ajax({# url, headers, onSuccess #}) {
  url = new URL(url) // throws TypeError('cannot assign to const `url`')
  fetch(url, { headers }).then(onSuccess)
}
ajax({ url: 'http://example.com', onSuccess: console.log })
Click here to see the desugared version
function ajax(_ref1) {
  const _ref2 = Object.seal({ url: undefined, headers: undefined, onSuccess: undefined }) // seal now, const later
  Object.assign(_ref2, _ref1)
  const url = _ref2.url
  const headers = _ref2.headers
  const onSuccess = _ref2.onSuccess

  url = new URL(url) // throws TypeError('cannot assign to const `url`')
  fetch(url, { headers }).then(onSuccess)
}
ajax({ url: 'http://example.com', onSuccess: console.log })

Potential extensions - sketches

Sealed function param bindings

function add(| a, b |) {
  return a + b
}
add(2, 2, 2) === 6
// throws TypeError('invalid third parameter, expected 2`)
Click here to see the desugared version
function add(_1, _2) {
  if (arguments.length > 2) {
    throws TypeError('invalid third parameter, expected 2')
  }
  let a = arguments[0]
  let b = arguments[1]

  return a + b
}
add(2, 2, 2) === 6
// throws TypeError('invalid third parameter, expected 2`)

Frozen function param bindings

function add1(# a #) {
  a += 1 // throws TypeError `invalid assignment...`
  return a
}
add1(1) === 2
Click here to see the desugared version
function add1(_1) {
  if (arguments.length > 1) {
    throws TypeError('invalid second parameter, expected 1')
  }
  const a = arguments[0]

  a += 1 // throws TypeError `invalid assignment...`
  return a
}
add1(1) === 2

Extending syntax to general destructing for better type safety

const foo = { a: 1, b: 2 }
const {| a, b, c |} = foo
// Throws TypeError 'invalid assignment to unknown property c'
Click here to see the desugared version
const foo = { a: 1, b: 2 }
if (!('a' in foo)) throw new TypeError('invalid assignment to unknown property a')
const a = foo.a
if (!('b' in foo)) throw new TypeError('invalid assignment to unknown property b')
const b = foo.b
if (!('c' in foo)) throw new TypeError('invalid assignment to unknown property c')
const c = foo.c
// Throws TypeError 'invalid assignment to unknown property c'

proposal-object-freeze-seal-syntax's People

Contributors

keithamus avatar deltaevo avatar msegado avatar morkro avatar xtuc avatar graingert avatar uzitech avatar

Stargazers

Lorenzo Bloedow avatar 즈눅 avatar  avatar  avatar Hyunwook Nam avatar Denis Tokarev avatar Patrick Smith avatar Mark Penner avatar James H. avatar Lauren Yim avatar Luma avatar Dominik Piekarski avatar  avatar Jorge Gonzalez avatar James Yang avatar ud2 avatar Kevin Koshiol avatar Oisin Grehan avatar  avatar Marina Miyaoka avatar  avatar Amir Irani avatar Vahagn Mkrtchyan avatar Kento TSUJI avatar  avatar J. Pichardo avatar Jed Fox avatar Scott avatar Ryoji Miyazato avatar Leo avatar Simon Kjellberg avatar Andrew Chung avatar Steven avatar Kristóf Poduszló avatar Geoffrey Dhuyvetters avatar Jacob Parker avatar Indrajith Bandara avatar Linus Långberg avatar  avatar Tom Bonnike avatar  avatar Krzysztof Borowy avatar Felix Billon avatar Michael avatar  avatar kotborealis avatar Atakan Goktepe avatar mvirbicianskas avatar Aleh Kashnikaў avatar Divjot Singh avatar Bo Lingen avatar  Matthew Phillips avatar Furkan Tunalı avatar Caleb Cox avatar Lucas Bento avatar Jeremiah Senkpiel avatar Michael Wagner avatar azu avatar Trotyl Yu avatar

Watchers

 avatar HE Shi-Jun avatar James Cloos avatar Denis Pushkarev avatar Trotyl Yu avatar Nicolò Ribaudo avatar ud2 avatar Ryan Zimmerman avatar Andrew Chung avatar  avatar Jed Fox avatar  avatar  avatar  avatar

proposal-object-freeze-seal-syntax's Issues

How should frozen syntax interact with destructuring assignment?

The more I think about this proposal, the more I like it! But, there are some cases where I don't really know what the natural semantics would be, for example:

let x;
{# x #} = { x: 1 };

let {# y #} = { y: 1 };

const {# z #} = { x: 1 }

The question from here is: are those two variable bindings const, the way that arguments are const?

One constraint here is that it'd be complicated to change the "const"-ness of a variable after it's created. That's not something that JS environment records do right now; in real implementations, it would probably take some extra runtime bookkeeping that we otherwise got rid of when upgrading to ES6 const. The dynamic const issue comes up in the x case: imagine if we did that destructuring binding in a branch. What would it do? Best to avoid these semantics IMO.

Given that constraint, I see a few options:

  • SyntaxError for all frozen destructuring binding: Since we can't make all of the cases work, we can just prohibit everything to be "on the safe side". People will learn to use { } on the left hand side of a destructuring assignment.
  • Drop the const-ness for destructuring binding: We could just ignore the # in this case and make {# #} be the same as { } on the left hand side of a destructuring binding.
  • Only permit in the same statement with let, and make it const: In a case that's just like y, we could interpret the statement as if it were const { y } = { y: 1 }. This choice is a little unfortunate, since otherwise two lines like used for x are the same as y (and some implementations even desugar internally this way I believe).
  • SyntaxError unless it's used in a const binding, and then it's ignored: This way we'd permit z, but not x or y.`

What do you think of these options? Have a favorite? Do you have any alternative ideas?

Null only as default prototype

Is null the only prototype that these objects may have?

What would be the result of this code?

const frozen = {#
    __proto__: Map.prototype,
    x: 1.5,
    y: 4.5
#};

Reflect.getPrototypeOf(frozen); // Map.prototype or null?
"has" in frozen; // true or false?

I think that these should be Map.prototype and true appropriately, and that null should merely be the default prototype, if left unspecified.

Would these semantics make any sense in the eyes of TC39? Could this be considered to be a part of the proposal?

Pattern matching connection

This proposal seems great in conjunction with @bterlson's pattern matching proposal. In particular, using sealed object (and maybe array) literals is a good way to assert that there is nothing else, while at the same time not giving multiple meanings to an object literal on the left-hand side.

* for freeze (snowflake)

As for me, * character fits better for freeze operation

const foo = {*
  a: {*
    b: {*
      c: {*
        d: {*
          e: [* "some string!" *]
        *}
      *}
    *}
  *}
*}

Confusion between privacy, frozen, const and extraneous properties

This proposal adds sugar syntax that serves three very different purposes:

  • Create immutable objects (frozen objects that don't inherit Object.prototype)
  • Destructure function arguments into constants
  • Forbid extraneous properties on destructured function arguments

I believe that using the same syntax for these different purposes is likely to cause confusion, even more so when the symbol # is already used to mark private class fields. In fact, the way this proposal is phrased seems to already confuse the meaning of frozen objects and constants.

Create immutable objects

// proposed syntax
let p1 = {# x: 0, y: 0 #};
p1.x = 5; // TypeError in strict mode

// current alternative
let p2 = Object.freeze(Object.setPrototypeOf({ x: 0, y: 0 }, null));
p2.x = 5; // TypeError in strict mode

Note that I used Object.setPrototypeOf instead of a combination of Object.create(null) and Object.assign because the latter does not preserve getter properties.

While I would like to see some sugar syntax added to JavaScript for this, I don't think the use of {# ... #} for this is intuitive or legible, especially when used inside a class with private fields:

get position() {
    return {# x: this.#x, y: this.#y #};
}

Destructure arguments into constants

// proposed syntax
function horiz({# x, y #}) {
    y = 0; // TypeError
    return {x, y};
}

// current alternative
function horiz(point) {
    const {x, y} = point;
    y = 0; // TypeError;
    return {x, y};
}

I don't think we need new syntax for this, but if we did, it should arguably use the const keyword.

This proposed syntax not only introduces a new way to create constants, but also makes destructuring in function arguments work differently than in an assignment:

let numbers = { odd: [1, 3, 5], even: [0, 2, 4] };
let {# odd, even #} = numbers;  // is this allowed? what does it do?

Forbid extra properties on destructured object

function ajax({| url, headers, onSuccess |}) {
  fetch(url, { headers }).then(onSuccess)
}

ajax({ url: "http://example.com", onsuccess: console.log });
// throws TypeError("cannot define property `onsuccess`. Object is not extensible")

The error message "Object is not extensible" is incorrect. The object passed to the function is very much extensible. This feature seems out of place in this proposal, as it has nothing to do with freezing/sealing objects.

This syntax also does not provide a mean to forbid extraneous positional arguments.

Frozen classes

It might also be useful to extend the syntax to classes?

class P {#
  frozenPrototypeMethod () {
  }
  static frozenStatic () {
  }
#}

where both the prototype and class object are frozen.

Potential syntactic conflict with private fields in object literals

It might be useful for object literals to have strong encapsulation too, just like classes do. At some point, this was part of my "unified class features" proposal (which evolved into the decorators proposal). I was proposing a syntax like this:

let x = {
  #p: 1,
  inc() { return this.#p++ }
};
x.inc();  // => 1
x.inc();  // => 2

The idea here is that #p is only accessible inside the object literal. I haven't pushed the idea any further since that restricted scope seems too limited for a lot of reasonable applications, but I'd like to be able to consider extending this way in the future. Maybe we'll make some other construct for setting up the scope of #p. I think demand for this sort of thing will grow as more people develop libraries in a functional style and more and more people encounter issues evolving libraries that are based on everything being public (or based on closures or WeakMaps, which have performance and ergonomics issues).

Unfortunately, there's an ambiguity between your proposal and my shelved one: If the parser is running on-line and sees a character sequence like {#p (to start things off), how should it interpret it? A frozen object literal, or a private field of an object literal? It's a tall order to look ahead to see if it's closed by #}--TC39 generally tries to avoid the need to look very far ahead in the grammar.

A couple of ideas offhand to eliminate the ambiguity (neither is very fun):

  • Require spaces after or before #. So the syntax for frozen objects would be {# x: y #} and this would be a SyntaxError: {#x:y#}. This syntax disambiguates because you can't put a space between after the # in private field access. I'm not sure if this is acceptable; JS doesn't tend to require this kind of whitespace.
  • Switch which characters you use as brackets. The only one that comes to mind is {< x: y >}. I think that would work for all of your cases, but I'm not positive. I'm not sure if this is ergonomic/evocative enough; it seems like a number of people were really looking forward to # in some form for frozen objects.

Maybe it's not so important to make room for a future private field in object literals proposal, or maybe that proposal should have some other kind of syntax, though.

Freeze as the primary syntax

In thinking about the syntax, with both seal and freeze as the variations, it's worth thinking about aesthetic precedence.

For example, {# obj #} might seem less aesthetically pleasing to most than {| obj |} (while this is subjective, there's likely a rough average towards the latter due to greater shape congruence between the different symbols in this case).

For this reason, and with these examples, I would suggest that freeze should be the priority aesthetic. For the reason that the guarantees of freezing provide the most useful correctness and security guarantees.

So effectively I would be suggesting with the current syntax that they should be swapped around in meaning - {| |} for freeze, and {# #} for seal.

Deep freeze shorthand

I think the most common use case for the freeze syntax is deep freeze, so I propose a shorthand to make it more readable

Syntax:

var foo = ## { 
                     bar :{ 
                       prop: ‘val’ 
                    } 
}

A ‘##’ followed by the object being frozen. A potential benefit of having the frozen syntax precede the object is that it allows it to be used with variables.

Ex.

var foo = { bar: { prop: ‘val’ } }

untrustedCode( ##foo );

"Freezing an functions destructured options bag" example seems incorrect

As written, the new syntax example contains what look like implementation details (including the _ref1 variable), and the desugared version uses let instead of const... I'm guessing these were just copy/paste/editing errors and that it was intended to be more like the following?

function ajax({# url, headers, onSuccess #}) {
  url = new URL(url) // throws TypeError('cannot assign to const `url`')
  fetch(url, { headers }).then(onSuccess)
}
ajax({ url: 'http://example.com', onSuccess: console.log })

...desugars to...

function ajax(_ref1) {
  const _ref2 = Object.seal({ url: undefined, headers: undefined, onSuccess: undefined }) // seal now, const later
  Object.assign(_ref2, _ref1)
  const url = _ref2.url
  const headers = _ref2.headers
  const onSuccess = _ref2.onSuccess

  url = new URL(url) // throws TypeError('cannot assign to const `url`')
  fetch(url, { headers }).then(onSuccess)
}
ajax({ url: 'http://example.com', onSuccess: console.log })

How does this interact with the record/tuple proposal?

I understand that freezing/sealing objects will still be a valuable feature to have, even if/when the record/tuple proposal comes through. However ...

  • A number of use cases this proposal solves will also be solved by the record/tuple proposal - would the decreased usefulness of Object.freeze() and Object.seal() still create enough demand to add special syntax for it?
  • The currently proposed freeze syntax is extremely similar to the currently proposed record syntax: {# ... #} vs #{ ... }

If record/tuples gets through, are there plans to still continue pushing this proposal? If so, how would it accommodate the issues described above?

Overloading the # symbol

Private class fields also use the # symbol, is it worth avoiding the # symbol to prevent any confusion?

Use syntax at root only?

Your example:

const foo = {#
  a: {#
    b: {#
      c: {#
        d: {#
          e: [# "some string!" #]
        #}
      #}
    #}
  #}
#}

The syntax looks a bit tedious to write and read to me. I'm just wondering why the freezing or the sealing is not recursive.

My proposal:

const foo = {#
  a: {
    b: {
      c: {
        d: {
          e: [ "some string!" ]
        }
      }
    }
  }
#}

or

const foo = {|
  a: {
    b: {
      c: {
        d: {
          e: [ "some string!" ]
        }
      }
    }
  }
|}

Which would behaves the same. To me it's not a regular JS Object anymore, it's an immutable record.

What do you think?

Why not Arrays too?

This could be especially useful for the destructuring bind case, to have sealed arrays. But also, frozen array literals could be useful for when you have an array of constants that shouldn't change for semantic reasons.

Why the odd symbolic design?

I am curious what the aversion to adding new proper keywords to JS is. The new private properties proposal also uses #, rather than doing what most other languages on earth do: private!

Why not use two new keywords, sealed and frozen to indicate that objects should be sealed or frozen? The use of single-character symbols, especially when they have already been used for other things, just increases the confusion of the language.

Just off the cuff (I'm sure there are issues with this, but the general idea):

let as = sealed { b: sealed { ... }, c: frozen { ... }, d: { ... }, ... };
let af = frozen {b: frozen { ... }, c: sealed { ... }, d: { ... }, ... };

I know someone asked for a champion to move to stage 2...but my honest opinion here is, symbols is not the right approach here. We shouldn't be overloading # (now the private member modifier) or | (used for logical and bitwise operations) with yet another purpose, especially when you might be combining object creation with logical operations or assigning a private property to a newly created sealed/frozen object.

I'm just unsure why there seems to be such strong aversion to keywords in JS language proposals when a keyword would present a much more usable, understandable and clear language than symbols...especially overloaded symbols.

Alternative Syntax Ideas

I was just wondering if {{ name: 'value' }} might be a viable syntax here?

Even 3 brackets look fine - eg {{ sealed }}, {{{ frozen }}}?

Optional freezing for fields

Maybe it's worth to add optional freezing for part of object fields, making their descriptors non-writable and non-configurable? Syntax can be the same as Flow use for now:

const obj = {
  +freezedField: 'foo',
  normalField: 'bar'
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.