忙しい人のためのValibot入門
前書き
この記事はスキーマバリデーションライブラリを使ったことがない人が Valibot を最低限使えるようになることを目指しています。
Valibot とは
公式サイトのトップには以下のように書かれています。
Valibot is the open source schema library for TypeScript with bundle size, type safety and developer experience in mind.
DeepL で翻訳すると
Valibot は、バンドルサイズ、型の安全性、開発者のエクスペリエンスを考慮した TypeScript 用のオープンソースのスキーマライブラリです。
つまり TypeScript 用のスキーマライブラリです。
何ができる?
データ構造(以下、スキーマ)を定義してデータをバリデーションすることができます。
バリデーションに成功した時に返される値は、定義したスキーマの型が付与されます。
外部から取得する any 型のデータやユーザー入力のデータを扱う際に便利です。
簡単な例を以下に示します。ここでは雰囲気を掴んでいただければ大丈夫です。
import * as v from "valibot";
// スキーマを定義
// ここでは name: string と age: number を持つオブジェクトを定義している
const User = v.object({
name: v.string(),
age: v.number(),
});
// APIなどから取得したany型のデータ
const data: any = {
name: "John",
age: 20,
};
// バリデーション
// 失敗したらエラーをthrowし、成功したらdataの値が型付きで返される
const user = v.parse(User, data);
// const user: {
// name: string;
// age: number;
// }
console.log(`${user.name} は ${user.age} 歳です`);
// John は 20 歳です
本当はバリデーションだけでなくデータの整形も出来るのですが、この記事ではバリデーションについてのみ説明します。
柱となる要素
スキーマ
スキーマはデータ構造を定義するためのものです。これが無いと始まりません。
上記の例で言うと以下の部分です。
const User = v.object({
name: v.string(),
age: v.number(),
});
ここではよく使うであろうスキーマを紹介します。
もっと詳しく知りたい方は公式のリファレンスを参照してください。
v.number(); // number
v.string(); // string
v.boolean(); // boolean
v.null(); // null
v.undefined(); // undefined
v.any(); // any
v.unknown(); // unknown
v.literal(7); // 7
v.literal("hello"); // "hello"
v.literal(true); // true
v.array(v.string()); // string[]
v.tuple([v.string(), v.number()]); // [string, number]
v.object({ key: v.string() }); // { key: string }
v.nullable(v.string()); // string | null
v.optional(v.string()); // string | undefined
v.nullish(v.string()); // string | null | undefined
v.nonNullable(v.nullable(v.string())); // string
v.nonOptional(v.optional(v.string())); // string
v.nonNullish(v.nullish(v.string())); // string
v.union([v.string(), v.number()]); // string | number
// 判別可能なユニオン型 (discriminated union)
v.variant("type", [
v.object({ type: v.literal("a"), foo: v.string() }),
v.object({ type: v.literal("b"), bar: v.number() }),
]); // { type: 'a'; foo: string } | { type: 'b'; bar: number }
// enum
// objectを使うパターン
const Direction = {
Left: "LEFT",
Right: "RIGHT",
} as const;
v.enum(Direction); // "LEFT" | "RIGHT"
// 配列を使うパターン
const Direction = ["LEFT", "RIGHT"] as const;
v.picklist(Direction); // "LEFT" | "RIGHT"
v.pipe()
を使うことでさらに厳密にスキーマを定義することもできます。
// stringでemailの書式で@example.comで終わる
const Email = v.pipe(v.string(), v.email(), v.endsWith("@example.com"));
// numberで0以上100以下
const Volume = v.pipe(v.number(), v.minValue(0), v.maxValue(100));
これらを組み合わせて自由にスキーマを定義することができます。
import * as v from "valibot";
const Name = v.pipe(v.string(), v.minLength(2), v.maxLength(20));
const Age = v.pipe(v.number(), v.minValue(0), v.maxValue(130));
const User = v.object({
name: Name,
age: Age,
gender: v.picklist(["male", "female", "other"]),
comment: v.string(),
pet: v.optional(
v.variant("species", [
v.object({ species: v.literal("dog"), run: v.function() }),
v.object({ species: v.literal("cat"), sleep: v.function() }),
v.object({ species: v.literal("bird"), fly: v.function() }),
])
),
});
ちなみにスキーマの命名は PascalCase が推奨されています。
バリデーション
parse でバリデーションを行います。(本当はデータの整形も行いますがこの記事ではバリデーションについてのみ説明します)
parse には 2 種類あります。
Parse
v.parse(スキーマ, データ)
の形で使います。
バリデーションに失敗した際にエラーを throw します。
import * as v from "valibot";
// 成功時
try {
const EmailSchema = v.pipe(v.string(), v.email());
const email = v.parse(EmailSchema, "jane@example.com");
console.log(email);
} catch (error) {
console.log(error);
}
// jane@example.com
// 失敗時
try {
const EmailSchema = v.pipe(v.string(), v.email());
const email = v.parse(EmailSchema, "some text");
console.log(email);
} catch (error) {
console.log(error);
}
// ValiError: Invalid email: Received "some text"
Safe parse
v.safeParse(スキーマ, データ)
の形で使います。
バリデーション失敗時にエラーを throw せず、成功時も失敗時も .success
プロパティを持つオブジェクトを返します。
成功時は .success
に true
が入り、 .output
にバリデーション後のデータが入ります。
失敗時は .success
に false
が入り、 .issues
に問題の内容が入ります。
import * as v from "valibot";
// 成功時
const EmailSchema = v.pipe(v.string(), v.email());
const result = v.safeParse(EmailSchema, "jane@example.com");
if (result.success) {
// こっちに入る
const email = result.output;
console.log(email);
} else {
console.log(result.issues);
}
// jane@example.com
// 失敗時
const EmailSchema = v.pipe(v.string(), v.email());
const result = v.safeParse(EmailSchema, "some text");
if (result.success) {
const email = result.output;
console.log(email);
} else {
// こっちに入る
console.log(result.issues);
}
// [
// {
// kind: "validation",
// type: "email",
// input: "some text",
// expected: null,
// received: "\"some text\"",
// message: "Invalid email: Received \"some text\"",
// requirement: /^[\w+-]+(?:\.[\w+-]+)*@[\da-z]+(?:[.-][\da-z]+)*\.[a-z]{2,}$/iu,
// path: undefined,
// issues: undefined,
// lang: undefined,
// abortEarly: undefined,
// abortPipeEarly: undefined,
// }
// ]
TS の型生成
v.InferOutput<typeof スキーマ>
でスキーマから TypeScript の型を生成することができます。
import * as v from "valibot";
const User = v.object({
name: v.string(),
age: v.number(),
});
type User = v.InferOutput<typeof User>;
// type User = {
// name: string;
// age: number;
// }
const user: User = {
name: "John",
age: 20,
};
ちなみにスキーマ名と型名は同じにすることが推奨されています。
Valibot 使用例
DB にユーザーを保存してまた取り出すというシンプル(だけど無意味)な例を紹介します。
import * as v from "valibot";
export const User = v.object({
id: v.number(),
name: v.string(),
age: v.number(),
});
export type User = v.InferOutput<typeof User>;
import * as v from "valibot";
import { User } from "./model";
export const repository = {
save: async (user: User): Promise<void> => {
// dbに保存する処理
},
get: async (id: number): Promise<User | undefined> => {
const dbRes: any = {}; // dbから取得する処理
const safeParseRes = v.safeParse(User, dbRes);
if (safeParseRes.success) {
return safeParseRes.output;
} else {
console.error(safeParseRes.issues);
return;
}
},
};
import { User } from "./model";
import { repository } from "./repository";
async function main() {
const user: User = {
id: 1,
name: "John",
age: 20,
};
await repository.save(user);
const john = await repository.get(1);
if (john) {
console.log(`${john.name} is ${john.age} years old`);
}
}
main();
まとめ
この記事では、Valibot の基本的な使い方について説明しました。主なポイントは以下の通りです:
-
Valibot は TypeScript 用のスキーマバリデーションライブラリ
-
スキーマを定義することでデータ構造を表現し、バリデーションを行うことができる
-
主要なスキーマ定義のための関数(
v.string()
,v.number()
,v.object()
など) -
バリデーションには
v.parse()
とv.safeParse()
の 2 つの方法がある-
v.parse()
はバリデーション失敗時にエラーを throw する -
v.safeParse()
は成功/失敗の結果をオブジェクトで返す
-
-
定義したスキーマから TypeScript の型を生成できる(
v.InferOutput<typeof Schema>
)
今回紹介しきれていないこともたくさんあるので、valibot が気になった人はぜひ公式の Guides(特に Main concepts)を読んでみてください。
この記事が皆様のエンジニアライフのお役に立てば嬉しいです。
ありがとうございました。
Discussion