値をzodのスキーマで検証するAssertion Functionを定義する


zodで定義したスキーマとマッチしているか検証するAssertion Functionを定義する方法について、書きます。

zodとは?

Zod is a TypeScript-first schema declaration and validation library.
https://www.npmjs.com/package/zod

「TypeScriptファーストなスキーマ定義、Validationライブラリです」とのことです。この記事では詳しくは説明しませんので、知らないという方はzodのREADMEやzodについて紹介している記事を見るとよいかもです。

Assertion Functionsとは?

TypeScript 3.7で追加された機能です。

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions

ユーザー定義のtype guardsと似ていますが、値を検証して型に合っていないと判断した場合にbooleanを返す代わりにErrorをthrowします。

※厳密に言うとこの説明は間違っている気がする。アサーションにtype guardの機能を持たせるためのものという感じ。

// ユーザー定義のtype guards
// 値の検証結果をbooleanで返す
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

// Assertion Functions
function assertString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('…');
  }
}

// Node.jsのassertモジュールも
// Assertion Functionsの恩恵を受けれるようになっていた気がする。
import assert from "assert";
const value: unknown = 'hoge';
assert(typeof value === 'string');
value; // string

詳しくは、色んな人が既に記事を書いているので、そちらを読んでください。以下のuhyo氏の記事などかなり詳細に書かれていてよいと思います。

TypeScript 3.7のasserts x is T型はどのように危険なのか - Qiita

値をzodのスキーマで検証するAssertion Function

は、以下です。

asserts.ts
import { z, ZodType } from "zod";

export function assertZodSchema<T extends ZodType<any, any, any>>(
  schema T,
  value: unknown
): asserts value is z.infer<T> {
  schema.parse(value);
}

以下のように使います。

import { z } from "zod";
import { assertZodType } from "./asserts";

const hogeSchema = z.string();

const value: unknown = 'hoge';
assertZodSchema(hogeSchema, value);
value; // string

ちなみに、他言語のアサーションは設定でオフにできることが多いと思いますが(ex: Java, PHPなど)、ECMAScriptやNode.jsにそういう機能はありません。以下のように自前で実装するか、unassertを使ってアサーションを取り除く必要があります。

asserts.ts
export function assertZodSchema<T extends ZodType<any, any, any>>(
  zodType T,
  value: unknown
): asserts value is z.infer<T> {
  if (process.env.NODE_ENV !== 'production') {
    zodType.parse(value);
  }
}

よいところ: 「assertsの危険性」を回避できる

上記で紹介したuhyo氏の記事にassertsの危険性について、以下のように記述されています。

いま紹介した便利なasserts型述語なのですが、実は迂闊に使うと危険なのです。危険というのはどういうことかというと、せっかくTypeScriptが保証してくれている安全性を破壊してしまうという意味です。
https://qiita.com/uhyo/items/b8d2ea6fbf6214fc4194

以下の例は極端ですが、ユーザー定義のAssertion Functionは検証ロジックにバグがあると型の安全性は保証されません。

function assertString(value: unknown): asserts value is string {
  // 何も…
  // 検証していないのである!!!
}

const value: unknown = 1000000;
assertString(value);
value; // 実態はnumberだが、TSの静的解析上はstringになり、実行時のどこかで死ぬかも

assertZodSchemaは、zodのスキーマから検証後に解決される型を決めているので、zodにバグが無い限りは検証ロジックと型が乖離しません。

おわり

実務では、Lambdaのハンドラーの引数の型や設定ファイルから読みだしたデータの型などの検証にAssertion Functionを使っています。type guardとの使い分けですが、個人的には「type guardで書くと検証失敗したときの処理を書かないといけないが本番ではそこには絶対に入らない、そして、俺はasでキャストしたくない人間なんだ〜」というときにAssertion Functionを選択していることが多いかもしれないです。