かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

TypeScript FormData.entries() で取得したフォームデータの型を interface から作成したい

TypeScript な React のフォームを作成していて new FormData() で取得したフォーム内容のデータの型を送信する形式の interface から作成したかったのメモ

FormData で取得されるデータの形式

const formData = [...new Form(form).entries()];
// => [[name1, value1], [name2, value2], ...]

new Form(form) は FormData オブジェクトなので、フォームの namevalue を扱うために FormData.entries() したものを使う。
FormData.entries() はフォームの [name, value] のセットの配列を返す。

interface から FormData.entries() の返す配列の型を作成する

interface

interface IFromSchema {
  name: string;
  email: string;
  amount: number;
  isAgree: boolean;
}

FormData.entries() の返す形式が [name, value][] なので、nameIFromSchema の key、value の型は IFromSchema[key] で最終的に次のような型ができれば良さそう

type IFromSchemaEntries = [
  ['name', string];
  ['email', string];
  ['amount', number];
  ['isAgree', boolean];
];

タプルの方が良いのかな?

Mapped Types を利用して Entries で作成される型を作る

type Entries<T> = {
  [K in keyof T]: [K, T[K]];
}[keyof T][];

👇

type T1 = {
  [K in keyof IFromSchema]: [K, IFromSchema[K]]
};
// T1 は次のようなオブジェクト形式の型になる
// {
//   name: [name, string],
//   email: [email, string],
//   amount: [amount, number],
//   isAgree: [isAgree, boolean],
// }

type T2 = T1[keyof IFromSchema];
// T2 はオブジェクトに[key] でアクセスしている形なので次用な型になる
// => type T2 = [name, string] | [email, string] | [amount, number] | [isAgree, boolean];
// 配列 or タプル形式になっていれば良いので

type T3 = T1[keyof IFromSchema][];
// => type T3 = ([name, string] | [email, string] | [amount, number] | [isAgree, boolean])[];
// key と key に対応した型がセットになったタプル型の配列になる

// 抽象化して
type Entries<T> = {
  [K in keyof T]: [K, T[K]];
}[keyof T][];

type FormEntriesType = Entries<IFromSchema>;
// => ([name, string] | [email, string] | [amount, number] | [isAgree, string])[];

これで Object.entries() したときの型が Object の interface から作れつようになりました!
但し、([name, string] | [email, string] | [amount, number] | [isAgree, string]) なので確実に全ての key が含まれているとは限らない配列ということになると思います。Form のチェックボックスとかチェックがない場合 FromData に含まれないからこの方が扱いやすいかも!?

FormData で使ってみる

const handleFormSubmit = (evt: React.FormEvent<HTMLFormElement>) => {
  evt.preventDefault();
  const form = formRef.current;
  if (!form) {
      return;
  }

  const FormDataEntries = [...new Form(form).entries()] as  Entries<IFromSchema>;
  
  // Entries<IFromSchema> はキーが不足していても通るのでバリデーションエラーになるデフォルトのデータを用意しておく
  const defaultData: IFromSchema = {
    name: '',
    email: '',
    amount: 0,
    isAgree: false,
  };
 
  // IFromSchema 形式に戻してvalidation / post する
  const postData = FormDataEntries.reduce((data, item) => {
    const [key, value] = item;
    return {
      ...data,
      [key]: value,
    };
  }, defaultData);

  if (validate(postData)) {
    // フォームを送信
  }
};

[...new Form(form).entries()] as Entries<IFromSchema>as で型を付けているので完全に安全な訳ではないのでバリデーションでチェックする必要があるとお思いますが、型補完が効いて扱いやすくなりました!

所管

本当にこの方法が良いのか解ってない部分がありますが、実現したいことはまぁできたかな〜という感じ。こうやって調べている中で TypeScript のジェネリックもチョットづつ分かってきた気がします!
₍ ᐢ. ̫ .ᐢ ₎👌 TIL (Today I Learned) 少しづつ強くなる


[参考]

めばちさんの絵ホント好き (ネタ画像ネタ切れ!)