私の個人的な見解であり、所属する団体とは一切関係ありません。また、可能な限り検証は行っていますが、誤りがある可能性もあります。その際は、ご指摘いただけると嬉しいです!
はじめに
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> = {},
)
TFieldValues
は FieldValues
を継承して、なおかつ FieldValues
と等しい?それとも TFieldValues
は FieldValues = FieldValues
を継承している?と誤った理解をしていました。
紐解いていく
わかりやすそうなところから紐解いていく
FieldValues お前はだれだ!
export type FieldValues = Record<string, any>;
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 = FieldValues
は TFieldValues extends FieldValues
と = FieldValues
に分けて理解します。
TFieldValues extends FieldValues
は何だ!
TFieldValues
は FieldValues
を継承しています。型引数のコンテキストでオブジェクトを対象とした場合にオブジェクト指向で慣れ親しんだ継承とは違うようです。
次のサンプルをみて確認すると、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
はなにをしてるかというと
- 型チェックをパスできるのは
FieldValues
を継承したもの - 型を省略した場合は型引数の制約で FieldValues を継承したものを期待
- 型を省略した場合はデフォルト型引数の
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[])
終わりに
型むずかしい!