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

export type NonEmptyArray<T> = [T, ...T[]];
「要素が1つ以上あることを型レベルで保証した配列」を表す型。
TypeScript の T[] は「0個以上」を意味するため、空配列も許容される。その結果 arr[0] は T と推論されるのに、実行時には undefined が返ってくるという典型的な事故が起きる。NonEmptyArray<T> はこのズレをコンパイル時に潰すためのイディオム。
これはタプル型 + Rest 要素(可変長タプル)の組み合わせ。
| 部分 | 意味 |
|---|---|
[T, ...] |
先頭に必ず T が1つ存在する(タプルの固定要素) |
...T[] |
2番目以降は T が0個以上並ぶ(Rest 要素) |
つまり「1個 + 0個以上」= 最低1個を表現している。
type NonEmptyArray<T> = [T, ...T[]];
const a: NonEmptyArray<number> = [1]; // OK
const b: NonEmptyArray<number> = [1, 2, 3]; // OK
const c: NonEmptyArray<number> = []; // エラー: Source has 0 element(s) but target requires 1
NonEmptyArray<T> は T[] のサブタイプなので、T[] を受け取る関数にはそのまま渡せる。逆(T[] → NonEmptyArray<T>)は代入不可。
declare function sum(xs: number[]): number;
const ne: NonEmptyArray<number> = [1, 2];
sum(ne); // OK(配列として振る舞う)
const plain: number[] = [1, 2];
const bad: NonEmptyArray<number> = plain; // エラー
最大の利点。tsconfig.json で noUncheckedIndexedAccess: true を有効にしている場合、差が明確に出る。
function firstOfArray<T>(xs: T[]): T {
return xs[0]; // エラー: Type 'T | undefined' is not assignable to type 'T'
}
function firstOfNonEmpty<T>(xs: NonEmptyArray<T>): T {
return xs[0]; // OK。インデックス 0 はタプルの固定要素なので undefined にならない
}
noUncheckedIndexedAccess が無効でも、T[] の xs[0] は「型は T なのに実体は undefined」という嘘の型になりうる。NonEmptyArray を使えばその嘘が発生しない。
reduce の初期値なし版、Math.max、「最新の1件」系の処理は、空配列だと壊れるか意味のない値を返す。
// T[] だと空のとき -Infinity が返る(そして呼び出し側はそれに気付かない)
function maxLoose(xs: number[]): number {
return Math.max(...xs);
}
maxLoose([]); // -Infinity
// NonEmptyArray なら空はコンパイル時に弾かれる
function max(xs: NonEmptyArray<number>): number {
return Math.max(...xs);
}
reduce も初期値なしで書ける。T[] だと実行時に TypeError: Reduce of empty array with no initial value が飛ぶリスクがあるが、NonEmptyArray ならそれが構造的に起こりえない。
const total = ne.reduce((acc, x) => acc + x); // 初期値なしでも安全
「空チェックは入口で1回だけ」という設計を型が強制してくれる。関数の奥のほうで if (xs.length === 0) throw ... を書き散らす必要がなくなる。
T[] から変換する外部入力(APIレスポンス、DBの検索結果、フォーム入力)は T[] として入ってくる。境界で型ガードを通して NonEmptyArray<T> に絞り込む。
export function isNonEmpty<T>(xs: readonly T[]): xs is NonEmptyArray<T> {
return xs.length > 0;
}
const rows = await findUsers(); // User[]
if (!isNonEmpty(rows)) {
return { status: "empty" as const };
}
// ここから先 rows は NonEmptyArray<User>
const first = rows[0]; // User(undefined ではない)
例外を投げる版(アサーション)も用意しておくと便利。
export function assertNonEmpty<T>(
xs: readonly T[],
message = "配列が空です",
): asserts xs is NonEmptyArray<T> {
if (xs.length === 0) throw new Error(message);
}
xs is NonEmptyArray<T>の書き方は型ガード(ユーザー定義型述語)、asserts xs is ...はアサーション関数。後者は呼び出し以降ずっと絞り込みが効く。
map / filter を通すと普通の配列に戻るこれが実務で一番ストレスになる点。Array.prototype.map の型定義は U[] を返すので、非空という情報が落ちる。
const names: NonEmptyArray<string> = ["a", "b"];
const upper = names.map((n) => n.toUpperCase()); // string[](NonEmptyArray ではない)
filter は要素が減るので当然として、map は要素数が変わらないため情報が落ちるのは惜しい。専用ヘルパーを用意して対処する。
export function mapNonEmpty<T, U>(
xs: NonEmptyArray<T>,
fn: (value: T, index: number) => U,
): NonEmptyArray<U> {
return xs.map(fn) as NonEmptyArray<U>;
}
中で as を使うことになるが、as の危険性をライブラリ的な1関数に閉じ込めるという考え方。→ 型指定について
NonEmptyArray<T> は結局配列なので、pop / shift / splice が呼べる。型は非空のままだが、実体は空になる。
const xs: NonEmptyArray<number> = [1];
xs.pop(); // 型エラーにならない
xs[0]; // 型は number、実体は undefined
対策は readonly にすること。実務ではこちらを既定にしたほうが安全。
export type ReadonlyNonEmptyArray<T> = readonly [T, ...T[]];
const ys: ReadonlyNonEmptyArray<number> = [1];
ys.pop(); // エラー: Property 'pop' does not exist on type 'readonly [number, ...number[]]'
用途に応じて2つ用意しておくか、そもそも readonly 版だけを公開するのがおすすめ。
配列リテラルを一度 const で受けてから渡すと、number[] に広がって代入できないことがある。
const xs = [1, 2, 3]; // number[] と推論される
const ne: NonEmptyArray<number> = xs; // エラー
const ok: NonEmptyArray<number> = [1, 2, 3]; // 直接書けば OK
const ok2 = [1, 2, 3] as const; // readonly [1, 2, 3] → ReadonlyNonEmptyArray に代入可
length は絞り込まれない[T, ...T[]] の length は number になる(固定長タプルなら 3 などのリテラル型になるが、Rest があるため確定しない)。「1以上」であることは型に表れないので、length を使った分岐は普通に必要。
同じ目的を達成する別の定義もある。
// A: タプル + Rest(今回の主題。もっとも一般的)
type NonEmptyA<T> = [T, ...T[]];
// B: 末尾に固定要素を置く
type NonEmptyB<T> = [...T[], T];
// C: 交差型で 0 番目を必須にする
type NonEmptyC<T> = T[] & { 0: T };
| 定義 | 特徴 |
|---|---|
A [T, ...T[]] |
先頭アクセスが安全。head/tail の分解と相性が良い。基本これを使う |
B [...T[], T] |
末尾アクセス(at(-1))を意識した書き方。分割代入がやや扱いにくい |
C T[] & { 0: T } |
古くからあるハック。map などの配列メソッドの戻り型は素の配列になり、エラーメッセージも読みにくい |
A は分割代入がそのまま効くのが強い。
const [head, ...tail] = ne; // head: T, tail: T[]
Zod v3 の .nonempty() は推論結果が [string, ...string[]] になるため、手書きの NonEmptyArray とそのまま互換になる。
import { z } from "zod";
const schema = z.object({
tags: z.array(z.string()).nonempty(), // v3: 推論は [string, ...string[]]
});
type Input = z.infer<typeof schema>;
// Input["tags"] は NonEmptyArray<string> と構造的に同じ
ただし .nonempty() の型への影響はバージョンによって変わる(Zod v4 系では実行時の最小長チェックのみで、推論は string[] のまま、という挙動変更がある)。使っているバージョンで z.infer の結果を実際に確認するのが確実。型が落ちる場合は、パース後に自前の isNonEmpty を通すのが安全。
const parsed = schema.parse(input);
assertNonEmpty(parsed.tags); // ここで NonEmptyArray<string> に確定させる
「最低1件必要」というビジネスルールは、コメントやテストではなく型で書ける。
type Order = {
id: string;
// 明細が0件の注文は存在しない、というルールを型で表明する
items: ReadonlyNonEmptyArray<OrderItem>;
};
function totalPrice(order: Order): number {
// 空チェック不要。合計が 0 になる「明細なし注文」を考えなくてよい
return order.items.reduce((acc, item) => acc + item.price, 0);
}
DB の CHECK 制約が「矛盾したデータを入れさせない」のと同じ発想を、型の世界でやっている。→ CHECK制約について
[T, ...T[]] は「固定要素1つ + Rest」で最低1要素を表すタプル型T[] の代わりに使うと、arr[0] の undefined 事故と「空のとき何を返すか」問題が消えるisNonEmpty)を通し、内部はずっと非空前提で書くのが基本形map で情報が落ちる・pop で空にできる、の2点だけ注意。前者はヘルパー、後者は readonly で対処noUncheckedIndexedAccess: true と併用すると効果が最大になる