0
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?

【TypeScript】フォームエラーの型をMapped Typesで拡張する

0
Posted at

はじめに

この投稿は、TypeScript学習者が自身の理解のために書いています。フォームのバリデーションエラーの型をMapped Typesで書くとき、フィールドごとに型を変えたり、ネストしたフィールドに対応したりする拡張パターンを整理していきます。

会員管理アプリの登録フォームを例にします。フォームの値の型MemberFormValuesに対し、エラーの型はMapped Typesで次のように書けます。

type MemberFormValues = {
  name: string;
  email: string;
  phone: string;
  department: string;
};

type MemberFormErrors = {
  [K in keyof MemberFormValues]?: string;
};

[K in keyof T]?: stringは、Partial<Record<keyof T, string>>と同型です。この形を出発点に、Mapped TypesとConditional Typesを組み合わせた形と、ネストしたプロパティにMapped Typesを再帰的に適用する形を見ていきます。

React Hook FormやZodには、ネストしたエラーを扱う機能も用意されています。本投稿はライブラリを使わず、型の組み立て方だけを見ます。

1. フィールドごとにエラーの型を変える——Mapped TypesとConditional Types

すべてstringではなく、emailだけstring[]にしたい場合、Mapped Typesの値側をConditional Typesで分岐させます。

type MemberFormValues = {
  name: string;
  email: string;
  phone: string;
  department: string;
};

type MemberFormErrors = {
  [K in keyof MemberFormValues]?: K extends "email" ? string[] : string;
};

const errors: MemberFormErrors = {};

errors.name = "氏名は必須です";
errors.email = ["形式が不正です", "すでに登録済みです"];
errors.phone = "数字のみ入力してください";

K extends "email" ? string[] : stringはConditional Typesです。[K in keyof MemberFormValues]でキーを1つずつ回しながら、キー名に応じて値の型を変えています。

会員フォームと商品フォームのように複数画面で使い回すなら、FormErrors<T, MultiLineKeys>として共通化できます。複数メッセージにしたいキーだけ型引数で渡す形にします。

type FormErrors<
  T extends Record<string, unknown>,
  MultiLineKeys extends keyof T = never,
> = {
  [K in keyof T]?: K extends MultiLineKeys ? string[] : string;
};

type ProductFormValues = { sku: string; name: string; price: number };

type MemberFormErrors = FormErrors<MemberFormValues, "email">;
type ProductFormErrors = FormErrors<ProductFormValues>;

MemberFormErrorsではemailだけstring[]、それ以外はstringです。ProductFormErrorsMultiLineKeysの既定値がneverなので、すべてstringのままです。

2. ネストしたフィールド——Mapped Typesを再帰的に適用

MemberFormValuesAddressFormValues型のaddressプロパティを足すと、[K in keyof T]?: stringではaddressキー全体に1つのstringしか載せられません。postalCodecityのように子プロパティごとのエラーを返すには、Mapped Typesの値側をConditional Typesで分岐し、オブジェクトのプロパティにMapped Typesを再帰的に適用します。

type AddressFormValues = {
  postalCode: string;
  city: string;
};

type MemberFormValues = {
  name: string;
  email: string;
  address: AddressFormValues;
};

type NestedFormErrors<T> = {
  [K in keyof T]?: T[K] extends string | number | boolean
    ? string
    : T[K] extends readonly unknown[]
      ? string
    : T[K] extends Date
      ? string
    : T[K] extends object
      ? NestedFormErrors<T[K]> | string
      : string;
};

type MemberFormErrors = NestedFormErrors<MemberFormValues>;

const errors: MemberFormErrors = {};

errors.name = "氏名は必須です";
errors.address = { postalCode: "7桁で入力してください" };
errors.address = { postalCode: "7桁で入力してください", city: "市区町村は必須です" };
errors.address = "住所全体を確認してください";

T[K]がプリミティブならstring、プレーンなobjectならNestedFormErrors<T[K]> | stringです。addressプロパティには子プロパティごとのエラー对象か、プロパティ全体への1メッセージのどちらかを載せられます。

T[K] extends objectだけだと、string[]Dateobjectに当たり、意図しない再帰になります。上の定義では配列とDateを先にstringの葉として分岐しています。フォームに関数やクラスインスタンスが混ざる場合は、同様にもう一段除外が要ります。

まとめ

  • フォームエラーの型の基本は{ [K in keyof T]?: string }としてMapped Typesで書ける。
  • 値をK extends MultiLineKeys ? string[] : stringのようにConditional Typesで分岐し、型引数で複数フォームに使い回せる。
  • ネストしたプロパティごとにエラーを載せるには、Mapped Typesの値側をConditional Typesで分岐し、オブジェクトのプロパティにMapped Typesを再帰的に適用して書ける。
0
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
0
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?