-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
SelectControl: Infer value
type from options
#64069
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes we have to make in this file are what make me a bit hesitant about adding this value
type inference. The stricter types do not break runtime code obviously, but we will be forcing some consumers to deal with this new strictness, which may seem like overkill depending on who you ask.
I think we can get away with it, because the strictness will only kick in if the options
array is defined as immutable (either a literal array in the component call, or with an as const
). For example, there is a potential bug in CustomGradientPicker
but it isn't caught with the new strictness because GRADIENT_OPTIONS
is mutable.
We don't have a ton of data points in the type checked realm of this repo, but it seems like TimePicker
is the only "false positive" in the sense that it didn't surface a potential bug. (I just want to be sure that this type tightening is a net positive and not a nuisance 😅)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with what you're saying. It would likely be a good thing to document how to specify a loose type (by using SelectControl<string>()
) so that users who don't want to tighten their other code (in this case, the format
function) can still have the same level of leniency as they previously had, and then this change empowers users to use tighter types if they like.
Though, saying that, I'm not sure where this would be documented. In the SelectControl
docs as a note perhaps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just want to be sure that this type tightening is a net positive and not a nuisance 😅
This is definitely a big aspect to keep in mind. Overall, I think this change is a net positive — I hope that in the future we'll have a way to always apply the strictness, and not only when the options
array is immutable.
We should probably write a dev note for this change, to help consumers of the package deal with this change correctly (especially the ones who need to support multiple versions of Wordpress / Gutenberg).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a Dev Note so we at least have something to link to if anybody asks. If we do get some questions, we might need to document it somewhere more permanent. But in general I don't think we should have "TypeScript documentation" aside from our actual type files.
value={ | ||
orderBy === undefined || order === undefined | ||
? undefined | ||
: `${ orderBy }/${ order }` | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was an actual bug.
const newValues = selectedOptions.map( | ||
( { value } ) => value as T | ||
); | ||
props.onChange?.( newValues, { event } ); | ||
return; | ||
} | ||
|
||
props.onChange?.( event.target.value, { event } ); | ||
props.onChange?.( event.target.value as T, { event } ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These type assertions should be safe, as they will always match the options
types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, this isn't quite true because the select could be edited by someone using dev tools, however, I think this is fine because
- If you edit a website with dev tools, it's your fault you broke the code, not the code maintainer
- The likelihood of this happening is next to none
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you be willing to use OptionType
instead of T
here? T
doesn't really mean anything, and while it's a common practice in TypeScript, I see it as equivilent to naming a variable like const a = 'something'
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed them to V
(for value
— it's not actually the option type), does that read a bit better? We don't have official naming conventions for this project yet, so eventually we may decide on a prefixed convention like TValue
. But to write out a full name like Value
, I think it's harder to signal that it's a generic. And ValueType
is redundant because everything is a type 😅 We can revisit, but I hope V
is a good compromise for now.
@@ -24,7 +24,7 @@ type SelectControlBaseProps = Pick< | |||
Pick< BaseControlProps, 'help' | '__nextHasNoMarginBottom' > & { | |||
onBlur?: ( event: FocusEvent< HTMLSelectElement > ) => void; | |||
onFocus?: ( event: FocusEvent< HTMLSelectElement > ) => void; | |||
options?: { | |||
options?: readonly { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding a readonly
here so it allows immutable arrays. (Otherwise it would be a type error when passing an immutable array.)
* @default false | ||
*/ | ||
multiple?: false; | ||
value?: NoInfer< T >; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NoInfer<>
is a new utility type added in TS 5.4, which tells TS not to use this field to infer the generic type. We need this because we want it to infer from the values in the options
array, not the value
prop.
This kind of "don't infer" signaling used to be done with T & string
, but does not work anymore in TS 5.4+.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can it be demonstrated when the NoInfer
can be useful? A new test case that fails without it? I don't understand very well what it does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, Without NoInfer
Typescript will infer the generic type from the value prop as well as the options prop.
What this means is it won't detect that your value is not one of the provided options, and therefore won't provide an error for it.
I've created a simple TS Playground example to illustrate the difference for you. Hope that helps 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this is nice 🙂 I didn't realize that passing something as value
can expand the T
type to also include the type of value
. But we want the type to be checked against options
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice to see a clever usage of NoInfer
👍 I had read about it, but struggled to think of a good use case for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also found a good article about NoInfer: https://www.totaltypescript.com/noinfer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have some personal nit-picks, but ultimately approve it because I'd be fine if those were ignored :)
const newValues = selectedOptions.map( | ||
( { value } ) => value as T | ||
); | ||
props.onChange?.( newValues, { event } ); | ||
return; | ||
} | ||
|
||
props.onChange?.( event.target.value, { event } ); | ||
props.onChange?.( event.target.value as T, { event } ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, this isn't quite true because the select could be edited by someone using dev tools, however, I think this is fine because
- If you edit a website with dev tools, it's your fault you broke the code, not the code maintainer
- The likelihood of this happening is next to none
const newValues = selectedOptions.map( | ||
( { value } ) => value as T | ||
); | ||
props.onChange?.( newValues, { event } ); | ||
return; | ||
} | ||
|
||
props.onChange?.( event.target.value, { event } ); | ||
props.onChange?.( event.target.value as T, { event } ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you be willing to use OptionType
instead of T
here? T
doesn't really mean anything, and while it's a common practice in TypeScript, I see it as equivilent to naming a variable like const a = 'something'
.
One thing that's missing from my PR is the automated tests, perhaps it's worth copying those over? |
* Otherwise, the value received is a single value with the new selected value. | ||
*/ | ||
onChange?: ( | ||
value: T, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also use the NoInfer
utility for the onChange
's arguments, in case a consumer of the component explicitly types onChange
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good one, I added the constraints (and type tests) for those too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just want to be sure that this type tightening is a net positive and not a nuisance 😅
This is definitely a big aspect to keep in mind. Overall, I think this change is a net positive — I hope that in the future we'll have a way to always apply the strictness, and not only when the options
array is immutable.
We should probably write a dev note for this change, to help consumers of the package deal with this change correctly (especially the ones who need to support multiple versions of Wordpress / Gutenberg).
Co-authored-by: Mikey Binns <hello@mikeybinns.com>
You're right, I brought over your tests and reformatted them a bit to mesh with our existing type tests (ad20004). I tried to pare it down so we have a good cost/benefit balance between safety and maintenance burden — hope that's reasonable. |
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.
To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
I discussed this with a few other folks and nobody is outright opposed, so I think we can move forward to an official review so this can be merged 🙏 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good stuff! Straightforward implementation. Nice touch with the type tests, though it makes me wonder if we should add standards for type testing and/or support for tooling like https://github.com/MichiganTypeScript/type-testing - the eslint ignore comments for jest feel a bit like a hack.
As a reference, here's an example from a personal project where I do extensive type tests: https://github.com/DaniGuardiola/rpc-anywhere/blob/main/src/tests/types-test.ts
Other than that, I have no notes (other than a minor nit)!
'select', | ||
false | ||
> & { ref?: React.Ref< HTMLSelectElement > } | ||
) => ReturnType< typeof UnforwardedSelectControl >; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you were going for here, but I think I'd simplify the return type to just React.JSX.Element | null
. That's the only thing that a component can ever return anyway, and it looks clearer to me this way. I don't really mind though, this is a nit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense, thanks 🙏 2f195c2
# Conflicts: # packages/components/CHANGELOG.md # packages/components/src/select-control/index.tsx # packages/components/src/select-control/test/select-control.tsx # packages/components/src/select-control/types.ts
@DaniGuardiola It has come up, most recently in #62400 (comment). I don't think we're quite at the point where we need dedicated type test tooling, as the downsides are still outweighing the upsides in my opinion (e.g. having to manage the tooling package updates, forcing contributors to learn yet another testing library, etc). But I do think we should start to document some standards/conventions like you suggest, as this is the fourth time we're doing static type tests here! |
Thanks for all your work on this @mirka |
@mikeybinns Thanks for all your work as well 🙌 |
Updated take on #60293 (props @mikeybinns)
What?
Tighten the TypeScript type checking on
SelectControl
, so thevalue
type matches in thevalue
,options
, andonChange
props.See Dev Notes at the bottom for how this looks like to a consumer.
Why?
It can catch potential bugs.
How?
Adding a generic that can either be inferred from the
options
array (if it is immutable), or be set explicitly.Testing Instructions
See some of the static type tests in the unit test file, and play around by adding your own cases and see what errors.
✍️ Dev Note
Stricter type checking in
SelectControl
In TypeScript codebases,
SelectControl
will now be able to type check whether thevalue
type matches in thevalue
,options
, andonChange
props.This will be inferred automatically if the
options
array is declared as immutable.If the
options
array is mutable,value
will remain a simplestring
type.This is effectively the same type checking behavior as before.
You can also set an explicit
value
type.