Open8

MaterialUIのAutocomplete.onChangeに渡すイベントハンドラーに型定義をする

na9amurana9amura

概要としてはこういう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" />}
    />
  )
}
na9amurana9amura

まずこういう取り出し方を考えた。ハンドラーの型取り出しはこれでできる。

type AutocompleteProps = Parameters<typeof Autocomplete>[0]
type OnChange = NonNullable<AutocompleteProps['onChange']>

しかし問題は残っていた。こういう使い方をする際に、valueの方がunknownになっている。

  const onChange2 = useCallback<OnChange>((_, value) => {
    onChange(value || '')
  }, [onChange])
na9amurana9amura

型定義を辿る

// 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>>;
na9amurana9amura

AutocompletePropsの定義を読むと、options, valueの型とonChangeのvalueは型引数または引数からの推論をしているだろう。エディタでの型の変化からしても合ってそう。
(ここの型引数のリレーを読みきるにはAutocompleteでどう型を渡しているか把握する必要がある?それともTS力不足なのか)

na9amurana9amura

ハンドラーの型定義をする目的を達成するには型引数を埋めてあげれば辿り着けるか

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']>
na9amurana9amura

全体としてはこうなる。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" />}
    />
  )
}
na9amurana9amura

ややこしいのでコンポーネントのインスタンスから型を取り出すことができるならシンプルに解決できそう。だけど方法あるのかな。

na9amurana9amura

そもそもかっちり型を合わせなくても良い

  const onChange2 = useCallback(
    (_: unknown, value: string | null) => onChange(value || ""),
    [onChange]
  );