Closed26

Valibot 逆引きチートシート

ピン留めされたアイテム
uttkuttk

概要

「 Valibot であの処理書くにはどうすればいいんだっけ?」みたいなヤツをまとめたスクラップです。

uttkuttk

検証環境

valibot のバージョン
"dependencies": {
  "valibot": "0.31.0"
}
uttkuttk

作成したスキーマから型を生成する

検証した結果の型を生成したい時は v.InferOutput<T> を使う。

import * as v from 'valibot';

const Schema = v.object({
  email: v.string(),
  password: v.string(),
});

type SchemaType = v.InferOutput<typeof Schema>;
/*
  {
    email: string;
    password: string;
  }
*/
uttkuttk

エラー情報からメッセージのみを取り出す

検証に失敗した時に得られる issues からエラーメッセージのみを取り出すには v.flatten() を使用します。

import * as v from 'valibot';

const Schema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

const result = v.safeParse(Schema, {
  email: 'hoge',
  password: 12345,
});

if (!result.success) {
  const issues = v.flatten(result.issues);
  console.log(issues);
  /*
    {
      nested: {
        email: ["Invalid email: ..."],
        password: ["Invalid type: ..."],
      }
    }
  */
}

ちなみに v.flatten() を実行しない場合、result.issues の中身は以下のようになります。

コードを見る
result.issuesの中身
[
  {
    kind: 'validation',
    type: 'email',
    input: 'hoge',
    expected: null,
    received: '"hoge"',
    message: 'Invalid email: Received "hoge"',
    requirement: RegExp,
    path: [
      {
        type: 'object',
        origin: 'value',
        input: {
          email: 'hoge',
          password: 1111,
        },
        key: 'email',
        value: 'hoge',
      },
    ],
    issues: undefined,
    lang: undefined,
    abortEarly: undefined,
    abortPipeEarly: undefined,
  },
  {
    kind: 'schema',
    type: 'string',
    input: 1111,
    expected: 'string',
    received: '1111',
    message: 'Invalid type: Expected string but received 1111',
    requirement: undefined,
    path: [
      {
        type: 'object',
        origin: 'value',
        input: {
          email: 'hoge',
          password: 1111,
        },
        key: 'password',
        value: 1111,
      },
    ],
    issues: undefined,
    lang: undefined,
    abortEarly: undefined,
    abortPipeEarly: undefined,
  },
]
uttkuttk

検証に失敗した時のフォールバック値を設定する

検証に失敗した時、デフォルトではエラーオブジェクトが返されますが、エラーオブジェクトではなく任意の値を返して欲しい場合は v.fallback() を使用します。

import * as v from 'valibot';

const Schema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

const FallbackSchema = v.fallback(Schema, {
  email: 'hoge@example.com',
  password: 'fallback_password',
});

// フォールバックされるので検証に失敗しても throw されない
const result = v.parse(FallbackSchema, null)
console.log({ result });
/*
  {
    result: {
      email: "hoge@example.com",
      password: "test_password"
    }
  }
*/
uttkuttk

オブジェクトスキーマからキーを取り出してスキーマを作成する

オブジェクトスキーマからキー文字列を取り出してスキーマを作成するには v.keyof() を使用します。

import * as v from 'valibot';

const Schema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

const SchemaKeys = v.keyof(Schema)
// → v.PicklistSchema<["email", "password"], undefined>

type SchemaKeysType = v.InferOutput<typeof SchemaKeys>
// → "email" | "password"
uttkuttk

Internationalization ( 国際化対応 ) について

エラーメッセージを i18n (Internationalization) させたい時があります。
基本的なメッセージであれば公式が用意してくれている @valibot/i18n を使用するだけで対応できます。

'@valibot/i18n'の使い方
// 全ての言語を適用します (オススメしません)
import '@valibot/i18n';

// 特定の言語のみ適用します
import '@valibot/i18n/ja';

// スキーマのみ適用します
import '@valibot/i18n/ja/schema';

// 特定の関数のみに適用します
import '@valibot/i18n/ja/minLength';

言語の切り替え

表示する言語を指定する場合は v.setGlobalConfig() を使用するか v.parse() などで検証を実行する時に直接指定します。

import * as v from 'valibot';

// 実行後、全ての検証で日本語のエラーメッセージが適用されます
v.setGlobalConfig({ lang: 'ja' });

// この検証のみで日本語のエラーメッセージが適用されます
v.parse(Schema, input, { lang: 'ja' });
uttkuttk

@valibot/i18n では対応できない時

v.setGlobalMessage(), v.setSchemaMessage(), v.setSpecificMessage() を使用することで、個別にエラーメッセージを対応させることができます。

import * as v from 'valibot';

// デフォルトのエラーメッセージを設定します (これがフォールバックとして使用されます)
v.setGlobalMessage((issue) => `検証に失敗しました`, 'ja');

// スキーマのデフォルトのエラーメッセージを設定します
v.setSchemaMessage((issue) => `スキーマの検証に失敗しました`, 'ja');

// 特定の関数のデフォルトのエラーメッセージを設定します
v.setSpecificMessage(v.minLength, (issue) => `長さが不正です`, 'ja');

Paraglide JS と連携もできる

また、Paraglide JS と連携することも可能なようです。

公式ドキュメントより引用
import * as v from 'valibot';
import * as m from './paraglide/messages.js';

const LoginSchema = v.object({
  email: v.pipe(
    v.string(),
    v.nonEmpty(m.emailRequired),
    v.email(m.emailInvalid)
  ),
  password: v.pipe(
    v.string(),
    v.nonEmpty(m.passwordRequired),
    v.minLength(8, m.passwordInvalid)
  ),
});
uttkuttk

非同期な検証を実装する

非同期な検証をするには v.pipeAsync(), v.objectAsync() などのような非同期に対応するスキーマを作成し、v.parseAsync() などのような専用の関数で検証します。

import * as v from 'valibot';

const isUsernameAvailable = async (input: string): boolean => {
  const result = await fetch(`/api/check_username?username=${input}`)
  return result.isAvailable;
}

// 非同期なスキーマを定義
const Schema = v.objectAsync({
  username: v.pipeAsync(v.string(), v.checkAsync(isUsernameAvailable)),
  avatarUrl: v.pipe(v.string(), v.url()),
});

const input = { username: 'hoge', avatarUrl: 'https://example.com/avatar.png' }
const result = await v.parseAsync(Schema, input);
console.log({ result });
/*
  {
    result: {
      username: string;
      avatarUrl: string;
    }
  }
*/
uttkuttk

throw される値について

通常の検証処理では、検証に失敗するとエラーメッセージなどの情報を取得できますが、自前で用意した非同期な検証では throw した値がそのまま検証結果として返される点に注意してください。

自前で実装した非同期な検証でthrowしてしまった例
import * as v from 'valibot';

const isCheckName = async (input: string): Promise<boolean> => {
  throw new Error("hoge"); // わざと throw する
}

const Schema = v.objectAsync({
  name: v.pipeAsync(v.string(), v.checkAsync(isCheckName))
})

v.parseAsync(Schema, { name: "" }).catch(err => {
  console.error(err);  // `Error: hoge` ← isCheckName() で throw した値になってしまう
})

そのため、自前で非同期な検証処理を行う時はなるべく throw させないようにしましょう。

uttkuttk

入力値を別の値に変換する

入力値を別の値へ変換するには v.transform() を使用します。

import * as v from 'valibot';

const Schema = v.pipe(
  v.string(),
  v.transform((input) => input.length), // string → number へ変換する
  // 以降は number 型として検証できる
  v.minValue(5),
  v.maxValue(10)
);

// number 型になる
type SchemaType = v.InferOutput<typeof Schema>;

const result: number = v.parse(Schema, 'hello,world!')
console.log({ result }) // { result: 12 }
uttkuttk

スキーマ内のスキーマを取得する

v.object()v.array() などスキーマを内包するスキーマがありますが、そのスキーマから内包しているスキーマを取り出すことができます。

v.object() の場合

.entries プロパティから内包しているスキーマを取得できます。

import * as v from 'valibot';

const Schema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

// Schema から`email`のスキーマを取り出す
const EmailSchema = Schema.entries.email

v.array() の場合

.item プロパティから内包しているスキーマを取得できます。

import * as v from 'valibot';

const Schema = v.array( v.pipe(v.string(), v.email()))

// Schema から`email`のスキーマを取り出す
const EmailSchema = Schema.item

v.record() の場合

.key プロパティでキーのスキーマを、.value で値のスキーマを取得できます。

import * as v from 'valibot';

const Schema = v.record(v.string(), v.pipe(v.string(), v.email()))

const UsernameSchema = Schema.key; // キーのスキーマを取り出す
const EmailSchema = Schema.value; // 値のスキーマを取り出す
uttkuttk

検証処理をカスタマイズする

検証処理に独自の検証を追加したい時があります。その場合は、v.check() または v.custom() を使用することで目的を達成できます。

v.custom() を使う場合

v.custom() は独自の検証処理を持つスキーマを定義するときに使用します。

"${number}px"の文字列スキーマを定義する
import * as v from 'valibot';

const Schema = v.custom<`${number}px`>((input) => {
  return typeof input === 'string' ? /^\d+px$/.test(input) : false
});

type SchemaType = v.InferOutput<typeof Schema> // `${number}px`

v.check() を使う場合

v.check() は既にあるスキーマに対して個別の検証処理を使いする時に使用します。

"${number}px"の文字列スキーマを定義する
import * as v from 'valibot';

const Schema = v.pipe(
  v.string(),
  v.check<`${number}px`>((input) => /^\d+px$/.test(input))
);

type SchemaType = v.InferOutput<typeof Schema> // `${number}px`
uttkuttk

v.custom() と v.check() の違いについて

valibot では スキーマ ・メソッド・アクション という役割を持つ関数の組み合わせで成り立っています。


https://valibot.dev/guides/mental-model/ より引用

今回の場合、v.custom() は スキーマ、v.check() は アクション というそれぞれ違う役割を担っています。

スキーマは、文字列、オブジェクト、日付などの特定のデータ型を独立して検証します。そのため再利用性が高く、他のオブジェクトにネストさせたり、他のスキーマやアクションと合わせて別のスキーマを作るなど柔軟な検証ができます。

一方アクションの方は、スキーマの検証をさらに強化したり、検証の前処理などを行う役割があります。スキーマほど再利用性は高くありませんが、データ型に左右されない柔軟な処理を行うことが可能です。(例: v.transform()など)

違いまとめ表

名前 メリット デメリット
v.custom() 再利用性の高い検証を定義できる 検証するデータ型が必要
v.check() データ型に左右されない処理ができる 再利用性が高くない
uttkuttk

生成される型に情報を付加したい(幽霊型:Phantom Type)

TypeScript では構造が同じ値は同じ型として扱われます。スキーマから生成した型ももちろん構造が同じであれば v.parse() などの戻り値じゃなくても代入することができます。

import * as v from 'valibot';

const Schema = v.pipe(v.string(), v.maxLength(5));
type SchemaType = v.InferOutput<typeof Schema>; // string 型になる

// 問題なく代入できる
const validStr: SchemaType = v.parse(Schema, 'valid');

// v.parse() してないけど string なので問題なく代入できる
const invalidStr: SchemaType = 'invalid';

しかし、これでは SchemaType を使っているにもかかわらず検証していない値が入る可能性があります。

そこで、v.brand() を使うことで SchemaType を使っている変数には v.parse() などで検証した値しか入らないようにできます。

v.brand()を使って実装
import * as v from 'valibot';

// v.brand() を使って幽霊型を付与する
const Schema = v.pipe(v.string(), v.maxLength(5), v.brand('any'));

// string & v.Brand<"any">型になる
type SchemaType = v.InferOutput<typeof Schema>;

// 問題なく代入できる
const validStr: SchemaType = v.parse(Schema, "valid");

// 型エラーになり代入できなくなる
const invalidStr: SchemaType = "invalid";
//    ~~~~~~~~~~
//    └─ Type 'string' is not assignable to type 'string & Brand<"any">'.
uttkuttk

検証してない値を検証結果に含める

一番簡単な方法は v.is() を使って Type Guard してしまうことです。

Type Guardを使う例
import * as v from 'valibot';

const Schema = v.object({ key: v.string() })
const value = { key: "hello",  key2: "world" }

if(v.is(Schema, value)) {
  value; // 型では `{ key: string }` だが実際は `{ key: "hello",  key2: "world" }` となる
}

もしくは、v.looseObject()v.looseTuple()などでスキーマ定義時に含めるように指定します。

スキーマ定義時に未検証の値を含めるようにする例
import * as v from "valibot";

const Schema = v.looseObject({ key: v.string() });
const value = { key: "hello",  key2: "world" }

const result = v.parse(Schema, input)
console.log({ result }) // { result: { key: "hello",  key2: "world" } }
uttkuttk

スキーマからデフォルト値を取得する

v.optional()v.nullable() と言ったスキーマはデフォルト値を設定できますが、それらのスキーマからデフォルト値を取得するには v.getDefault() を使用します。

import * as v from 'valibot';

const Schema = v.optional(v.string(), "default value");
const defaultValue = v.getDefault(Schema);

console.log({ deafultvalue }); // { defaultValue: "default value" }

また、オブジェクトスキーマなどからデフォルト値を取得するには v.getDefaults() を使用します。

再帰的にデフォルト値を取得する
import * as v from 'valibot';

const Schema = v.object({
  key: v.optional(v.string(), "default value"),
  key2: v.string()
});
const defaultValue = v.getDefaults(Schema);

console.log({ defaultValue });
/*
  {
    defaultValue: {
      key: "default value",
      key2: undefeind, // デフォルト値が無いスキーマ undefined になることに注意!
    }
  }
*/
uttkuttk

スキーマを使って型ガード(Type Guards)する

検証を Type Guard として実行するには v.is() を使用します。

import * as v from 'valibot';

const Schema = v.pipe(v.string(), v.email());
const data: unknown = 'jane@example.com';

if (v.is(Schema, data)) {
  const email = data; // string
} else {
  const email = data; // unknown
}
uttkuttk

複数のオブジェクトのスキーマをマージする

.entries から各プロパティのスキーマを参照できるので、それを使ってマージする。

import * as v from 'valibot';

const Schema1 = v.object({ foo: v.string(), baz: v.number() });
const Schema2 = v.object({ bar: v.string(), baz: v.boolean() });

const MergedSchema = v.object({
  ...Schema1.entries,
  ...Schema2.entries,
});
// → { foo: string; bar: string; baz: boolean }
uttkuttk

オブジェクトのスキーマから特定のプロパティを除外する

v.omit() を使う。

import * as v from 'valibot';

const Schema = v.omit(
  v.object({
    key1: v.string(),
    key2: v.number(),
    key3: v.boolean(),
  }),
  ['key1', 'key3']
);

type SchemaType = v.InferOutput<typeof Schema>
// { key2: number }
uttkuttk

省略可能(Optional)なスキーマを必須(Required)にする

v.required() を使う。

import * as v from 'valibot';

const Schema = v.required(
  v.object({
    key1: v.optional(v.string()),
    key2: v.optional(v.number()),
  })
); 

type SchemaType = v.InferOutput<typeof Schema>
// { key1: string; key2: number }
uttkuttk

ラップされているスキーマを取り出す

v.nullable()v.optional() と言ったスキーマをラップするようなスキーマから内部のスキーマを取り出すには v.unwrap() または .wrapped プロパティを使用します。

import * as v from 'valibot';

const Schema = v.optional(v.string());

// `string | undefined` から `string` のスキーマを取り出す
const StringSchema = v.unwrap(Schema);

// または以下のようにも書けます
const WrappedSchema = Schema.wrapped;
uttkuttk

スキーマの入力値の型を生成する

v.transform() など入力値と出力値の型が違う時に、入力値の型を生成するには v.InferInput<T> を使用する。

import * as v from 'valibot';

const Schema = v.object({
  date: v.pipe(v.string(), v.transform(str => new Date(str)))
});

type SchemaType = v.InferInput<typeof Schema>
// { date: string; }
uttkuttk

デフォルトのエラーメッセージを設定する

v.parse()v.safeParse() の第三引数で設定できます。

import * as v from 'valibot';

const Schema = v.object({
  key: v.string('個別に設定しているエラーメッセージ'),
  key2: v.string(),
});

const input = {}
const result = v.safeParse(
  Schema,
  input,
  {
    message: 'これはデフォルトメッセージです', // ここでデフォルト値を設定できる
  },
);

if (!result.success) {
  console.log(v.flatten(result.issues));
  /*
    {
      nested: {
        key: [
          "個別に設定しているエラーメッセージ"
        ],
        key2: [
          "これはデフォルトメッセージです"
        ]
      }
    }
  */
}
uttkuttk

入力値や期待値を使ってエラーメッセージをカスタマイズする

例えば、入力された文字から残りの文字数を計算してエラーメッセージとして出すには以下のようにします。

import * as v from 'valibot';

const Schema = v.pipe(
  v.string(),
  v.maxLength(10, (issue) => {
    return `あと${issue.input.length - issue.requirement}文字短くして下さい`;
  }),
);

const result = v.safeParse(Schema, '1234567890ABC');

if (!result.success) {
  console.log(result.issues[0].message); // → "あと3文字短くして下さい"
}
このスクラップは2ヶ月前にクローズされました