Closed zepumph closed 2 years ago
Tagging @jessegreenberg
I found that I had to make a couple of things public from private/protected in Voicing because of TS4094 (explained in https://github.com/Microsoft/TypeScript/issues/30355#issuecomment-473060190). I marked a couple of TODOs pointing to the change.
I tried to convert Voicing to typescript so that I could specify that it can only provide Node subtypes to be extended, and I ran into that same issue. Since there are protected/private members on Node, it seems like we are a bit stuck. I am pretty sure that I have to give up here. At least for now.
voicingTypescriptPatch.txt (first you need to rename Voicing.js -> Voicing.ts)
I also wanted to tag @samreid because I was talking to him on slack and he mentioned https://www.typescriptlang.org/docs/handbook/mixins.html#constrained-mixins. I believe that this is how we ran into the issue above where we can't have private members.
@jonathanolson is the expert on TypeScript mixins and the constraints around private/public members and may be able to recommend how to proceed here.
Also, I skimmed the patch and am wondering if it would work for this case to do class VoicingNode extends Node
instead?
I do not think so, the current structure is as such for 6+ mixins:
InteractiveHighlighting
Voicing
ReadingBlock
AccessibleValueHander
AccessibleSlider
AccessibleNumberSpinner
That said, it is a recent addition from AccessibleValueHandler to extend Voicing, and if we had to, we could likely move away from it (it's sorta up in the air anyways).
Many usages of Voicing and AccessibleValueHandler are added into the middle of hierarchies, and with typescript I wouldn't know how to say Voicing extends Node, but could somehow be tacked into a hierarch under Path/Rectangle etc.
To see usages of this, I searched for extends [^N](.*\n.*)*initializeVoicing
, and found that Voicing is mixed into Node, Rectangle, Path, Vbox, RichTextLeaf and many others in the future.
I reached out to @jonathanolson to see if we could move forward with this pattern.
In a conversation with @jonathanolson this afternoon, he pointed me towards the pattern used in Paintable, as well as the general issue https://github.com/phetsims/chipper/issues/1127.
So far so good. I'm going to proceed with some of the TODOs in the issue. (currently 21)
Alrighty. Three days later, I realized that everything was quite interconnected. It was hard to separate the new mixin pattern from the typescript conversion without putting a fair number of bandaids on things. Furthermore, since the mixin hierarchy is pretty much all together, I think it was best to do them all at one. I changed the name of this issue BTW.
A summary of the work completed:
X as unknown as Node
. This is probably the worst part about the pattern, but perhaps @jonathanolson and I can keep working on it (it's the best we can do right now)._
. There are also a fair number of these that would be really nice to pair with someone on so that I can feel a bit more confident about wonky changes.I'll check back in the morning to see what CT has to say on the matter.
And thank you @samreid and @jonathanolson for the help along the way.
I am not exited about the downstream effects of our mixin pattern at this time. Right now Every Slider is not known as a Node. I'm trying to figure out the best way forward. I tried something like this
const AccessibleSliderClass = AccessibleSlider( Node );
class Slider extends ( AccessibleSliderClass as typeof AccessibleSliderClass & Node ) {
But I got this error on the typecast: "TS2510: Base constructors must all have the same return type."
I wonder if this pattern could help us https://github.com/microsoft/TypeScript/issues/40110
Right now Every Slider is not known as a Node.
That's confusing, the mix-in pattern presumably should handle that correctly. Can we collaborate again?
Yes please. I definitely think there will be a workable solution, I just don't have it quite yet.
It also think it is quite likely that it will be complicated enough that when you have a Typescript mixin used by a javascript common code used by a Typescript sim class, the Typescript->Javascript->Typescript will lose the typing specificity, and so to get a working solution, We may need to convert Slider.js and a couple others.
@jonathanolson showed me a couple of strategies about how to handle mixins
We need the same constructor signature everywhere, which means ...args[]:any
, and then plucking out our parameters.
Above is a bandaid. Here are the next steps from another meeting with @jonathanolson. This will provide typesafety, and the flexibility for these mixins to mix into any arbitrary Node class, even when they have extra parameters and or options that must be used in their constructor:
Lots of progress above. There are still a lot of TODOs in the code, but I think most things are working at this time. The next highest priority is trying to fix regressions that have occurred from having NumberSpinner, NumberPicker, and Slider having options passed to super instead of mutate. For example positioning is broken, see the workaround here:
I'll keep cracking at things.
To discuss:
Constructor<>
?I am about to meet with @jonathanolson to solidify a path forward on some of these harder ones.
Need to mutate transform options after super, in its own mutate call, how to split things out?
Polymorphic constructors and supporting these mixins. Is some sort of assertion possible?
Convention for protected members/methods?
@protected
jsdoc, that will be good enoughBest central place for Utility Typescript Types to live, for example Constructor<>?
Constructor.ts
file in phet-core. Doesn't tandem always need to be the last mutatorKey? @samreid do you remember this at all?
@samreid nevermind, we figured it out.
Here is the pattern for mutator keys in mixins:
VoicingClass.prototype._mutatorKeys = VOICING_OPTION_KEYS.concat( VoicingClass.prototype._mutatorKeys );
assert && assert( VoicingClass.prototype._mutatorKeys.length === _.uniq( VoicingClass.prototype._mutatorKeys ).length, 'duplicate mutator keys in Voicing' );
No need to touch Paintable usages or Image. They are working fine.
Above, all option keys should now be respected, and transform is no longer buggy. I could remove the workaround in HeaterCoolerFront.
Thanks tremendously for all the help that @jonathanolson has given over the past couple of days. Things are in a much more stable place now. All bugs that I know of have been taken care of (save https://github.com/phetsims/molarity/issues/242). Now the rest of the work is just cleanup, mostly about documenting private/protected members of the mixins.
@jonathanolson, I think that you should be the one that takes a look before removing the blocking label.
This would block the RC of Molecule Shapes, which I think is right on the horizon.
I created https://github.com/phetsims/scenery/issues/1348 and now there are only 18 TODOs for this issue. @jonathanolson please glance at them as part of the review to see if any feel blocking to you.
Above is a bug fix reported in https://github.com/phetsims/gravity-force-lab-basics/issues/319 https://github.com/phetsims/ratio-and-proportion/issues/426 https://github.com/phetsims/molarity/issues/242
Because I wasn't passing the optionized options through in AccessibleNumberSpinner or AccessibleSlider. Basically all start/endDrag listeners weren't firing. Fixed above!
My recommendation about how to review:
Mixin(
In TwoSceneSelectionNode, there is:
super();
// Separate call to support mutating InteractiveHighlight options
this.mutate( options );
Is that... needed anymore? My understanding is that it's not.
InteractiveHighlightInteractionNode in BalloonNode also has that.
Lots of things are mixing into the same base type... memoizing sounds helpful there (but would need to figure out how we should memoize with the 2 parameters). Otherwise we're losing the in-memory benefits.
Perhaps we can programmatically determine the number of supertype arguments and autodetect the optionsArgPosition?
The mixed superclass is separated into a separate once-used variable. Is that on purpose?
@zepumph questions listed above, have a few inline REVEW comments, and overall the changes in the above commits (mostly already discussed). Can you review the changes above and see if those look good?
The structure of everything looks great and as anticipated. I'll be looking into a few ways we could improve the typescript bits, since we don't have much type protection or conciseness in a few cases.
It looks like https://github.com/phetsims/friction/commit/efcc0c2ea90f2edbb7a94c41ec764e725870d2c5 caused a visual regression. It moved super( options )
to super()
with a mutate( options )
at the end, changing behavior. We should scan other commits to see if this pattern happened in any sim repo. @zepumph thoughts on whether this was done often?
Why did https://github.com/phetsims/inverse-square-law-common/commit/7dce161a32ac74e2fc943608c09db0db6a8d6ab1 split ISLCForceArrowNode's super( options )
into a super and mutate next to each other?
https://github.com/phetsims/scenery-phet/commit/d347e7d4b256e98e31f80de106389c89e71d833b and FaucetNode / NumberPicker / SpectrumSlider should be checked
SpectrumSlider has an earlier options (in the super) instead of a mutate at the end, positioning code based on it could be off.
FaucetNode also has earlier options that should be mutated near the end. NumberPicker looks like it has the bounds-related options put at the end and is likely good.
https://github.com/phetsims/sun/commit/1ad9e76414658b3797009662e651d3d41b48d34a NumberSpinner also needs this.
To be on the safe side, I’m going to consider this blocking for https://github.com/phetsims/function-builder/issues/139, and not proceed with publishing Function Builder for dev testing. Function Builder uses NumberPicker.
The broken common code files are NumberSpinner, FaucetNode, and SpectrumSlider. NumberPicker and Slider are working correctly. @pixelzoom please adjust your blocking status accordingly. Commits coming soon for the broken classes listed above, as well as a fair number of sim-specific usages of AccessibleSlider etc.
I will fix this by applying the same strategy @jonathanolson and I discussed and implemented for Slider, like https://github.com/phetsims/sun/blob/35aee14690a552602c5abfd49d8166838dd3d1b8/js/Slider.js#L140-L142.
I also found a few places where we weren't providing the index of where to find the options arg, this was helpful in finding those cases: (ReadingBlock|InteractiveHighlighting|Accessible[A-Z]\w+)\( \w+ \)
Commits for that, plus an assertion to make sure we don't do so in the future, and the mutate/options fix discussed above coming soon! I'm just testing thoroughly now.
Lots of things are mixing into the same base type... memoizing sounds helpful there (but would need to figure out how we should memoize with the 2 parameters). Otherwise we're losing the in-memory benefits.
Is this as simple as adding memoize around InteractiveHighlighting?
Yes hopefully, since it will ignore the params argument that shouldn't change. It may need some changes to type correctly.
Perhaps collaborating (at a lower priority) would be good for this?
All comments after https://github.com/phetsims/scenery/issues/1340#issuecomment-1032862162 have had a fix applied above. @jonathanolson, can you spot check the above as a micro commit to ensure we are on the same page. The other parts of the review seem lower stakes.
I tested with fuzzing, and with the following:
I also added an assertion for passing in a number for optionsArgPosition
, and found a couple places I was missing it.
Perhaps we can programmatically determine the number of supertype arguments and autodetect the optionsArgPosition?
The mixed superclass is separated into a separate once-used variable. Is that on purpose?
I refactored them to be inline, at one point it was needed, but I don't think I ever even committed that version.
Lots of TODOs were removed in https://github.com/phetsims/scenery/issues/1351.
There are only two more open questions for me in this issue. 4 TODOs (2 topics repeated in two spots each):
can you figure out why this is causing a lint error?
Not immediately sure. I don't think that eslint checks the phet-types file, presumably we'd want that type somewhere it's imported.
This is for typescript in general in scenery. I'm wondering how scenery listeners can support more specific subtypes of the window.Event interface. For example, I need access to items that only some events have, like KeyboardEvent. Can you recommend how to proceed here?
Presumably SceneryEvent would support a type parameter, and we could have subtypes so that it's more type-safe.
I liked typecasting as a KeyboardEvent much more, enough to remove the TODO. Even if this sticks around for a while, it feels pretty low stakes, and safe. I also converted AlertableDef to typescript, just to add the type (TAlertableDef), which is fine for now. We can't rename it until all usages of AlertableDef.isAlertableDef are in typescript files (unless we delete them). I'm ready to close this issue. Anything else here @jonathanolson?
This will be helpful for https://github.com/phetsims/ratio-and-proportion/issues/404, and is aligned with quarterly goals.
Note: https://github.com/phetsims/tasks/issues/1096