第15章 コレクション不変条件:空禁止・重複禁止・上限など📦🚫✨

この章はね、「配列(コレクション)って便利だけど、放っておくと一番バグりやすい😇」ってところを、型+操作APIでカッチリ守れるようにする回だよ〜!💪💎
今日のゴール🎯✨
次の“ありがちなルール”を、途中で壊れない設計にできるようになるよ🙂
- 空は禁止(最低1個は必要)🚫🫙
- 重複は禁止(同じタグ2回入れない)🚫🔁
- 上限あり(最大5個まで)🧢5️⃣
- ついでに:中身の形式(空白NG、長さ制限、正規化)🧼🔤
15-1. なんで配列は事故りやすいの?😵💫🌀
配列って、気づいたらこうなるの…👇
- いろんな場所で
if (tags.length > 5) ...が散る🌀 - 追加するときはチェックしたのに、別の場所で
push()して壊れる💥 - “重複なしのはず” がいつの間にか重複してる😇
- 空配列が混ざって「表示できない」「保存できない」が発生😭
結論:配列を生で持たせないのが超効くよ🛡️✨ (=「壊れる操作」を封じる!)
15-2. コレクション不変条件を守る「3つの型」💎📦
A) 非空(NonEmpty)🫙🚫
「必ず1個以上ある」って保証が欲しいとき!
- 例:注文の明細は最低1行必要🧾
- 例:ユーザーが選んだ候補は最低1つ🎯
B) ユニーク(Unique)🚫🔁
「同じものは2回入れない」!
- 例:タグ、ロール、権限、選択肢🏷️🔐
C) 上限(Bounded)🧢
「最大◯個まで」!
- 例:タグ最大5個、画像最大10枚📸
これらを、“生成”と“更新”の両方で守るのがポイントだよ😊✨
生成時だけ守っても、あとで push() されたら終わり😇
15-3. 守り方の基本方針🛡️✨
コレクションの理想はこれ👇
- 外からは
readonlyとして見せる(勝手に変更できない)🧊 - 変更は add/remove みたいな「意図のある操作」だけ提供する🧰
- 操作の中で不変条件チェック✅
- 失敗は Result で返す(例外乱発しない)📦🙂
readonly や ReadonlyArray は TypeScript の定番の守り方だよ🧊✨(読み取り専用配列の型注釈) (typescriptbook.jp)
15-4. 実装してみよう:タグは「最大5個・重複なし・空白NG」🏷️🧼🧢
ここからは実戦! 題材は「記事に付けるタグ」みたいなやつね🙂✨
① まず Result 型(超ミニ)📦✨
成功/失敗を返すための箱だよ🎁
export type Ok<T> = { ok: true; value: T };
export type Err<E> = { ok: false; error: E };
export type Result<T, E> = Ok<T> | Err<E>;
export const ok = <T>(value: T): Ok<T> => ({ ok: true, value });
export const err = <E>(error: E): Err<E> => ({ ok: false, error });
② Tag(値オブジェクト)を作る💎🏷️
「タグは string です」じゃなくて、ルール込みの Tagにするよ!
ルール例👇
- 前後空白はトリム✂️
- 空文字は禁止🚫
- 小文字に正規化(好みで)🔡
- 長すぎ禁止(例:20文字まで)📏
export type Tag = {
readonly value: string;
};
export type TagError =
| { type: "Tag.Empty" }
| { type: "Tag.TooLong"; max: number }
| { type: "Tag.InvalidChars" };
const MAX_TAG_LEN = 20;
// 超シンプル:英数とハイフンだけ許可(必要に応じて調整してね🙂)
const TAG_PATTERN = /^[a-z0-9-]+$/;
export const Tag = {
create(raw: string): Result<Tag, TagError> {
const normalized = raw.trim().toLowerCase();
if (normalized.length === 0) return err({ type: "Tag.Empty" });
if (normalized.length > MAX_TAG_LEN) return err({ type: "Tag.TooLong", max: MAX_TAG_LEN });
if (!TAG_PATTERN.test(normalized)) return err({ type: "Tag.InvalidChars" });
return ok({ value: normalized });
},
} as const;
これで「Tag 型の value は、必ず検証済み」って言えるようになるよ💎😊
③ TagList(コレクション不変条件の本丸)📦🔥
今回の不変条件はこれ👇
- 最大5個まで🧢5️⃣
- 重複禁止🚫🔁
- 中身は Tag(検証済み)💎
- 外からは変更不可(readonly)🧊
export type TagList = {
readonly items: ReadonlyArray<Tag>;
};
export type TagListError =
| { type: "TagList.TooMany"; max: number }
| { type: "TagList.Duplicated"; value: string };
const MAX_TAGS = 5;
const isDuplicated = (tags: ReadonlyArray<Tag>): string | null => {
const seen = new Set<string>();
for (const t of tags) {
if (seen.has(t.value)) return t.value;
seen.add(t.value);
}
return null;
};
export const TagList = {
// まとめて作る(例:保存データや入力変換のあとに使う)
create(tags: ReadonlyArray<Tag>): Result<TagList, TagListError> {
if (tags.length > MAX_TAGS) return err({ type: "TagList.TooMany", max: MAX_TAGS });
const dup = isDuplicated(tags);
if (dup) return err({ type: "TagList.Duplicated", value: dup });
return ok({ items: [...tags] });
},
// 1個追加(意図のある操作✨)
add(list: TagList, tag: Tag): Result<TagList, TagListError> {
const next = [...list.items, tag];
return TagList.create(next);
},
// 削除(なければそのまま返す方針でもOK🙂)
remove(list: TagList, tag: Tag): TagList {
const next = list.items.filter((t) => t.value !== tag.value);
// removeは失敗させない(空許容かどうかはドメイン次第!)
// もし「最低1個」なら、ここもResultにして NonEmpty を守る✅
return { items: next };
},
} as const;
ここ、めっちゃ大事ポイント⚠️✨
itemsをReadonlyArray<Tag>にして、外からpush()できないようにしてる🧊- 変更は
add/remove経由にして、必ずcreate()で不変条件を再チェック✅ - 「更新しても壊れない」=これが設計の勝ち🏆💎
15-5. “空禁止(NonEmpty)” もやるならこうする🫙🚫✨
もし「タグは最低1個必要!」なら、remove も失敗する可能性があるよね🙂
その場合は NonEmptyTagList に分けるのがキレイ✨
考え方👇
TagList(0個OK)NonEmptyTagList(1個以上)🫙
こうすると、「空でもいい場面」と「空はダメな場面」が混ざらなくなるよ😊
15-6. テストで“壊れない”を固定しよう🧪😍
不変条件はテストが書きやすい!最高!🎉 テスト基盤は Vitest が今どきの定番だよ(ガイド) (vitest.dev)
テスト例(TagList)🧪
import { describe, it, expect } from "vitest";
import { Tag, TagList } from "./tag";
describe("TagList", () => {
it("最大5個まで", () => {
const tags = ["a","b","c","d","e"].map((s) => Tag.create(s)).map((r) => (r.ok ? r.value : null));
expect(tags.every((t) => t !== null)).toBe(true);
const list = TagList.create(tags as any);
expect(list.ok).toBe(true);
const sixth = Tag.create("f");
expect(sixth.ok).toBe(true);
const added = TagList.add(list.ok ? list.value : (null as any), sixth.ok ? sixth.value : (null as any));
expect(added.ok).toBe(false);
if (!added.ok) expect(added.error.type).toBe("TagList.TooMany");
});
it("重複は禁止", () => {
const t1 = Tag.create("React");
const t2 = Tag.create("react"); // 正規化で同じになる想定🙂
expect(t1.ok && t2.ok).toBe(true);
const list = TagList.create([t1.ok ? t1.value : (null as any), t2.ok ? t2.value : (null as any)]);
expect(list.ok).toBe(false);
if (!list.ok) expect(list.error.type).toBe("TagList.Duplicated");
});
});
15-7. よくある落とし穴集⚠️😇
❌ string[] のまま頑張る
「チェックを散らす」方向に行きがちで、漏れます🌀
❌ Set<string> にすればOKと思う
ユニークにはなるけど、
- 上限
- 正規化
- 表示順
- エラー理由(何がダメ?) がふわっとしがち😇 “ドメインのルール” を表現するには、型+APIが強いよ💎
❌ readonly なら完全に安全?
TypeScript の readonly はコンパイル時の防波堤🧊
運用で as any しちゃうと壊せるので、境界での運用ルールも大事だよ🙂
(readonly配列の基本はここがまとまってるよ) (typescriptbook.jp)
15-8. ミニ課題📝✨(やると一気に身につく!)
- NonEmptyArray を作ってみよう🫙
create(items)は空なら Errhead()は必ず返る(undefined禁止)✅
- UniqueList
を作ってみよう🚫🔁
create(items, keyFn)で重複チェック- 失敗時は「どのキーが重複したか」を返す🎯
- BoundedList
を作ってみよう🧢
maxを受け取って、超えたら Err
15-9. AI活用(この章向け)🤖✨
AIは「コードを全部書かせる」より、漏れ探しが超得意だよ〜!🔍💕
おすすめ質問テンプレ👇
- 「この TagList の不変条件が破れる入力パターンを20個出して」🧠
- 「add/remove 以外に必要な操作ある? replaceAll とか rename とか」🧰
- 「エラー型の粒度、初心者向けに改善案ある?」🫶
- 「テスト観点(成功/失敗/境界値)を列挙して」🧪
ちょい最新情報メモ(2026っぽい空気)🗞️✨
- TypeScript は npm 上だと 5.9.3 が最新として掲載されてるよ(2025-09-30公開) (NPM)
- さらに次の大きな流れとして、TypeScript 6.0 は “橋渡し” で、TypeScript 7 はネイティブ化で高速化…みたいな話が公式から出てるよ🚀 (Microsoft for Developers)
(でもこの章の内容は、そういう未来が来てもずっと使える設計の基礎だよ💎🙂)
まとめ🌸✨
コレクション不変条件はこう守る!👇
- 生配列をドメインに入れない📦🚫
- create(生成)で保証🏭✅
- 更新は add/remove みたいな意図APIだけ🧰✨
- 外へは readonly で見せる🧊
- 失敗は Result で返す📦🙂