-
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: improve prop types for single vs multiple selection #47390
Conversation
value
types
Hey @sirreal , I'd love to get your help on this (although it's definitely not urgent!). What I'd like to land on, is a way for The Is there a way to write types so that a consumer of <SelectControl
multiple={ true }
// This should error, but it's currently ok because at the moment `value` can be both `string` or `string[]`
value="example"
/> Thank you! |
6907390
to
43efc9f
Compare
👋 I took a look and have an example implementation to share. Whenever we say "this needs to be this or that" we're talking about a union type. In this case we have These are the important bits. We make the props we want for either side of the union, one with For convenience, we put the shared props in another interface and each side of the union extends that. interface PropsBase {
readonly aNumber: number;
readonly aString: string;
}
interface PropsMultiple extends PropsBase {
readonly multiple: true;
readonly value: ReadonlyArray<string>;
readonly onChange: (values: ReadonlyArray<string>) => void;
}
interface PropsSingle extends PropsBase {
readonly multiple?: false;
readonly value: string;
readonly onChange: (values: string) => void;
}
type Props = PropsMultiple | PropsSingle; Then, when we go to use the component, based on the presence or absence (truthiness) of import type { FC } from 'react';
declare const Komponent: FC<Props>;
<Komponent aNumber={0} aString='a' value='a' onChange={(val:string) => val.toLowerCase()}/>;
// @ts-expect-error This has multiple but is a single string!
<Komponent aNumber={0} aString='a' multiple value='a' onChange={(val:string) => val.toLowerCase()}/>;
<Komponent aNumber={0} aString='a' multiple value={['a']} onChange={(val:readonly string[]) => val.map(s=>s.toLowerCase())}/>;
// @ts-expect-error This is an array of strings but doesn't have multiple!
<Komponent aNumber={0} aString='a' value={['a']} onChange={(val:readonly string[]) => val.map(s=>s.toLowerCase())}/>; |
43efc9f
to
bdcf2a5
Compare
bdcf2a5
to
bef66de
Compare
const SelectControlWithState: ComponentStory< typeof SelectControl > = ( | ||
props | ||
) => { | ||
const [ selection, setSelection ] = useState< string[] >(); | ||
|
||
if ( props.multiple ) { |
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 felt slightly hacky, but I couldn't figure out a much more elegant way.
Basically, since the value
of SelectControl
is controlled with the selection
variable from internal state, I couldn't find an easy way to get TypeScript to automatically understand when selection
can be used as a string
and when it can be used as a string[]
.
Therefore, I decided to have selection
to always be string[]
, and I've added some code to "adapt" the array to a single value for when multiple !== true
.
In doing do, I also had to "override" the multiple
prop passed to SelectControl
to avoid a TypeScript error.
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.
For stories this seems reasonable.
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.
Agreed with Jon here, I believe it also makes it quite explicit and transparent what we're aiming to do, which is always useful for a story/test/example.
// `TreeSelect` inherits props from `SelectControl`, but only | ||
// in single selection mode (ie. when the `multiple` prop is not defined). | ||
export interface TreeSelectProps | ||
extends Omit< SelectControlSingleSelectionProps, 'value' | 'multiple' > { |
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.
We can omit the multiple
prop from TreeSelect
(since it doesn't seem to be relevant to the component). This simplified the component's prop types and removes a bunch of TS errors, allowing us to remove hacks like the ones in the QueryControls
component
Hey @sirreal , I pushed some changes to the component. The types should now work as expected, but it would be great to get some final feedback from you before I mark the PR as ready for final review. I added some self-review comments to point out the most interesting aspects of the PR. |
Flaky tests detected in ebe1e8f. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/4354320245
|
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 looked this over and the changes seem reasonable to me 👍
const SelectControlWithState: ComponentStory< typeof SelectControl > = ( | ||
props | ||
) => { | ||
const [ selection, setSelection ] = useState< string[] >(); | ||
|
||
if ( props.multiple ) { |
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.
For stories this seems reasonable.
bef66de
to
4a7c322
Compare
After solving a couple of TypeScript hurdles, this PR is now ready for review |
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.
Looks great 👍 Nice work unionizing the single and multiple selection scenarios 🚀
@@ -39,7 +41,7 @@ function UnforwardedSelectControl( | |||
label, | |||
multiple = false, | |||
onBlur = noop, | |||
onChange = noop, | |||
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.
Breaking change?
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.
It shouldn't be, since I've added optional chaining when calling it in the file, which should have the same effect as defaulting to noop
const SelectControlWithState: ComponentStory< typeof SelectControl > = ( | ||
props | ||
) => { | ||
const [ selection, setSelection ] = useState< string[] >(); | ||
|
||
if ( props.multiple ) { |
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.
Agreed with Jon here, I believe it also makes it quite explicit and transparent what we're aiming to do, which is always useful for a story/test/example.
}; | ||
|
||
const classes = classNames( 'components-select-control', className ); | ||
|
||
/* eslint-disable jsx-a11y/no-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.
I realised that this rule was not relevant anymore — removing this line doesn't seem to cause any ESLint errors. Furthermore, the rule is deprecated
I am not certain that there is actually an API change or if this just converted the component to typescript to improve the DX experience. Flagged for Dev note to get a mention in the Misc Editor Dev note for 6.3 |
Hey @bph , this PR only improves the types for the |
Thank you. |
What?
Type the props for the
SelectControl
component with more details, taking advantage of the fact that that thevalue
andonChange
prop types are affected by whether the component has multiple selection enabled or notWhy?
The way
SelectControl
is currently typed is not precise and can lead to hacky types workarounds, like in #46721How?
Rewrite
SelectControlProps
by making the result of the union of two types:multiple
istrue
, we can assume thatvalue
(and therefore the argument ofonChange
) is of typestring[]
value
(and theonChange
argument) is of typestring
Testing Instructions
SelectControl
works as expected in Storybook, and that Storybook docs still look correctQueryControls
when editing the settings of the "Latest posts" block)