header

<K extends T = T> ジェネリクスが代入されていたので、調べてみた

公開日: 2022年1月22日

最終更新日: 2023年1月7日

前置き

TypeScript + react hook formを使用してプロジェクトを作成していると、とても便利で難しい書き方をしているネットの記事を見つけました。
そのコードがこちらです。

type Props<T extends FieldValues> = {
  label: string;
  type?: React.HTMLInputTypeAttribute;
} & UseControllerProps<T>;

この型は Input の型定義で使用しており、コード全体ですと

import { UseControllerProps, FieldValues, useController } from 'react-hook-form';

type Props<T extends FieldValues> = {
  label: string;
  type?: React.HTMLInputTypeAttribute;
} & UseControllerProps<T>;

export const Input = <T extends FieldValues>({ label, type, name, control }: Props<T>) => {
  const {
    field: { ref, onChange },
  } = useController({ name, control });

  return (
    <label className='block py-2'>
      <span className='block pb-1 text-blue-800'>{label}</span>
      <input
        className='py-1 px-2 text-blue-600 rounded-md border border-blue-600/50'
        ref={ref}
        onChange={onChange}
        type={type}
      />
    </label>
  );
};

この様になります。

呼び出し側はこの様に記載しています。

import type { NextPage } from 'next';
import { useForm, SubmitHandler } from 'react-hook-form';

import { H1, H2, SubmitButton } from 'C/common';

import { Input } from 'C/first';

type FirstFieldValues = {
  name: string;
  age: number;
};

const Home: NextPage = () => {
  const { handleSubmit, control } = useForm<FirstFieldValues>();

  const firstSubmitHandler: SubmitHandler<FirstFieldValues> = (data) => {
    alert(`名前: ${data.name}\n年齢: ${data.age}`);
  };

  return (
    <main className='p-4 mx-auto max-w-3xl bg-slate-100'>
      <H1>型のテストページ</H1>
      <H2>ネットで見たフォームの型定義の書き方</H2>
      <form onSubmit={handleSubmit(firstSubmitHandler)}>
        {/* ここで、nameの補完として name, age が出てくる! */}
        {/* 今回は、この型を勉強・実装してみる。 */}
        <Input control={control} name='name' label='名前' />
        <Input control={control} name='age' label='年齢' />
        <SubmitButton label='送信' />
      </form>
    </main>
  );
};

export default Home;

これで何が便利だったかというと、

スクリーンショット 2022-01-19 23.04.36

このように、nameを入力したときに、FirstFieldValuesのkeyに基づいてnameの補完をしてくれます。
さらに、それが control を渡すだけで実現できるので、フォームにずれが発生しないという状態です。

この書き方は便利だ!と思い、型宣言を見たところ、見慣れない書き方がいくつかありました。

export type UseControllerProps<
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
  name: TName;
  rules?: Omit<
    RegisterOptions<TFieldValues, TName>,
    'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'
  >;
  shouldUnregister?: boolean;
  defaultValue?: UnpackNestedValue<FieldPathValue<TFieldValues, TName>>;
  control?: Control<TFieldValues>;
};

https://github.com/react-hook-form/react-hook-form/blob/master/src/types/controller.ts

TFieldValues extends FieldValues = FieldValuesん!?ジェネリクスって = で代入できるの?

今回は変数を渡したら、変数のジェネリクスで使っているkeyを取得→補完までを自分で実装してみようと思います。

環境・ライブラリ

  • Next.js
  • tailwindcss
  • react-hook-form

react-hook-formの型を見てみる

まずは、実際に UseControllerProps の型定義を見てみましょう。

(前置きで書いていたものと同じです。)

export type UseControllerProps<
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
  name: TName;
  rules?: Omit<
    RegisterOptions<TFieldValues, TName>,
    'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'
  >;
  shouldUnregister?: boolean;
  defaultValue?: UnpackNestedValue<FieldPathValue<TFieldValues, TName>>;
  control?: Control<TFieldValues>;
};

https://github.com/react-hook-form/react-hook-form/blob/master/src/types/controller.ts

なるほどなるほど。

TFieldValues を活用して、 TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, この TName を宣言しているみたいですね。

でも、この書き方は始めてみました。

この、型定義の中での = の使い方を今回は調べることとします。

ジェネリクスでのイコールの使い方について調べる

とりあえずググったところstackoverflowに同じ質問をしている方を発見。

Qiitaの記事もありました!

https://qiita.com/Quramy/items/b45711789605ef9f96de

詳しく解説してくださってますね!
では、これをうまいことReactの型に組み込んでみようと思います!

型定義を実装する

まずはコンポーネント実装

なんとなくこのようなコンポーネントを実装してみました。

type Value = string | number;

type Values = {
  [x: string]: Value;
};

type Props = {
  label: string;
  values: Values;
  keyStr: string;
};

export const Label = <T extends Values>({ label, values, keyStr }: Props<T>): JSX.Element => (
  <dl className='block py-2'>
    <dt className='block pb-1 text-green-800'>{label}</dt>
    <dd className='py-1 px-2 text-green-600 rounded-md border border-green-600/50'>
      {keyStr}: {values[keyStr]}
    </dd>
  </dl>
);

この場合の欠点としては、 keyStr が間違っていた時点でjsエラーが発生してしまいますね。

型に keyof を使用して、object keyを活用

type Props<T extends Values> = {
  label: string;
  values: T;
  keyStr: keyof T;
};

export const Label = <T extends Values>({ label, values, keyStr }: Props<T>): JSX.Element => (
  <dl className='block py-2'>
    <dt className='block pb-1 text-green-800'>{label}</dt>
    <dd className='py-1 px-2 text-green-600 rounded-md border border-green-600/50'>
      {keyStr}: {values[keyStr]}
    </dd>
  </dl>
);

はい!これで呼び出しのときにobjectのkeyが保管されるようになりましたね!

  const [junk] = useState({
    drink: 'コカ・コーラ',
    food: 'ハンバーガー',
    price: 300,
  });
// (略)
			<div>
        <Label label='飲み物' values={junk} keyStr='drink' />
      </div>

![スクリーンショット 2022-01-22 16.50.50](/Users/gashira/Pictures/screenshot/スクリーンショット 2022-01-22 16.50.50.png)

これで、呼び出しのときに keyStr の打ち間違いは減りました!

最後にPropsのジェネリクスを活用しましょう!

react-hook-formの書き方に近づける

TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>

この書き方を真似るために、KeyOf という受け取ったObjectに keyof を実行するだけの型を定義しました。

type Value = string | number;

type Values = {
  [x: string]: Value;
};

// ここが、複雑な型になったときにも Props の方はシンプルにかけて、流用もできる。
type KeyOf<T extends Values> = keyof T;

type Props<T extends Values, K extends KeyOf<T> = KeyOf<T>> = {
  label: string;
  values: T;
  keyStr: K;
};

export const Label = <T extends Values>({ label, values, keyStr }: Props<T>): JSX.Element => (
  <dl className='block py-2'>
    <dt className='block pb-1 text-green-800'>{label}</dt>
    <dd className='py-1 px-2 text-green-600 rounded-md border border-green-600/50'>
      {keyStr}: {values[keyStr]}
    </dd>
  </dl>
);

この書き方で、 K extends KeyOf<T> = KeyOf<T> = KeyOf<T> を消してみたところ、エラーが出ました。

![ts-error-props](/Users/gashira/Pictures/screenshot/スクリーンショット 2022-01-22 16.58.57.png)

ジェネリック型 'Props' には 2 個の型引数が必要です。

なるほど! = KeyOf<T> を書くことによって、型の代入をしているので2個めの型が必要なくなる。という流れだったんですね。

jsのデフォルト引数と同じ使い方だと理解しました。

なんだか、すごく親近感が湧くようになりました。これでこの書き方も読めるようになった気がします。

最後に

ちょっとだけTypeScriptに強くなった気がするので、これから書くコードも可読性、使いやすさを向上させて行けるかなと感じました。
また、新しく知った技術や小ネタなどあったらちょくちょく書いていきます。

もし間違ったことや、聞きたいことがありましたら Twitter にてDMなどいただければと思います。
近々メールフォームか、コメント機能を用意したいですね。