Unyablog.

のにれんのブログ

TypeScript 型わくわく日記

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)
}

これを、

  1. non-null 用のバリデーション関数を引数とし、null チェックを行う処理を差し込んだ nullable 用の新しいバリデーション関数を返す高階関数を作成する
  2. 様々なバリデーション関数の引数に対応するため、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 たのし~~