YASD-TECH
YASD TECH
# TypeScript

NonEmptyArray(空でない配列の型)

投稿日:2026/7/26

更新日:2026/7/26

ttitleImage

概要

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;  // エラー

何が嬉しいのか

1. 先頭要素のアクセスが安全になる

最大の利点。tsconfig.jsonnoUncheckedIndexedAccess: 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 を使えばその嘘が発生しない。

2. 集約処理で「空のとき何を返すか」を考えなくてよい

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); // 初期値なしでも安全

3. バリデーションの位置が明確になる

「空チェックは入口で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 ... はアサーション関数。後者は呼び出し以降ずっと絞り込みが効く。


ハマりどころ

1. 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関数に閉じ込めるという考え方。→ 型指定について

2. 破壊的メソッドで空にできてしまう

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 版だけを公開するのがおすすめ。

3. リテラルから直接代入するときの型推論

配列リテラルを一度 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 に代入可

4. length は絞り込まれない

[T, ...T[]]lengthnumber になる(固定長タプルなら 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 と併用すると効果が最大になる

Index

  • 概要
  • 構文の中身
  • 何が嬉しいのか
  • 1. 先頭要素のアクセスが安全になる
  • 2. 集約処理で「空のとき何を返すか」を考えなくてよい
  • 3. バリデーションの位置が明確になる
  • 型ガードで T[] から変換する
  • ハマりどころ
  • 1. map / filter を通すと普通の配列に戻る
  • 2. 破壊的メソッドで空にできてしまう
  • 3. リテラルから直接代入するときの型推論
  • 4. length は絞り込まれない
  • 他の書き方との比較
  • 実践パターン
  • スキーマバリデーションと組み合わせる
  • ドメインルールを型で表す
  • まとめ