超直感的なバリデーション?ArkType入門
こんにちは、フォルシア株式会社エンジニアの籏野です。
今回はzodやvalibotと同じバリデーション用ライブラリであるArkTypeについて紹介したいと思います。
ArkTypeの特徴
ArkTypeの大きな特徴としては以下の点が挙げられます。
- TypeScriptの型記法に似た記法によりバリデーション定義が可能
- 実行時にはzodの100倍高速に動作し、エディター上でも高パフォーマンスな補完を提供する
-
Standard Schemaへの対応
- ※Standard Schemaはzodやvalibotといったバリデーションライブラリの共通インターフェース仕様です。
本記事では主にArkTypeの利用方法について紹介していきます。
セットアップ
筆者の環境は以下の通りです。
- Node.js v22.14.0
- pnpm 10.5.2
ArkTypeを利用する場合は以下の通り、TypeScriptの設定が必要です。
- TypeScriptのバージョンは5.1以上
-
strict
もしくはstrictNullChecks
を有効にする -
exactOptionalPropertyTypes
を有効にすることを推奨
またVSCodeを利用している場合、以下のように設定をしておくとよいようです。
// allow autocomplete for ArkType expressions like "string | num"
"editor.quickSuggestions": {
"strings": "on"
},
// prioritize ArkType's "type" for autoimports
"typescript.preferences.autoImportSpecifierExcludeRegexes": [
"^(node:)?os$"
],
またVSCodeユーザーはArkTypeの拡張機能を入れておくと、シンタックスハイライト/エラー表示等がサポートされるのでおすすめです。
上記用意したうえで、以下のコマンドでArkTypeをインストールします。
pnpm install arktype
記法
ArkTypeでは、最初に説明した通りTypeScriptの型記法に似た記法でバリデーション定義が可能です。
例えば、name
というプロパティでstring
型を持つuserオブジェクトを定義する場合は以下のように記述します。
import { type } from "arktype";
const user = type({
name: "string",
});
// バリデーションの実行
const result = user({...});
// 型定義は以下のようになる
// type User ={
// name: string;
// }
type User = typeof user.infer;
バリデーション定義user
とそこから生成される型User
を見比べてみると一目瞭然ですが、どちらもnameというプロパティをstring
で表現しています。
また、string もしくは numberのどちらかを満たす値のバリデーション定義にはユニオン型で用いられるパイプ記号(|
)を用いることで以下の用に記述することができ、「TypeScriptの型記法に似た記法」というのがよくわかるかと思います。
const user = type({
name: "string",
version: "string | number", // -> string | number
});
なお、ArkTypeではプリミティブな値のバリデーション定義として以下をサポートしており、これらを組み合わせて定義を作っていくことが基本となります。
その他にも以下のように様々なTypeScriptでの記法を利用することができます。
// リテラル型
type("'android' | 'ios' | 1 | true"); // -> 'android' | 'ios' | 1 | true
// 配列
type("string[]"); // -> string[]
type("(string | number)[]"); // -> (string | number)[]
// オブジェクト
type({
// 必須の項目
required: "string",
// 任意の項目
"optional?": "string",
})
// スプレッド構文
const user = type({ isAdmin: "false", name: "string" })
const admin = type({
"...": user,
isAdmin: "true",
permissions: "string[]"
}) // -> { isAdmin: true; permissions: string[]; name: string; }
TypeScriptの記法を超えたバリデーション定義
フォーム定義でよく使われる「最大10文字まで」や「メールアドレスであること」といった様々な制約条件は、TypeScriptの型記法だけで表現するのはなかなか難しく、そのような定義をArkTypeはどのように定義しているのでしょうか?
制約条件を"キーワード"で指定する
ArkTypeにおいて「メールアドレスであること」のような制約条件は、先に紹介したプリミティブな値に対するバリデーション定義に対して、.{{ 制約条件 }}
というような形でキーワードを用いて指定することができます。
以下にその一部を紹介しますが、他にも様々な制約条件が用意されているので、ぜひ公式ドキュメントも合わせて参照してみてください。
// string
type("string.email"); // -> メールアドレスであること
type("string.alphanumeric"); // -> 英数字のみであること
// number
type("number.integer"); // -> 整数であること
制約条件を"演算子"で指定する
「10文字以上」「5以上」「2の倍数」といった制約条件は、「<, <=, >=, >, %」といったTypeScriptでも用いる演算子を利用して指定することができます。
string型とnumber型で意味が微妙に異なる点は少し注意が必要かもしれません。
// string
type("string >= 10"); // -> 10文字以上であること
+type("0 < string <= 10"); // -> 0文字より大きく10文字以下であること
// number
type("number > 5"); // -> 5より大きいこと
type("number % 2"); // -> 2の倍数であること
より複雑な制約条件の定義
では、上記のような記法では表現しきれないさらに複雑なバリデーション定義はどのように表現するのでしょうか?
単純に独自のバリデーション方法を導入したい場合は、narrow
メソッドを用いることで定義できます。
// 奇数であることを表すバリデーション定義
const odd = type("number").narrow((n, ctx) =>
// if even, add a customizable error and return false
n % 2 === 0 ? ctx.mustBe("odd") : true
);
const out = odd(2);
if (out instanceof type.errors) {
// hover summary to see validation errors
console.error(out.summary); // -> must be odd (was 2)
} else {
console.log(out); // -> 1
}
他にもpipe
メソッドを用いることによりバリデーション結果を変換->再度バリデーションを行うこともできます。
const parseJson = type("string").pipe.try(
(s): object => JSON.parse(s),
type({
name: "string",
version: "string.semver"
})
);
またJSON.parseについては、ArkTypeがstring.json.parse
という定義を用意しているため、これを用いると単純にバリデーション定義同士を連結すればよいことになります。
その場合はto
メソッドを用いることで連結ができます。
const parseJson = type("string.json.parse").to({
name: "string",
version: "string.semver"
});
その他気になった機能の紹介
その他バリデーションライブラリを扱う上で気になりそうな点をいくつかまとめていきます。
デフォルト値の定義
APIの型定義などでは値が未指定の場合のデフォルト値の定義もしたいことがあります。
ArkTypeではオブジェクトかそれ以外かでデフォルト値の書き方が異なります。
// オブジェクトの場合、 = {{ デフォルト値 }}で指定する
const defaultableObject = type({
defaultableKey: "boolean = false"
});
defaultableObject({}); // -> { defaultableKey: false }
// それ以外の場合は、pipeメソッドを用いる
const defaultablePrimitive = type("string | undefined").pipe(v => v ?? "default");
defaultablePrimitive(undefined); // -> "default"
余分なプロパティに対する挙動
例えばzodの場合、あるオブジェクトのバリデーション定義に対して余分なプロパティがある場合はその値を削除します。
一方、ArkTypeでは余分なプロパティに対しては何もせず検証を通過させます。
const target = {
name: "John",
extraProperty: "extra"
}
const zodDefinition = z.object({
name: z.string()
});
const arkTypeDefinition = type({
name: "string"
});
zodDefinition.parse(target); // -> { name: "John" }
arkTypeDefinition(target); // -> { name: "John", extraProperty: "extra" }
ArkTypeでこの挙動を変更したい場合には、オブジェクトのバリデーション定義に+
プロパティを追加します。
const arkTypeDefinition = type({
name: "string",
// reject: 余計なプロパティがある場合はエラーとする
// delete: 余計なプロパティを削除する
// ignore: 余計なプロパティを無視する(デフォルト)
"+": "reject"
});
また、上記のような挙動をグローバルに設定する方法も用意されていますので、興味のある方は参照してみてください。
パターンマッチング
ArkTypeでは上記のようなTypeScriptライクな記法を用いてパターンマッチングを行う関数も提供しているので紹介します。
import { match } from "arktype";
const sizeOf = match({
"string | Array": (v) => v.length,
number: (v) => v,
bigint: (v) => v,
default: "never",
});
default
はどのパターンにもマッチングしなかった時に実行される分岐であり、以下のように生成される関数の入力の型と実行時の挙動が異なります。
値 | 入力の型 | 実行時の挙動 |
---|---|---|
assert | unknown型 | 例外を投げる |
never | default以外の指定値から推論 | 例外を投げる |
reject | unknown型 | 例外は投げず、ArkErrorを返す |
(data: In) => unknown | unknown | 関数を実行 |
最後に
ArkTypeいかがでしたでしょうか?
TypeScriptと似た記法という点は確かに学習コストという点で良いかもしれません。
ArkTypeには他にも紹介しきれなかった様々なメソッドがありますので、本記事で興味が出た方はぜひ試してみてください!
この記事を書いた人
籏野 拓
2018年新卒入社
Discussion