Open8
MaterialUIのAutocomplete.onChangeに渡すイベントハンドラーに型定義をする
概要としてはこういうAutocomplete.onChangeのイベントハンドラーを変数として定義する(直接propsに入れるのではなく)コードを書きたい。が、イベントハンドラーをの型付けに悩んだ。
import { Autocomplete, TextField } from "@material-ui/core"
export const MyComponent = ({ onChange } ) => {
// omit value and options setup
const onChange2 = useCallback((_, value) => {
onChange(value || '')
}, [onChange])
return (
<Autocomplete
value={value}
options={options}
onChange={onChange2}
renderInput={(params) => <TextField {...params} label="ID" />}
/>
)
}
まずこういう取り出し方を考えた。ハンドラーの型取り出しはこれでできる。
type AutocompleteProps = Parameters<typeof Autocomplete>[0]
type OnChange = NonNullable<AutocompleteProps['onChange']>
しかし問題は残っていた。こういう使い方をする際に、valueの方がunknownになっている。
const onChange2 = useCallback<OnChange>((_, value) => {
onChange(value || '')
}, [onChange])
型定義を辿る
// Autocomplete.d.ts
export default function Autocomplete<
T,
Multiple extends boolean | undefined = undefined,
DisableClearable extends boolean | undefined = undefined,
FreeSolo extends boolean | undefined = undefined,
>(props: AutocompleteProps<T, Multiple, DisableClearable, FreeSolo>): JSX.Element;
// Autocomplete.d.ts
export interface AutocompleteProps<
T,
Multiple extends boolean | undefined,
DisableClearable extends boolean | undefined,
FreeSolo extends boolean | undefined,
ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
> extends UseAutocompleteProps<T, Multiple, DisableClearable, FreeSolo>,
StandardProps<React.HTMLAttributes<HTMLDivElement>, 'defaultValue' | 'onChange' | 'children'> {
...
// useAutocomplete.d.ts
export interface UseAutocompleteProps<
T,
Multiple extends boolean | undefined,
DisableClearable extends boolean | undefined,
FreeSolo extends boolean | undefined,
> {
...
options: ReadonlyArray<T>;
...
value?: AutocompleteValue<T, Multiple, DisableClearable, FreeSolo>;
...
onChange?: (
event: React.SyntheticEvent,
value: AutocompleteValue<T, Multiple, DisableClearable, FreeSolo>,
reason: AutocompleteChangeReason,
details?: AutocompleteChangeDetails<T>,
) => void;
}
// useAutocomplete.d.ts
export type AutocompleteValue<T, Multiple, DisableClearable, FreeSolo> = Multiple extends
| undefined
| false
? DisableClearable extends true
? NonNullable<T | AutocompleteFreeSoloValueMapping<FreeSolo>>
: T | null | AutocompleteFreeSoloValueMapping<FreeSolo>
: Array<T | AutocompleteFreeSoloValueMapping<FreeSolo>>;
AutocompletePropsの定義を読むと、options, valueの型とonChangeのvalueは型引数または引数からの推論をしているだろう。エディタでの型の変化からしても合ってそう。
(ここの型引数のリレーを読みきるにはAutocompleteでどう型を渡しているか把握する必要がある?それともTS力不足なのか)
ハンドラーの型定義をする目的を達成するには型引数を埋めてあげれば辿り着けるか
UseAutocompleteProps<
T,
Multiple extends boolean | undefined,
DisableClearable extends boolean | undefined,
FreeSolo extends boolean | undefined,
>
今回の実際のケースでは、文字列を扱い、複数選択なし、入力クリアは有効、自由入力あり、なのでこういう型引数
UseAutocompleteProps<string, false, false, true>
ハンドラーを取り出すとこう
type OnChange = NonNullable<UseAutocompleteProps<string, false, false, true>['onChange']>
全体としてはこうなる。Autoompleteのpropsを変更すると型定義も変えないといけないのは面倒だけど、型チェックでエラーにはなるので不整合は発見できる。
import { Autocomplete, TextField } from "@material-ui/core"
type OnChange = NonNullable<UseAutocompleteProps<string, false, false, true>['onChange']>
export const MyComponent: FC<{ onChange: (v: string) => void }> = ({ onChange } ) => {
// omit value and options setup
const onChange2 = useCallback<OnChange>((_, value) => {
onChange(value || '')
}, [onChange])
return (
<Autocomplete
value={value}
options={options}
onChange={onChange2}
renderInput={(params) => <TextField {...params} label="ID" />}
/>
)
}
ややこしいのでコンポーネントのインスタンスから型を取り出すことができるならシンプルに解決できそう。だけど方法あるのかな。
そもそもかっちり型を合わせなくても良い
const onChange2 = useCallback(
(_: unknown, value: string | null) => onChange(value || ""),
[onChange]
);