TypeScript w/ Zod でリテラルな文字列の配列を型として扱う

Zod の Schema Validation でリテラルな文字列の数パターンのみを受け付けるようにする

z.enum() にリテラルな文字列の配列を渡すことでその配列のみを有効な入力になるように制限することができます。

(悪いね、今日はカツオ(bonito) は入っていないんだ)

import { z } from 'zod';

const menuAvailableToday = ['tuna', 'mackerel', 'eel', 'squid', 'flounder', 'yellowtail', 'amberjack'];
const orderAcceptable = z.object({
    order: z.enum(menuAvailableToday)
});

let yourOrder;

// This passes.
yourOrder = orderAcceptable.parse({ order: 'tuna' });
console.log(`You ordered ${yourOrder.order}`);

// This fails.
// yourOrder = orderAcceptable.parse({ order: 'bonito' });
// console.log(`You ordered ${yourOrder.order}`);

固定化された数パターンの画像ファイル名のみを受け付けるようにする

ここでは、期待される特定のファイル名(サイズ + 拡張子)のパターンを生成しています。 TypeScript の標準ライブラリには順列組み合わせの関数が用意されていないようなので、array-combination/src/combination.ts at main · idw111/array-combination · GitHub を使用しています。

  • 1040.png, 700.png, 460.png, 300.png, 200.png
  • 1040.jpg, 700.jpg, 460.jpg, 300.jpg, 200.jpg
  • 1040.jpeg, 700.jpeg, 460.jpeg, 300.jpeg, 200.jpeg

z.parse() ではなく z.safeParse() だと型チェックに失敗した時のエラーに応じて処理が可能です。

handle_array_of_literal_string_as_type.ts

import { z } from 'zod';

// https://github.com/idw111/array-combination/blob/main/src/combination.ts
const combination = <T extends any>(...arrays: T[][]): T[][] => {
    if (arrays.length === 1) return arrays[0].map((item) => [item]);
    const [firstArray, ...restArrays] = arrays;
    return combination(...restArrays)
        .map((result) => firstArray.map((item) => [item, ...result]))
        .flat();
};

const permutations: string[][] = combination(['1040', '700', '460', '300', '200'], ['png', 'jpg', 'jpeg']);

// FIXME: `candidates: string[]` fails with `the error TS2769: No overload matches this call.`
const candidates: any = permutations.map(element => element[0].concat('.', element[1]));
const imageFile = z.object({
    fileName: z.enum(candidates)
});

const imf1 = imageFile.parse({ fileName: '1040.png' });
console.log(imf1)

// const imf2 = imageFile.parse({ fileName: '1041.png' });
// console.log(imf2)

ファイル名が所定の組み合わせ以外の場合は ZodError で弾かれます。

➜ node_modules/.bin/tsc handle_array_of_literal_string_as_type.ts --target es2016 --module nodenext --moduleResolution nodenext
➜ node --import tsx handle_array_of_literal_string_as_type.ts
{ fileName: '1040.png' }

/Users/kyagi/lab/skeleton-zod-001/node_modules/zod/lib/types.js:43
                const error = new ZodError_1.ZodError(ctx.common.issues);
                              ^

ZodError: [
  {
    "received": "1041.png",
    "code": "invalid_enum_value",
    "options": [
      "1040.png",
      "700.png",
      "460.png",
      "300.png",
      "200.png",
      "1040.jpg",
      "700.jpg",
      "460.jpg",
      "300.jpg",
      "200.jpg",
      "1040.jpeg",
      "700.jpeg",
      "460.jpeg",
      "300.jpeg",
      "200.jpeg"
    ],
    "path": [
      "fileName"
    ],
    "message": "Invalid enum value. Expected '1040.png' | '700.png' | '460.png' | '300.png' | '200.png' | '1040.jpg' | '700.jpg' | '460.jpg' | '300.jpg' | '200.jpg' | '1040.jpeg' | '700.jpeg' | '460.jpeg' | '300.jpeg' | '200.jpeg', received '1041.png'"
  }
]
(... snip ...)

Jest でのテスト例

safeParse() の返り値とデータの両方をチェックするテストの例です。

handle_array_of_literal_string_as_type.test.js

import { imageFile } from "./handle_array_of_literal_string_as_type";

describe('1040.png is a valid file name', () => {
    const result = imageFile.safeParse({
        fileName: "1040.png"
    });
    test('fileName: 1040.png is OK', () => {
        expect(result.success).toBe(true);
    });
    test('fileName: 1040.png is OK', () => {
        expect(result.data.fileName).toBe("1040.png");
    });
});

describe('1041.png is NOT a valid file name', () => {
    const result = imageFile.safeParse({
        fileName: "1041.png"
    });
    test('fileName: 1041.png is NG', () => {
        expect(result.success).toBe(false);
    });
});
➜ node_modules/.bin/jest handle_array_of_literal_string_as_type.test.js
 PASS  ./handle_array_of_literal_string_as_type.test.js
  1040.png is a valid file name
    ✓ fileName: 1040.png is OK (2 ms)
    ✓ fileName: 1040.png is OK (4 ms)
  1041.png is NOT a valid file name
    ✓ fileName: 1041.png is NG (1 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.698 s, estimated 1 s
Ran all test suites matching /handle_array_of_literal_string_as_type.test.js/i.

参考情報

zod.dev

github.com