-
Notifications
You must be signed in to change notification settings - Fork 7
Description
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>