React Hook FormのFieldErrorsの型を掘り下げる【自由研究】

## 目次
- 概要
- 調査のきっかけ
- 前提知識
- 調査
- 原因考察
- 修正方法
- 教訓


## 概要

- React Hook Formの型を掘り下げてみた自由研究記事
- `useFormState` と ジェネリクス を組み合わせたときの型エラーが起きた
- なんてことはなくただのType Narrowingの問題だった

※執筆時点でのバージョン (7.54.2)を利用
※ただの自由研究なのであまり有益な情報はないかもしれません

## 調査のきっかけ

React Hook Formの `useFormState`から 取得できる `errors` で、うまく型が当たらないケースに遭遇したため。

https://react-hook-form.com/

`FormState`からエラーを取り出すためのカスタムフックを作ろうとした。
ここで、以下のようにスキーマが確定していればTypeScriptからは怒られない。

```ts
type TodoFormSchema = { items: Array<string> }; // 配列を含むスキーマなのがポイント
function useTodoErrorMessage(key: keyof TodoFormSchema) {
  const { errors } = useFormState<TodoFormSchema>();
  const error = errors[key];
  // 配列として認識されているらしく、index Accessできる
  console.log(error?.[0]?.message); // OK
  console.log(error?.[1]?.message); // OK
  ...
}
```

なお、フォームスキーマをジェネリクスにして汎用化するとうまくいかない。

```ts
function useErrorMessage<T extends FieldValues>(key: keyof T) {
  const { errors } = useFormState<T>();
  const error = errors[key];
  // 配列にはなっておらずindexでアクセスできない
  console.log(error?.[0]?.message); // OK
  console.log(error?.[1]?.message); // OK
 ...
}
```

気になったので調べてみることにした。


## 前提知識

### Conditonal Types

条件分岐を使って型を定義できる。例えば以下のような場合 `true`型になる。

補足:このようケースの場合、`true`型のまま使うというよりは、この結果を使ってさらに別のConditional Typeを作ることのほうが多いように思う。

```ts
// IsString<"A"> = true
// IsString<1> = false
type IsString<T> = T extends string ? true : false;
```

https://typescriptbook.jp/reference/type-reuse/conditional-types

### keyof 演算子

keyofとinを組みあわせて利用すると、オブジェクトや配列を一括置換したりできる。

```ts
// ToString<{ a: number; b: boolean }> = { a: string; b: string; }
// ToString<[number, boolean]> = [string, string]
type ToString<T> = { [K in keyof T]: string }
```

https://typescriptbook.jp/reference/type-reuse/keyof-type-operator

### 調査
React Hook Formで定義されている型を順番に紐解いていく。


### IsAny型

名前の通り。`T`が`any`なら`true`型だし、`string`などの場合は`false`型になる。

```ts
// IsAny<any> = true
// IsAny<string> = false
export type IsAny<T> = 0 extends 1 & T ? true : false;
```

### DeepRequired型

これはよくみるやつで、オブジェクトに対し再帰的にnullableを外していくもの。

```ts
// DeepRequired<{ a?: { b?: string } }> = { a: { b: string } }
export type DeepRequired<T> = T extends BrowserNativeObject | Blob
  ? T
  : {
      [K in keyof T]-?: NonNullable<DeepRequired<T[K]>>;
    };
```

### FieldValues型

なんてことはないただの `Record`。第二引数が `any`なので object in objectな入れ子構造にも対応しているし、配列でも構わない。

```ts
export type FieldValues = Record<string, any>;
```

### FieldErrors型

`FieldErrorsImpl`のラッパーと思われるもの。`FieldValues extends IsAny<FieldValues>`は常に`false`だし、`root`は今回あまり気にしなくて良いので、`FieldErros<T> = FieldErrorsImpl<T>`と考えてしまって良さそう。

```ts
export type FieldErrors<T extends FieldValues = FieldValues> = Partial<
  FieldValues extends IsAny<FieldValues>
    ? any
    : FieldErrorsImpl<DeepRequired<T>>
> & {
  root?: Record<string, GlobalError> & GlobalError;
};
```



### FieldErrorsImpl型

`FieldErrors`の実体。渡された`FieldValues` (中身はRecord)を見て、それぞれのkeyに対して適当なobjectを設定しているように見える。

```ts
export type FieldErrorsImpl<T extends FieldValues = FieldValues> = {
  [K in keyof T]?: T[K] extends BrowserNativeObject | Blob
    ? FieldError
    : K extends "root" | `root.${string}`
    ? GlobalError
    : T[K] extends object
    ? Merge<FieldError, FieldErrorsImpl<T[K]>>
    : FieldError;
};
```

### 原因考察

ここまでは調査で、ようやくここからが本題。
ここで`FieldErrorsImpl` と `useErrorMessage` の両方を並べて見る。
するととあることに気づくことができた。

```ts
export type FieldErrorsImpl<T extends FieldValues = FieldValues> = {
  [K in keyof T]?: T[K] extends BrowserNativeObject | Blob
    ? FieldError
    : K extends "root" | `root.${string}`
    ? GlobalError
    : T[K] extends object
    ? Merge<FieldError, FieldErrorsImpl<T[K]>>
    : FieldError;
};
```

```ts
// MEMO: FieldValuesは Record<string, any>
function useErrorMessage<T extends FieldValues>(key: keyof T) {
  // MEMO errorsの型は FieldErrorsM<T> = FieldErrorsImpl<T>
  const { errors } = useFormState<T>();
  // FieldError | GlobalError | Merge<FieldError, FieldErrorsImpl<T[K]>>
  // のどれかになっていそう
  const error = errors[key];
```

`T extends FieldValues (=T extends Record<string, any>)` のままでは、

- `FieldError` 
- `GlobalError`
- `Merge<FieldError, FieldErrorsImpl<T[K]>>`

のどれなのかが決定できないからうまく型を推論してくれないのである。

そのため、以下のどちらかで対応すれば良さそうである。

- `T` に正しい制約をつける
- 関数内でType Narrowingを行う


## 修正方法

今回は関数内でType Narrowingする方法で修正した。

```ts
function useErrorMessage<T extends FieldValues>(key: keyof T) {
  const { errors } = useFormState<T>();
  
  // この段階では絞り込めていない
  // FieldError | GlobalError | Merge<FieldError, FieldErrorsImpl<T[K]>>
  const error = errors[key];

  // 
  // Merge<FieldError, FieldErrorsImpl<T[K]>>の時
  // 以下のようなerrorを想定
  // [{ message: "1st error" }, { message: "2nd error" }]
  //
  if(Array.isArray(error)) {
    if("message" in error[0]) {
      const message = error[0].message
      console.log(message)
    }
    ...
    return
  }
  
  ...
}
```

## 教訓
型を使えばうまく動くけどジェネリクスだと動かないようなケースは型の絞り込みに注意したい。