🤖

【TS】ユーザー定義型ガードの代わりにValibotを使う

2024/12/25に公開

はじめに

Specteeでエンジニアをしている永野です。

TypeScript での開発において、DB や Web API から取得した値が any 型になっていることがよくありますが、any をそのまま使ったり as で型アサーションをすることが危険であることはよく知られています。
そして、それらの危険性を回避するためにユーザー定義型ガードを使用することがあるかと思います。

しかし、ユーザー定義型ガードは危険性の存在する範囲を狭めることは出来ますが、完全に排除することは出来ません。
そこで、今回はユーザー定義型ガードの代わりに Valibot を使用する方法を紹介します。

ユーザー定義型ガードの危険性

ユーザー定義型ガードでは、判定ロジックに間違いがあると不適切な型が付与されてしまう可能性があります。
例えば以下のようなコードです。

function isUser(value: any): value is { name: string; age: number } {
  // 間違った判定ロジック
  return typeof value.name === 'string';
}

このコードは value.namestring であることしか検証していないため、 value.agenumber であることを保証していません。
しかし、value.namestring であれば引数は { name: string; age: number } であると判定されてしまいます。

そのため、以下のようなコードを書いた場合、コンパイルエラーは発生しませんが実行時エラーが発生します。

const user: unknown = { name: 'Taro' };

function isUser(value: any): value is { name: string; age: number } {
  // 間違った判定ロジック
  return typeof value.name === 'string';
}

if (isUser(user)) {
    console.log(user.age.toString());
  // 実行時エラー
  // TypeError: Cannot read properties of undefined (reading 'toString')
}

このように、ユーザー定義型ガードは間違ったロジックを実装してしまう可能性があるため、テストを用意するなどして対策を行う必要があります。

また、上記の例では省略していますが、実際にはisUserの引数が nullundefined の場合に isUser 内でエラーが発生しないための対策も必要であり、必要以上にコードが複雑になります。

そこで、これら問題を解決するために Valibot の使用を提案します。

Valibot とは

Valibotは TypeScript 向けのオープンソースのスキーマライブラリで、値のバリデーションと型付けを行うことができます。

https://valibot.dev/

詳細な使い方は公式のガイドや別の技術ブログ等に任せて、ここでは簡単な使い方を紹介します。

基本的な流れは以下の通りです。

  1. バリデーションのルールとなるスキーマを定義する
  2. 値をバリデーションし、成功した場合は型付きの値が返される

簡単な例を以下に示します。

import * as v from 'valibot';

// 型が分からない値
const unknownValue: unknown = 1;

// スキーマ定義
// number 型であるかを検証するスキーマ
const Age = v.number();

// バリデーション
// unknownValue が Age を満たす場合、unknownValueの値が Age で定義した型として返される
const age = v.parse(Age, unknownValue);
// age: number

console.log(age.toString());
// => 1

まず、スキーマの定義をします。
今回の例ではnumber型であるかという単純なものとなっていますが、オブジェクトや配列、enumやその組み合わせなど複雑な型にも対応しています。

そして、parse関数を使用してバリデーションを行います。
成功した場合は型付きの値が返され、失敗した場合は実行時エラーが発生します。

以降はparseから返された型付きの値を使って処理を書いていくことができます。

ここまでの説明で、ユーザー定義型ガードで行っていた値の検証と型の付与を Valibot で行うことができることが分かると思います。
さらに、 Valibot が検証と型の付与を行うため、ユーザー定義型ガードで発生しうるロジックの間違いやコードの複雑性の問題を回避することができます。

コードの比較

ユーザー定義型ガードと Valibot を使用したコードを比較してみましょう。

まずはユーザー定義型ガードを使用したコードです。

ユーザー定義型ガード
const user: unknown = { name: 'Taro', age: 20 };

function isNotNullish(value: unknown): value is Record<string, unknown> {
  return value != null;
}

function isUser(value: unknown): value is { name: string; age: number } {
  if (!isNotNullish(value)) {
    return false;
  }
  return typeof value.name === 'string' && typeof value.age === 'number';
}

if (isUser(user)) {
  console.log(user.age.toString());
}

次に Valibot を使用したコードです。

Valibot
import * as v from 'valibot';

const unknownValue: unknown = { name: 'Taro', age: 20 };

const User = v.object({
  name: v.string(),
  age: v.number(),
});

const user = v.parse(User, unknownValue);

console.log(user.age.toString());

Valibot を使った方がコードの可読性も向上したように思います。
(もちろんユーザー定義型ガード側はisNotNullish 関数を共通化してimportしてくるようにできたり、Valibot側はエラーハンドリングをしないといけなかったりでこのサンプルだけを見て判断できるものではないですが)

まとめ

Valibot を使うことでユーザー定義型ガードの問題点を解決できるだけでなく、コードの可読性も向上させることができます。
今回は Valibot を紹介しましたが、似たライブラリはいくつかあるので自分に合ったものを選んで使ってみてください。

ユーザー定義型ガードについて調べた際、ライブラリを使いましょうという記事があまり無かったので今回このような記事を書いてみました。
少しでも参考になれば幸いです。

ここまで読んでいただきありがとうございました!

Spectee Developers Blog

Discussion