keithamus / proposal-object-freeze-seal-syntax

A JavaScript TC39 proposal for Object.freeze & Object.seal syntax
https://www.keithcirkel.co.uk/object-freeze-seal-syntax
59 stars 8 forks source link

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 ```js 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 ```js 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 ```js 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 ```js 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 ```js 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 ```js 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 ```js 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' ```