🛸

Zod の1500倍高速に動作する Typia を使う

2023/05/04に公開

はじめに

Typescript のバリデーションはいくつかライブラリがありますが、最近人気があるものは Zod ではないでしょうか。

私もよく使っていますが、先日 Typia というライブラリを知り、実際に使ってみました。Typia の特徴はわかりやすい記述と速度です。Zod と比較しながらみていきます。

https://typia.io/

記述の比較

インストールや細かい使い方は後述するとして、まずは Zod と Typia のバリデーションの記述を比較してみましょう。

// Zod
import { z } from "zod";

const User = z.object({
  username: z.string(),
});

User.parse({ username: "Ludwig" });

// 推論された型を取り出すこともできる
type IUser = z.infer<typeof User>;
// { username: string }
// Typia
import typia from "typia";

interface IUser {
  username: string;
}

typia.assert<IUser>({ username: "Ludwig" });

Zod は Zod オブジェクトを作成し、それを使ってバリデーションを行います。一方 Typia は interface をそのまま使用してバリデーションを行っています。Zod は z.infer() で型を取り出すことができますが、 Typia はその必要がありません。

バリデーションのオプション

Zod ではメールアドレスや UUID などのバリデーションを validator.js を使って行うことができます。 Typia はコメントタグを使用してこれらのバリデーションを行います。

// Zod
z.string().max(5);
z.string().min(5);
z.string().length(5);
z.string().email();
z.string().url();
z.string().emoji();
z.string().uuid();
z.string().cuid();
z.string().cuid2();
z.string().ulid();
z.string().regex(regex);
z.string().includes(string);
z.string().startsWith(string);
z.string().endsWith(string);
z.string().datetime(); // defaults to UTC, see below for options
z.string().ip(); // defaults to IPv4 and IPv6, see below for options
// Typia
export const checkCommentTag = typia.createIs<CommentTag>();
 
interface CommentTag {
    /**
     * @type int
     */
    type: number;
 
    /**
     * @exclusiveMinimum 19
     * @maximum 100
     */
    number?: number;
 
    /**
     * @minLength 3
     */
    string: string;
 
    /**
     * @pattern ^[a-z]+$
     */
    pattern: string;
 
    /**
     * @format date-time
     */
    format: string | null;
 
    /**
     * In the Array case, possible to restrict its elements.
     *
     * @minItems 3
     * @maxItems 100
     * @format uuid
     */
    array: string[];
}

Zod は validator.js を使用しているので豊富に種類がありますが、 Typia のサポートしているタグ少なく以下の種類があります。必要最低限が実装されており、必要があれば自身でカスタムのタグを作成すればよいとのことです。 (Customization)

  • number
    • @type {"int"|"uint"}
    • @minimum {number}
    • @maximum {number}
    • @exclusiveMinimum {number}
    • @exclusiveMaximum {number}
    • @multipleOf {number}
  • string
    • @minLength {number}
    • @maxLength {number}
    • @pattern {regex}
    • @format {keyword}
      • email
      • uuid
      • ipv4
      • ipv6
      • date: YYYY-MM-DD
      • date-time: Date.toISOString()
  • array
    • @minItems {number}
    • @maxItems {number}

検証方法

Zod に safeParse() があるように、 Typia は is()assert()validate() メソッド等を用意しており、エラーを throw したいかどうかなどで使い分けることができます。

// Zod
User.safeParse({ username: "Ludwig" });
// Typia
const matched: boolean = typia.is<IUser>({
  username: "Ludwig",
});
console.log(matched); // true

const res: typia.IValidation<IUser> = typia.validate<IUser>({
  username: 5,
});
if (!res.success) console.log(res.errors);

JSON文字列のパース

Typia には JSON文字列のパースも parse() メソッド等が用意されており、 JSON.parse() を挟まずバリデーションができます。

// Zod
User.parse(JSON.parse("{ username: \"Ludwig\" }");
// Typia
const parsed: IUser = typia.assertParse<IUser>("{ username: \"Ludwig\" }");

また Typia にはstringify() も用意されています。 JSON.stringify() で十分かと思いましたが、ネイティブの stringify より高速に動作するようです。(stringify() functions)

As typia.stringify<T>() function writes dedicated JSON serialization code only for the target type T, its performance is much faster than native JSON.stringify() function. However, because of the dedicated optimal JSON serialization code, when wrong typed data comes, unexpected error be occured.

typia.stringify<T>()関数は、対象のT型に対してのみ専用のJSONシリアライズコードを記述するため、ネイティブのJSON.stringify()関数よりもはるかに高速に動作します。しかし、専用の最適なJSONシリアライズコードを記述するため、間違った型のデータが来た場合、予期せぬエラーが発生することがあります。

値の変換

Zod にはバリデーションをかけた値を別の値に変換する transform() というメソッドがありますが、 Typia には値を変換するような機能は提供されていません。

速度の比較

Typia は Super-fast and super-safe. を売りにしているようで、ほかライブラリとのベンチマークもソースコード含めて公開しています。公開されているコードを使用して手元でベンチマークを実行してみました。Zod は safeParse()、 Typia は is() メソッドを使用しています。

なんと、object(simple)では Zod の 1500 倍以上のスコアが出ています。また複雑なユニオン型であっても Typia は検証が可能ということです。

stringify についてもネイティブと比較したベンチマークを測定しました。たしかにobject(simple)で、Typia が6倍ほど高速に動作しているようです。

Typia の導入

Zod との比較を説明してきましたが、次に、 Typia の使い方が2パターンあるのでそれぞれ導入方法を確認します。

Typia には Transformation モードと Generation モードがあります。Transformation モードは TypeScript のコンパイルを tsc で行う場合に使えて、 swc などそれ以外の方法でコンパイルを行う場合は、 Generation モードを使うことになります。

Transformation モード

Transformation モード は一番簡単にバリデーションを作成することができる AOT (Ahead of Time)のコンパイルモードです。以下のコマンドを実行することで、Typiaを導入することができます。npx typia setup で必要なライブラリのインストールと npm scripts の設定が自動で行われます。

% yarn add typia
% npx typia setup
----------------------------------------
 Typia Setup Wizard
----------------------------------------
? Package Manager yarn // 使用しているパッケージマネージャーを選択する

yarn add -D ts-patch@latest
yarn add -D ts-node@latest
yarn add -D typescript@4.9.5
npm run prepare

下記のファイルを tsc コマンドでコンパイルすると、出力された JavaScript ファイルへ IMember 型に最適なバリデーションコードが記述されます。

examples/src/check.ts
import typia from "typia";
 
export const check = typia.createIs<IMember>();
 
interface IMember {
    /**
     * @format uuid
     */
    id: string;
    
    /**
     * @format email
     */
    email: string;
 
    /**
     * @exclusiveMinimum 19
     * @maximum 100
     */
    age: number;
}
examples/bin/check.js
import typia from "typia";

export const check = (input) => {
  const $is_uuid = typia.createIs.is_uuid;
  const $is_email = typia.createIs.is_email;
  return "object" === typeof input &&
      null !== input &&
      (
          "string" === typeof input.id && $is_uuid(input.id) &&
	  ("string" === typeof input.email && $is_email(input.email)) && 
	  ("number" === typeof input.age && 19 < input.age && 100 >= input.age;)
      );
};

上記の例では、 createIs<T>() を使用していますが、これは再利用可能な is<T>() の関数ジェネレータです。同じ型に対して is<T>() 関数を繰り返し呼び出すと、 AOT コンパイルが重複するため、 JavaScript のファイルサイズが大きくなってしまいますが、 createIs<T>() 関数を使用することで回避することが可能です。

Vite で使う場合は、 vite.config.ts を下記のように設定することで Transformation モードで動作するようですが私の手元では動作確認ができませんでした。

追記(2023/12/28)

vite.config.ts を下記のように設定することで Vite でも動作することを確認しました。

vite.config.ts
import { defineConfig } from 'vite';
import typescript from 'rollup-plugin-typescript2';
import tspCompiler from 'ts-patch/compiler';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    {
      ...typescript({
        typescript: tspCompiler,
      }),
      enforce: 'post',
    },
  ],
  esbuild: false,
});

Generation モード

swc など、非推奨の TypeScript コンパイラを使っている場合は Transformation モードが使えないため Generation モードを使うことになります。

Generation モードは少し面倒なのですが、 generate コマンドを実行することで、 --input のTypeScript コードを読み込み,--output ディレクトリに変換された TypeScript コードが書き込まれます。

npm install --save typia
 
# GENERATE TRANSFORMED TYPESCRIPT CODES
npx typia generate \
    --input src/templates \
    --output src/generated \
    --project tsconfig.json
examples/src/templates/check.ts
import typia from "typia";
 
import { IMember } from "../structures/IMember";
 
export const check = typia.createIs<IMember>();
examples/src/generated/check.ts
import typia from "typia";
import { IMember } from "../structures/IMember";
export const check = (input: any): input is IMember => {
    const $is_uuid = (typia.createIs as any).is_uuid;
    const $is_email = (typia.createIs as any).is_email;
    return "object" === typeof input && 
        null !== input && 
        (
            "string" === typeof input.id && is_uuid(input.id) && 
            ("string" === typeof input.email && $is_email(input.email)) && 
            ("number" === typeof input.age && 19 <= input.age && 100 >= input.age)
        );
};

実際に使うとなると、 npm run devnpm run build の前に実行しておく必要があります。また、バリデーションを使用するとき、生成後のファイルをインポートして使うことになります。

examples/src/hoge.ts
// 生成された TypeScript ファイルからインポートする
import {check} from "../generated/check";

console.log(
  check({
    id: 'aa1ff915-3907-d321-d724-cf5b2eb3057d',
    email: 'test@example.com',
    age: 20,
  })
);

私が確認した限り、 --input と --output に渡すファイルの指定にワイルドカードの指定はできないため、現状ではどこか一箇所にバリデーションを記述し、どこか一箇所に生成ファイルを出力する必要があります。ここは使い勝手が悪いなと感じた点です。

その他の利用方法

Typia は NestJS 用に Nestia というヘルパーライブラリセットを用意しており、下記の機能をサポートしています。速度比較で検証されている通り、 class-validator より 10,000倍以上速く動作します。

Nestia is a set of helper libraries for NestJS, supporting below features:

  • @nestia/core: superfast decorators using typia
  • @nestia/sdk: evolved SDK and Swagger generators
  • @nestia/migrate: Swagger to NestJS
  • nestia: just CLI (command line interface) tool

ほかにも、PrismatRPC での使用例も公開されています。

まとめ

私自身 Next.js を使うことが多く、 Generation モードを使わざるを得ないのですが、その場合はバリデーションファイルをどのディレクトリに配置するかなど、考える必要があるのが懸念点です。
しかし、速度が速いことと、バリデーションライブラリ独自のオブジェクトを作成せずに interface を用意するだけで済むというのはとても魅力的です。また NestJS などサーバーサイドでは威力を発揮できるのではないかと思います。

参考

Discussion