投稿日:2026/7/26
更新日:2026/7/26

z.array(x).min(1) では実行時にしか空配列を防げない。型レベルでも保証したいときに z.tuple([x], x) を使う、という話。
「宛先が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('宛先がありません'); // 😩 起きないはずの分岐をあちこちに書く
}
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()}`); // ✅ ガード不要
};
「起きないはずの分岐」を書かなくてよくなるのが利点。
外部から来たデータ(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> と同じ
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引数に同じスキーマを渡すのがポイント。
このリポジトリのように「型を手で書き、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 については 型指定について を参照)。
実際に空配列を渡すと、こうなる(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 つ以上指定してください',
});
}
使う価値があるケース
[0] や .length > 0 のガードを何箇所も書いている使わない方がいいケース
.min(1) で十分)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 チェックが消える