Skip to content

Pattern for nestedOptions in optionize #127

@pixelzoom

Description

@pixelzoom

In phetsims/scenery-phet#737 (comment), @samreid asked about this pattern of using optionize where you don't want to require a default value for one or more nested options:

    const options = optionize<TimeControlNodeOptions,
      Omit<SelfOptions, 'playPauseStepButtonOptions' | 'speedRadioButtonGroupOptions'>, NodeOptions>()( {

We discussed this in Slack and decided that Omit<SelfOptions, 'someNestedOptions'> was a good pattern. See discussion below. It sounds like this pattern needs to be communicated to the PhET dev team.

Slack discussion 3/29/2022

Chris Malley 9:22 AM
I’m wondering if providedOptions should generally be providedOptions?: MyClassOptions | null in order to support composition and nested options. I’ve been running into this quite a bit while converting sun/scenery-phet, where it’s impossible to provide a default like (for example) numberDisplayOptions: null , then propagate to new NumberDisplay( .., options.numberDisplayOptions ). (edited)

Michael Kauzmann 9:35 AM
Totally fine with me, but shouldn't that null be internal to the type defining the nested options?

type NumberControlOptions = {
  numberDisplayOptions: NumberDisplayOptions|null
. . .
} (edited) 

[Chris Malley](https://app.slack.com/team/U6EMFARV2)  [9:36 AM](https://phetsims.slack.com/archives/C029UG5BVU3/p1648582579298039)
Yes, but you then can’t pass null to new NumberDisplay.

[Michael Kauzmann](https://app.slack.com/team/U6DFWDG1Z)  [9:37 AM](https://phetsims.slack.com/archives/C029UG5BVU3/p1648582665828029)
Right, and you want to?

[Chris Malley](https://app.slack.com/team/U6EMFARV2)  [9:43 AM](https://phetsims.slack.com/archives/C029UG5BVU3/p1648583022404239)
Here’s the situation:
type SelfOptions = {
  numberDisplayOptions: NumberDisplayOptions|null;
  ...
}

type NumberControlOptions = SelfOptions | NodeOptions;

constructor( ...., providedOptions?: NumberControlOptions ) {

  const options = optionize<NumberControlOptions, SelfOptions, NodeOptions>( {
    numberDisplayOptions: null,
    ...
  }, providedOptions );
  ...

  const numberDisplay = new NumberDisplay( ..., options.numberDisplayOptions );
}
The call to new NumberDisplay is a TS error, because NumberDisplay does not support null for its providedOptions.
The alternative currently is to provide {} as the default, which is wasteful:
type SelfOptions = {
  numberDisplayOptions: NumberDisplayOptions;
  ...
}

const options = optionize<NumberControlOptions, SelfOptions, NodeOptions>( {
    numberDisplayOptions: {},
    ...
  }, providedOptions );
[9:44](https://phetsims.slack.com/archives/C029UG5BVU3/p1648583082918839)
Because numberDisplayOptions is optional, it must have a default value specified in the call to optionize. (edited) 

[Michael Kauzmann](https://app.slack.com/team/U6DFWDG1Z)  [9:45 AM](https://phetsims.slack.com/archives/C029UG5BVU3/p1648583118077479)
Makes sense to me! I thought that we only had cases where that common code that uses nested options would also provide some defaults, meaning there would be an option. I'm totally fine with adding |null to providedOptions, personally. (edited) 

Chris Malley  9:45 AM
null is what we’ve used in the past as that default.
9:47
And add | null as we need them, or add them as a general pattern?  I’m not wild about having to change an API every time I encounter this.  I’d probably change the default to {} and waste an object.

Michael Kauzmann  9:48 AM
I just feel like it isn't that pervasive to warrant it as a general pattern.

Chris Malley  9:49 AM
Btw… If you happen to look at NumberControl.ts, it’s not a good example, because it’s still using merge , was not fully converted to TS.

Michael Kauzmann  9:49 AM
Also, the main reason that I would use the null as a default was because I was about to have a second merge/optionize call below that filled in that null to be an option, but needs other options to do so.
9:50
poking around usages of Options: null, right now to understand better
9:50
My thoughts on what the "classic" example is for why we use null is like so:
https://github.com/phetsims/sun/blob/e9d07497cebc286170b20dfa8c239ad8dc0015c2/js/AccordionBox.ts#L209-L213

Jonathan Olson  9:50 AM
Why would we need nulls in general there, just don't put an option? Is it just to placify optionize?

Chris Malley  9:50 AM
It’s often the case that the class does not fill in the nested options. The nested options are provided so that the client can customize.  For example OopsDialog.js:

      // nested options
      richTextOptions: null,

Michael Kauzmann  9:51 AM
So if there is a case where you don't go back and fill those in, can we just add the |null in the cases we need.
9:51
You are also in the thick of typescript conversion, so I defer to you opinion here. I guess I would most prefer to only add |null when needed.

[Chris Malley](https://app.slack.com/team/U6EMFARV2)  9:52 AM
To answer JO…. Yes, a default value is required by optionize.  If a field is optional, a default value must be specified.

Jonathan Olson  9:52 AM
Then remove it from the things that need defaults from optionize, since it's presumably permitted to be undefined?

Chris Malley  9:53 AM
optionize also will not let you provide a default value for a required field.

Michael Kauzmann  9:53 AM
@samreid just mentioned to me a solution that I prefer best! In Typescript the null default is actually superfluous, because it is redundant to how we declare things in the Options type.
9:53
So can we just delete that spot.

Chris Malley  9:53 AM
Can you clarify? What does “just delete that spot” mean?
9:54
I like JO’s idea of Omit.
9:55
But it sure makes the optionize call look complicated.  Looks like Omit is how JO avoided specifying a default for nested options in NumberControl:
optionize<NumberControlOptions, Omit<SelfOptions, 'numberDisplayOptions' | 'sliderOptions' | 'arrowButtonOptions' | 'titleNodeOptions'>, NodeOptions, 'tandem' >( ... )
(edited)

[Michael Kauzmann](https://app.slack.com/team/U6DFWDG1Z)  9:57 AM
Can we do that for now? Then SR and I could potentially add that to the internals of optionize, where anything that ends in Options does not need to be in the defaults.

[Chris Malley](https://app.slack.com/team/U6EMFARV2)  9:58 AM
I see. That’s what you meant by “just delete that spot”?

[Michael Kauzmann](https://app.slack.com/team/U6DFWDG1Z)  9:59 AM
It is not! But I like JO's solution better.
9:59
Well, I guess it kinda is
9:59
We are really all talking about the same thing in different ways.

Chris Malley  9:59 AM
:+1::skin-tone-2: Thanks for discussing. I’ll try the Omit pattern.

Sam Reid  9:59 AM
To me, the Omit seems preferable to supporting |null everywhere.
:+1::skin-tone-3::+1::skin-tone-2:
2

10:00
One day we may find a way to auto-Omit in optionize based on “*Options” suffix?

[Michael Kauzmann](https://app.slack.com/team/U6DFWDG1Z)  10:00 AM
Especially if it is a convention to factor that type out into a type like
type SelfOptions = . . . .
type SelfOptionsNoNested = . . . . 
:+1::skin-tone-2:
1

</details>

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions