Skip to content

Syntax for conditionally setting object properties #45606

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
5 tasks done
jcomputer opened this issue Aug 27, 2021 · 8 comments
Closed
5 tasks done

Syntax for conditionally setting object properties #45606

jcomputer opened this issue Aug 27, 2021 · 8 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@jcomputer
Copy link

jcomputer commented Aug 27, 2021

Suggestion

πŸ” Search Terms

conditionally assigned properties conditional properties exactOptionalPropertyTypes

βœ… Viability 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, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Create a syntax for conditionally assigning a property in an object at creation time in-line. This is especially relevant now that exactOptionalPropertyTypes is launched. See below for a proposal of how to do this.

πŸ“ƒ Motivating Example

Imagine this piece of JavaScript code:

function myFunction(val) {
  const myObject = {
    name: 'foo',
  };
  if (val !== undefined) {
    myObject.value = val;
  }
  return myObject;
}

This is valid JS code, and a somewhat common pattern for setting an optional property on an object.

But how would we convert this into strict TypeScript code, particularly with immutable types? Perhaps something like:

interface MyType {
  readonly name: string;
  readonly value?: string;
}

function myFunction(val?: string) {
  const myObject: MyType = {
    name: 'foo',
  };
  if (val !== undefined) {
    myObject.value = val;   // Error: cannot assign to `value` because it is a readonly property.
  }
  return myObject;
}

Well that doesn't work. :-(

Another option before exactOptionalPropertyTypes was:

function myFunction(val?: string) {
  const myObject: MyType = {
    name: 'foo',
    value: val,  // Not great, setting an optional property to `undefined` instead of leaving it unset. And fails with exactOptionalPropertyTypes
  };
  return myObject;
}

But this is bad practice for the reasons outlined in exactOptionalPropertyTypes, and is an error if you use that flag now.

Using good practices combined with strict TypeScript settings is preventing us from accomplishing this somewhat common pattern for constructing immutable objects with some optional properties. One workaround is to use something like type Mutable<T> = {-readonly [key in keyof T]: T[key]} inside this function, but this is worsening type scritness by circumventing the original type's immutability. Or you could tell the compiler to assume the value is non-null (!), then conditionally delete that property after, but that's error-prone and is circumventing TypeScript's type checking. I'm not aware of any safer way to do this without fundamentally modifying the code (eg. creating a builder pattern).

Proposal

What if we had something like this:

interface MyType {
  readonly name: string;
  readonly value?: string;
}

function myFunction(val?: string) {
  const myObject: MyType = {
    name: 'foo';
    value?: val;  // If `val` is `undefined`, does not set this key. Otherwise, sets this key to the the value in `val`.
  };
  return myObject;
}

This code would compile to the JavaScript code written at the top of this FR. But importantly, unlike my other examples, this code will build successfully because TypeScript understands the optional (but not undefined) key will not be set on this object unless its value is defined. It is type-safe.

How isn't this just a new syntactic sugar for JavaScript?

This problematic code is unique to TypeScript. JavaScript has simple solutions that build and run just fine (see first code example), but TypeScript does not (without sacrificing strictness). As such, this problem falls in the realm of TypeScript to solve, as JavaScript has far less incentive to implement what for them would only be a very minor syntactic sugar rather than a fix for unsupported use-cases.

Additionally, this is not writing any code that a human wouldn't write themselves if asked to solve this problem in JavaScript. This would just be TypeScript equivalent syntax that transpiles to that form, in the same way TypeScript class methods transpile to MyClass.prototype.foo = function(... syntax in JavaScript despite no mention of "prototype" in the TypeScript code.

πŸ’» Use Cases

The primary use-case is as described above: immutable types being able to correctly set optional properties optionally without compromising strictness. In addition, this would also be a useful syntactic sugar for mutable use-cases, particularly when exactOptionalPropertyTypes is set, avoiding the verbosity of a bunch of if statements.

Alternative proposal

Here's another possible syntax that could solve the same problem, but with a bit more expressive power:

function myFunction(val?: string) {
  const myObject: MyType = {
    name: 'foo';
    // If the value of a key is `delete`, does not set that key. This would only be a TypeScript syntax,
    // it would compile either to the JS at the top of this FR or else to a similar solution bu using the
    // `delete` keyword on this key within the opposite if statement instead.
    value: val ?? delete;
  };
  return myObject;
}

This alternative syntax is a bit quirkier, but it has the advantage of not treating undefined as a special value. undefined seems to be the primary use-case here, especially after exactOptionalPropertyTypes, but some users may want to have more flexibility to customize this behavior (eg. for the corner case that a property is defined as foo?: string|undefined).

@jcomputer
Copy link
Author

After writing this, I found #39376. It predates exactOptionalPropertyTypes and also doesn't call out the uniqueness of this problem to TypeScript and not JavaScript/ES, so I think this request is unique. But I'm tagging is for the sake of transparency.

@bmeck
Copy link

bmeck commented Aug 27, 2021

This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)

This request seems to require runtime stuff. Likely the better place for this feature of only setting the key would be the JS specification itself ( https://tc39.es/ecma262/ ).

@jcomputer
Copy link
Author

jcomputer commented Aug 27, 2021

@bmeck As I mentioned in the proposal, this doesn't require any more runtime behavior than creating a class method or using the elvis operator.

The reason I don't think it belongs in the JS spec, as I outlined, is this isn't a problem for JS, it's a unique problem to TS. Why would the JS spec want to solve a TS problem?

@andrewbranch
Copy link
Member

You’re right, I doubt TC39 would want this. But we can’t take it either, because it is a runtime feature.

@andrewbranch andrewbranch added Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript labels Aug 27, 2021
@MartinJohns
Copy link
Contributor

it's a unique problem to TS. Why would the JS spec want to solve a TS problem?

But it's not. You have the same issue in JS: You optionally want to set a property. You don't want a property with an undefined value, you either want a proper value or the property not exist in the first place. This is what exactOptionalPropertyTypes means.

@bodograumann
Copy link

It is possible to write this inside the object literal with:

{ ...(value === undefined ? {} : { key: value }) }

So I am wondering whether it is possible to introduce a different syntax for that, like e.g.

{ key?: value }

and transform it into the above.
My first thought was a babel-plugin, but I guess the transformation would have to run before the code reaches typescript.
Any ideas?

@IanIsFluent
Copy link

It is possible to write this inside the object literal with:

{ ...(value === undefined ? {} : { key: value }) }

So I am wondering whether it is possible to introduce a different syntax for that, like e.g.

{ key?: value }

This is what I'd like to be able to write! Would it be a Javascript feature, then?

@bodograumann
Copy link

That is the problem that I was asking about. If it is was implemented as a custom feature in javascript, which is transformed by a babel-plugin for now, typescript would still have to understand it.
Typechecking would have to work for it and the typescript output would need to keep it as-is.
So this doesn't look like a viable approach.

Naively we could also try to build a preprocessor which converts { key?: value } to { ...includeIfSet(key, value) } with a helper function

function includeIfSet<K extends string, V>(key: K, value?: V) {
  if (value === undefined) {
    return {};
  }
  return { [key]: value };
}

Having a solution directly in typescript would be much cleaner though.
Also, I am not aware of any plugin system for typescript that could be used to β€œbring your own syntax”.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants