YASD-TECH
YASD TECH
# TypeScript

Zodで1件以上の配列を型として保証する

投稿日:2026/7/26

更新日:2026/7/26

ttitleImage

Zod で「1件以上の配列」を型として保証する

z.array(x).min(1) では実行時にしか空配列を防げない。型レベルでも保証したいときに z.tuple([x], x) を使う、という話。


1. 何が問題か

「宛先が1人以上必要なメール送信」を考える。

type Mail = {
  to: string[];       // ← 空配列を型が許してしまう
  subject: string;
};

const send = (mail: Mail) => {
  const first = mail.to[0];
  //    ^? string | undefined   ← 空かもしれないので undefined が混ざる
  console.log(`代表宛先: ${first.toUpperCase()}`); // ❌ first が undefined かもしれない
};

to が空でないことは「決まりごと」なのに、型が知らないので使う側が毎回 undefined を気にする羽目になる。

const first = mail.to[0];
if (first === undefined) {
  throw new Error('宛先がありません'); // 😩 起きないはずの分岐をあちこちに書く
}

2. NonEmptyArray

TypeScript では「先頭 + 残り」のタプルで「1件以上」を表現できる。

type NonEmptyArray<T> = [T, ...T[]];
//                       ↑    ↑
//                       │    └ 2個目以降(0個でもよい)
//                       └ 1個目(必ずある)
const a: NonEmptyArray<string> = ['x'];        // ✅
const b: NonEmptyArray<string> = ['x', 'y'];   // ✅
const c: NonEmptyArray<string> = [];           // ❌ エラー

これを使うと、先ほどの undefined チェックが消える。

type Mail = {
  to: NonEmptyArray<string>;
  subject: string;
};

const send = (mail: Mail) => {
  const first = mail.to[0];
  //    ^? string   ← undefined が付かない
  console.log(`代表宛先: ${first.toUpperCase()}`); // ✅ ガード不要
};

「起きないはずの分岐」を書かなくてよくなるのが利点。

3. Zod でこの型を作る

外部から来たデータ(JSON、Excel など)を検証するときは Zod を使うが、NonEmptyArray を出力する書き方は限られる。

z.array(x).min(1) — 型は変わらない

const schema = z.array(z.string()).min(1);
type Out = z.infer<typeof schema>;
//   ^? string[]   ← 空を許す型のまま

実行時には空配列を弾いてくれるが、型は string[]NonEmptyArray<string> として扱えない。

z.array(x).nonempty() — v4 では効かない

const schema = z.array(z.string()).nonempty();
type Out = z.infer<typeof schema>;
//   ^? string[]   ← v4 ではこれも string[]

バージョン差に注意。 Zod v3 では .nonempty()[T, ...T[]] を返していたが、v4 (4.3.6 で確認) では this を返すようになり、型は string[] のまま。v3 の記事を読んでいるとハマる。

z.tuple([x], x) — これだけが [T, ...T[]] になる

const schema = z.tuple([z.string()], z.string());
type Out = z.infer<typeof schema>;
//   ^? [string, ...string[]]   ← NonEmptyArray<string> と同じ

4. z.tuple の読み方

z.tuple(要素の配列, rest?) の第2引数が「残りの要素の型」になる。

書き方 表す型 意味
z.tuple([z.string(), z.number()]) [string, number] ちょうど2個
z.tuple([z.string()]) [string] ちょうど1個
**z.tuple([z.string()], z.string()) [string, ...string[]] 1個以上**
z.tuple([z.string(), z.number()], z.boolean()) [string, number, ...boolean[]] 先頭2個は固定、以降は boolean

z.tuple([x], x) は「1個目は x、残りも x」=「x が1個以上」と読む。第1引数と第2引数に同じスキーマを渡すのがポイント。

5. 型定義と組み合わせる

このリポジトリのように「型を手で書き、Zod スキーマがそれを満たすことを検証する」構成なら、こうなる。

type NonEmptyArray<T> = [T, ...T[]];

const nonEmptyStringArray = z.tuple([z.string()], z.string())
  satisfies z.ZodType<NonEmptyArray<string>>;  // ✅ 型が一致するので通る

z.array(z.string()).min(1) に差し替えるとここでコンパイルエラーになる。型とスキーマのズレを機械的に検出できるsatisfies については 型指定について を参照)。

6. 注意点:エラーメッセージが分かりにくい

実際に空配列を渡すと、こうなる(Zod 4.3.6 で実行)。

z.tuple([z.string()], z.string()).safeParse([]);
[{
  "expected": "string",
  "code": "invalid_type",
  "path": [0],
  "message": "Invalid input: expected string, received undefined"
}]

「1件以上必要です」とは言ってくれない。 「0番目に string が必要だが undefined だった」という、内部構造がそのまま出たメッセージになる。

z.array(x).min(1) なら「Too small: expected array to have >=1 items」のように意図の通ったメッセージが出るので、エラーメッセージの分かりやすさとのトレードオフになる。

対処

利用者に見せるメッセージは自前で用意する。このリポジトリの一括取込でも、Zod のメッセージをそのまま画面に出さず、行番号と列名を添えた独自メッセージを組み立てている。

// 悪い例:Zod のメッセージをそのまま見せる
// → 「0番目に string が必要」と言われても利用者には意味が分からない

// 良い例:意図が伝わるメッセージを自分で作る
if (managementGroupIds.length === 0) {
  errors.push({
    rowNumber: 5,
    columnLabel: 'ユーザー種別',
    message: '管理者は「全体管理者」にするか、管理するグループを 1 つ以上指定してください',
  });
}

7. 使いどころの判断

使う価値があるケース

  • その配列が空でないことがドメイン上の決まりごと(例:グループ管理者は管理グループを1つ以上持つ)
  • 使う側が [0].length > 0 のガードを何箇所も書いている
  • 「完全コンストラクタ」で組み立て、以降は検証済みとして扱いたい

使わない方がいいケース

  • Zod のエラーメッセージをそのまま利用者に見せる設計
  • 空配列が普通に起こりうる(未所属・未選択など)
  • 単に「入力必須」を表したいだけ(.min(1) で十分)

8. 読みにくさへの対処

z.tuple([id], id) を初見で「1件以上の配列」と読むのは難しい。名前とコメントで補う。

/**
 * 1 件以上の ID 配列。
 * z.array().min(1) では型が string[] のままなので、rest 付きタプルで [T, ...T[]] を作る。
 */
const nonEmptyIdArray = z.tuple([id], id) satisfies z.ZodType<NonEmptyArray<string>>;

まとめ

やりたいこと 書き方
実行時に空を弾くだけでよい z.array(x).min(1)
型でも1件以上を保証したい z.tuple([x], x)
v3 の記事にある .nonempty() v4 では型が変わらない。使わない
  • NonEmptyArray<T> = [T, ...T[]] は「先頭 + 残り」で1件以上を表すタプル
  • 型で保証すると、使う側の undefined チェックが消える
  • 代償はエラーメッセージの分かりにくさ。利用者向けメッセージは自前で用意する

Index

  • Zod で「1件以上の配列」を型として保証する
  • 1. 何が問題か
  • 2. NonEmptyArray 型
  • 3. Zod でこの型を作る
  • ❌ z.array(x).min(1) — 型は変わらない
  • ❌ z.array(x).nonempty() — v4 では効かない
  • ✅ z.tuple([x], x) — これだけが [T, ...T[]] になる
  • 4. z.tuple の読み方
  • 5. 型定義と組み合わせる
  • 6. 注意点:エラーメッセージが分かりにくい
  • 対処
  • 7. 使いどころの判断
  • 8. 読みにくさへの対処
  • まとめ