TypeScript でオブジェクトのバリデータを作っている。それぞれの値ごとのバリデータは値の特性ごとに様々な関数がある((value: string) => Error[]
のような形)。
時折 Optional な値があるものの、バリデータ自体は non-null な値のバリデーションに専念してほしくて、Optional / Required な判定は別の関数に分けたい気持ちがあった。
別の関数に分けないと、バリデータの中で一々 Optional かどうかの引数を取って判定したり、下のように各バリデータに対応して Optional な判定を加えた関数を作る必要があって面倒。
const validateOptional = (value: string | null | undefined, allowAsterisk: boolean): Error[] => { if (value == null) { return [] // ok } return validate(value, allowAsterisk) }
これを、
- non-null 用のバリデーション関数を引数とし、null チェックを行う処理を差し込んだ nullable 用の新しいバリデーション関数を返す高階関数を作成する
- 様々なバリデーション関数の引数に対応するため、TypeScript の Variadic Tuple Types を使う
ことによってスッキリできた。
例として、non-null な値しか受け取らないバリデータに対して、値を Optional とするための高階関数を下のように作った。
const optional = <U, T extends unknown[]>(validate: (value: U, ...args: [...T]) => Error[]) => (value: (U | null | undefined), ...args: [...T]) => { if (value == null) { return []; // ok } return validate(value, ...args); }
nullable な value
を受け取って null チェックを行い、null | undefined であれば ok、non-null であれば validate
関数に再度投げている。それ自体は初めの関数と特に変わらない。
見所は Variadic Tuple Types を使っているところで、 validate
関数の1引数目以外をまとめて [...T]
と受け取ることで、1つ目以外の引数(とその型情報)を維持したまま新しい関数を生成できている。
これがなかったら、バリデータ関数のオプションを全て2引数目に Object として押し込む (value: U, options: T) => Error[]
のような形)ようにインターフェースを統一することになっていたと思う。
// 例 const validate = (val: string, allowAsterisk: boolean) => { const errors = []; if (val.length >= 10) { errors.push(new Error("value must be shorter than 10 chars")) } if (!allowAsterisk && val.includes('*')) { errors.push(new Error("value must not contain character *")) } return errors } // validate: (val: string, allowAsterisk: boolean) => Error[] // optional(validate): (val: string | null | undefined, allowAsterisk: boolean) => Error[] console.log( optional(validate)("*", true), // => [value must not contain character *] optional(validate)("*", false), // => [] optional(validate)(undefined, true) // =>[] )
TypeScript たのし~~