FormDataをTypeScriptで型安全に扱いたい

こんにちは、あかしぃです。 Ajax通信でファイルデータを送信するときによく使う「FormData」。こいつを型安全に扱いたい...というのがこの記事のテーマです。

シンプルにこう書けばいいのか...?と思って以下のようにやってみましたが、これはダメでした。

// この型を適用すれば、sample以外をappendしようとすると型エラーになったり...?
interface CustomFormData {
  sample: string;
}

// この時点でエラー。ダメだった...
const formData = new FormData() as CustomFormData;

フリーダムなkeyで登録することを許可するのはアレなので、なんとかしたいところです。

FormDataのtypeを見てみる

そもそも、FormDataのtypeはどうなっているのか?ということで、それを見てみると、

interface FormData {
    append(name: string, value: string | Blob, fileName?: string): void;
    delete(name: string): void;
    get(name: string): FormDataEntryValue | null;
    getAll(name: string): FormDataEntryValue[];
    has(name: string): boolean;
    set(name: string, value: string | Blob, fileName?: string): void;
    forEach(callbackfn: (value: FormDataEntryValue, key: string, parent: FormData) => void, thisArg?: any): void;
}

getやhasのような、内部データにアクセスするメソッドは公開されているものの、格納されているデータそのものに直接アクセスするのは不可能っぽい...?といった感じ。

なので、外部から内部データの型を直接指定するのは無理そうです。

既存の型を拡張する方向で攻める

ここで方向転換して、上記のtypeをあーだこーだして型で縛る作戦を考えてみます。

formDataにデータを格納する際に使うメソッドは「append」です。であれば、こいつの引数を任意の型で縛れば、それ以外のデータは格納できなくなり、当初の目的は達成されるのでは...?ということで、appendの引数を上書きすることを考えます。

TypeScriptにおいて、既存の型を上書きしたり拡張するにはextendsを使います。そして、今回、上書きしたいのはappendだけなので、こうすればOK。

interface CustomFormData extends FormData {
  append(name: 'sample', value: string | Blob, fileName?: string)
}

// 「sample」以外のkeyでデータを登録しようとすると型エラーになる
const formData = new FormData() as CustomFormData;

// これはOK
formData.append("sample", "test");
// これは型エラー
formData.append("test", "sample");

よさそうですね!送信するデータのkey名がすでに決まっているなら、以下のようにもできます。

interface SubmitData {
  name: string;
  email: string;
  password: string
}

// name, email, password以外のkeyでappendしようとすると型エラーになる
interface CustomFormData extends FormData {
  append(name: keyof SubmitData, value: string | Blob, fileName?: string)
}

もう少し厳密にするならこうしてみたり...。

interface CustomFormData extends FormData {
  append<T extends string | Blob>(name: keyof SubmitData, value: T, fileName?: string)
}

const formData = new FormData() as CustomFormData;
// 格納するデータの型を明示的に指定できるように
formData.append<string>("email", "sample@example.com")

これで、formDataに想定していないkeyでデータが登録されるのを防ぐことができました。 時間があれば、formData自体がどういった実装になっているのか見てみるのも面白そうです。

\ SHARE /