Optional keys in objects

I have typical problem with javascript. Say, I have an input object, which, in some cases, could be enrich with additional keys. I have two options how to do that. First:

function enrich(input, optionalData) {
  return {
    ...input,
    additionalKey: optionalData
  }
}

Now I could have an object with { additionalKey: undefined }, which could lead to bugs. For example, additionalKey will be lost after JSON.stringify/parse. Another bug: check key with operator in ('additionalKey' in obj), which gives true, but the value is actually undefined → we have to write additional code which checks the value also.

Another option here is

function enrich(input, optionalData) {
  const output = { ...input }

  if (typeof optionalData !== 'undefined') {
    output.additionalKey = optionalData
  }

  return output
}

which gives us 3 more lines of code per key, and also cannot be properly typed with typescript.

What I propose:

function enrich(input, optionalData) {
  return {
    ...input,
    additionalKey?: optionalData
  }
}

if optionalData is undefined, then additionalKey will not be added to the output object.

8 Likes

I usually code like this:

const enrich = (input, optionalData) => ({...input, ...options && {options}})

In general case, it would be

const enrich = (input, optionalData) => ({...input, ...typeof additionalKey !== 'undefined' && {additionalKey: optionalData}})

:(

const enrich = (input, optionalData) => ({
    ...input,
    ...options != null && {options},
})

This is good for a small set of keys, but when there are several of them and additional values are calculated, a special syntax is much more convenient.

Compare

function enrich(target) {
  // we don't want to call calcDerived twice on same value
  const derivedOnTarget = calcDerived(target)
  const derivedOnSome = target.some ? calcDerived(target.some) : void 0
  return {
    length: Object.keys(target).length,
    ...derivedOnTarget !== undefined && { derived: derivedOnTarget},
    ...derivedOnSome !== undefined && { prop: derivedOnSome},
  }
}

vs

function enrich(target) {
  return {
    length: Object.keys(target).length,
    derived?: calcDerived(target),
    prop?: target.some ? calcDerived(target.some) : void 0,
  }
}
2 Likes

Love the idea, don't like the syntax as it's too rigid and not a fan of making it implicitly not add on undefined as it feels too magical.

What about this?

function enrich(target) {
  // we don't want to call calcDerived twice on same value
  const derivedOnTarget = calcDerived(target)
  const derivedOnSome = target.some ? calcDerived(target.some) : void 0
  return {
    length: Object.keys(target).length,
    if (derivedOnTarget !== undefined) {
      derived: derivedOnTarget,
    }
    if (derivedOnSome !== undefined) {
      prop: derivedOnSome,
    }
  }
}

Would also be a fan of allowing for and while loops as well. Note that the bodies here are either key: value or object literals to further mix in (with duplicate keys still syntactically checked).

1 Like

Dart has a similar idea to @claudiameadows proposal, called "collection if" and "collection for", but it only works on array.

Example from Dart's website:

var nav = [
  'Home',
  'Furniture',
  'Plants',
  if (promoActive) 'Outlet'
];

Such syntax would be nice in javascript too.

+1 for this topic. Frequently I have to write cumbersome code like this:

{ ...input, ...additionalKey !== undefined && { [additionalKey]: optionalData } }

This seems too long compared to what it does. The motivation here is the shortness of the code. @claudiameadows idea sounds reasonable but doesn't do much for this.

There're two concerns: emptiness of "key", and of "value". We need separate syntax for each of them. As ECMAScript already introduced "nullish" semantics to its syntax (e.g. optional chaining), we should use that here too, for example:

{ [key]?: value }

means

{ ...(key !== undefined && key !== null ? { [key]: value } : {}) }

and

{ [key]:? value }

means

{ ...(value !== undefined && value !== null ? { [key]: value } : {}) }
2 Likes

Can't say I'm fond of the implicitness of the syntax. It'd be easy to overlook a simple ? before : when studying the structure of a created object and then subsequently wonder how that property doesn't still exist, and I've already run into similar situations in the past from misreading typos with object keys.

1 Like

Nit: my proposal syntax is not really implicit, because there's explicit ?. From the syntactic point of view, this is a trade-off between succinctness vs verboseness, not implicitness vs explicitness.

Generally speaking, properties keyed by "undefined" or "null" shouldn't exist. Some people mistakenly create such objects, and a few people do that intentionally, but I definitely believe they (and we) should avoid such code. If really needed, explicitly converting it to string beforehand is the right way to go. I'd rather prefer the "implicitness" of omitting such properties, over the genuine implicitness of coercing nullish values into meaningless string keys like "undefined" and "null".

Of course they should exist - when the authors intend them to. null and undefined are far more explicit than always coercing to one type, like a string.

Of course they should exist - when the authors intend them to.

Yeah, if they really want. I don't say that the default semantics of the computed key syntax should be changed, that is nealy impossible as it introduces a breaking change to browsers.

null and undefined are far more explicit than always coercing to one type, like a string.

I mean just implicitly coerced computed keys like "undefined" and "null" are not intended in almost cases. undefined and null as values are still valid, as you said.

Hi! I was just linked to this thread from another where we're discussing some solutions to the same problem for both arrays and objects using modified ternary and null coalescence syntax. Thoughts on that approach (for objects)?

1 Like

I think your thread is for a bit complex topic than this thread. Your thread is about conditionally and optionally set a property/element/argument (correct me if I'm wrong), while this thread is (originally) just about optionally set a property in an object.

"conditional", is a big thing, as it needs a separate syntactic clause for the condition, and that makes me think it is already well-syntacticized enough by ternary expressions + spreading like ...hasCats ? ["Cats"] : []. It's not much different than writing hasCats ? "Cats" : .... Only a few characters longer.

As I understand it, spreading is not the topic of this thread, and not even a right concept to solve the verbosity of handling optional properties. Maybe that's why we use speading to illustrate the problem here. I want to remove ... from my optional-property-handling code.

But yes, if you think about "optional", we can handle objects and arrays (and function arguments?) in the same way. Personally I prefer the following syntax, single question mark looks readable enough to me:

const ghost = undefined
const array = ["foo", ghost?]
const object = { foo: "bar", baz: ghost? }
func("foo", ghost?)

(Of course, your proposal syntax like ["foo", ghost??] would also be a possible solution. Though I don't know how we can reach a consensus about such a stylistic choice, as I'm new to TC39 discussions :joy:. There might be concerns about parser complexity and the like.)

And my proposal syntax might be updated then:

- { [key]?: value }
+ { [key?]: value }

- { [key]:? value }
+ { [key]: value? }

Furthermore, we can also support the shorthand syntax:

{ value? }

meaning

{ ...(value !== undefined && value !== null ? { "value": value } : {}) }

I like the proposal, but I think it might be solved with Object's utility method first, like Object.assign. As a function it could be even more useful.

You can already do {...(cond ? {foo: "bar"} : {})}. This is just wanting to offer a syntax that makes that considerably less arcane.

Anectdotally, I've already heard plenty of complaints about the readability of that pattern at work. Part of my suggestion of allowing inner ifs grew out of experience with that.

Yep, but there is a corner stones here. Some might want to remove undefined and null, other NaN or Infinity. Method to delete properties could solve all of this.

Object.clean({a: undefined, b: null}) // => {}
Object.clean({a: false, b: null}, (v) => v !== false) // => {b: null}

// Method modifies target object
let value = {a: undefined}
'a' in value // => true
Object.clean(value)
'a' in value // => false

Usage

function enrich(input, options) {
  return Object.clean({
     ...input, additionalKey: options,
  })
}

And syntax for this should be part of object restructuring syntax proposal, which should be more powerful and cover more object manipulation cases.

This proposal isn't about adding a builtin object for an object value-oriented equivalent of Lodash's _.compact for arrays, but about conditionally adding keys to objects in object literals.

I've provided an alternative, which in my opinion is important in the context of thee topic. And provided a link to a syntax proposal which is more relevant and where this proposal suits better. So IMO if it's about functionality it could be solved with a function, because syntax changes are costly. If it's about syntax, then it should be relocated to preserve focus and create complete proposal instead of bunch of tiny little ones.

It's always been about syntax since the start, and syntax in general will always be under flux at least starting out. That's the nature of these discussions.