1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

useForm の型引数について調べてみた!

Last updated at Posted at 2024-11-17

私の個人的な見解であり、所属する団体とは一切関係ありません。また、可能な限り検証は行っていますが、誤りがある可能性もあります。その際は、ご指摘いただけると嬉しいです!

はじめに

react-hook-form を使ってフォームを追加して遊んでおり、ある用途のパラメータがわからないので react-hook-form を解析してるうちに型引数の沼にハマり、脱した(?)ので備忘録として残します。

出会いは突然に

useForm を調べていくうちに TFieldValues extends FieldValues = FieldValues と出会いました。そこから泥沼です

export function useForm<
  TFieldValues extends FieldValues = FieldValues,
  TContext = any,
  TTransformedValues extends FieldValues | undefined = undefined,
>(
  props: UseFormProps<TFieldValues, TContext> = {},
)

URL: https://github.com/react-hook-form/react-hook-form/blob/29ae59660995c4c0728aac26dd9051fc617a1548/src/useForm.ts#L46-L52

TFieldValuesFieldValues を継承して、なおかつ FieldValues と等しい?それとも TFieldValuesFieldValues = FieldValues を継承している?と誤った理解をしていました。

紐解いていく

わかりやすそうなところから紐解いていく

FieldValues お前はだれだ!

export type FieldValues = Record<string, any>;

URL: https://github.com/react-hook-form/react-hook-form/blob/29ae59660995c4c0728aac26dd9051fc617a1548/src/types/fields.ts#L26

Record<Keys, Type>

RecordはプロパティのキーがKeysであり、プロパティの値がTypeであるオブジェクトの型を作るユーティリティ型です。

となっています。

引用:https://typescriptbook.jp/reference/type-reuse/utility-types/record

つまり、Record<string, any> だと次のようなオブジェクトは型通りです。

{
  'name': 'Jiro YAMADA',
  'age': 56
}

TFieldValues extends FieldValues = FieldValues は何だ!

まず TFieldValues extends FieldValues = FieldValuesTFieldValues extends FieldValues= FieldValues に分けて理解します。

TFieldValues extends FieldValues は何だ!

TFieldValuesFieldValues を継承しています。型引数のコンテキストでオブジェクトを対象とした場合にオブジェクト指向で慣れ親しんだ継承とは違うようです。

次のサンプルをみて確認すると、Field 型は value プロパティを持つ型です。このコンテキストでいう継承は value プロパティを含むオブジェクトであれば OK ということです。また、サンプルでは型を省略して実行していますが、これは型引数の制約から推論しています。

type Field = {
  value: string
};

function submitForm<T extends Field>(fields: T[]) {
  fields.forEach((field, _) => {
    console.log(field)
  });  
}

// 実行できる
submitForm([
  {
    value: 'Taro'
  },
  {
    value: 'YAMADA'
  },
])

// 実行できる
submitForm([
  {
    value: 'Taro',
    hiddenValue: 'taro'
  },
  {
    value: 'YAMADA'
  },
])


// 実行できない
submitForm([
  {
    checkboxValue: 'Taro' // checkboxValue はない
  },
  {
    value: 'YAMADA'
  },
])

= FieldValues は何だ!

これは型引数のデフォルトになります。Field に型を指定しない場合は number になります。

type Field<T = number> = {
  value: T;
};

// 'banana' は number ではないので型チェックでエラー!
const field0: Field = {
  value: 'banana',
}

const field1: Field<string> = {
  value: "",
};

const field2: Field<"checkbox"> = {
  value: "checkbox",
};

合わせると・・・?

TFieldValues extends FieldValues = FieldValues はなにをしてるかというと

  1. 型チェックをパスできるのは FieldValues を継承したもの
  2. 型を省略した場合は型引数の制約で FieldValues を継承したものを期待
  3. 型を省略した場合はデフォルト型引数の FieldValues を継承したものを期待

をしています。

あれ?省略したときの振る舞いが同じですね。そうここがわからないポイントなんです。

勘違いしてる場合があります。ご注意ください。

react hook form でも同じような記述はありますし次のプログラムを書いてみたのですが、すべてのプロパティをすべて必須にしている(つもり)なのですが、一部のプロパティだけでも型チェックはパスします。

type Field = {
  value: string
};

type HiddenField = {
  value: string,
  hiddenValue: string
};

type RequiredHiddenField = Required<HiddenField>

function submitForm<T = RequiredHiddenField>(fields: T[]) {
  fields.forEach((field, _) => {
    console.log(field)
  });  
}

// 実行できる
submitForm([
  {
    value: 'Taro'
  },
  {
    value: 'YAMADA'
  },
])

もちろん次のように extends を使って継承すれば期待通り、すべてのプロパティがあるオブジェクトを期待します。

function submitForm<T extends RequiredHiddenField>(fields: T[])

終わりに

型むずかしい!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?