第13章:ACLの核③ コード値・列挙・区分の変換(謎コード撲滅)🔤🧹
0. 今日やること(ゴール)🎯✨
外部APIが返してくる 謎コード(例:"1", "2", "A", "X9" みたいなやつ)を、内側(ドメイン)では 意味のある型 に変換できるようにします😊
そして 未知コード が来たときに「どう扱うか」を、設計として決められるようになります🧠🛡️
1. そもそも「コード値」ってなに?なんで危険?😵💫💥
外部APIあるある👇
stu_kbn: "1"← これ、見ても意味わからん😂- 仕様書に「1=学部生、2=院生、9=その他」みたいに書いてある
- しかも増える/変わる(ある日突然
"7"が追加される)😇
もし内側でそのまま使うと…
- 条件分岐が
"1"/"2"だらけ 🌀 - 読む人が毎回「えっと…1って何だっけ?」になる📖💦
- 外部仕様変更で内側が壊れる(腐敗💀)
だからACLでやることは超シンプル👇 外のコード値 → 内側の“意味ある型”に翻訳する 🗣️➡️📘
2. 2026のTypeScript的に「enum」はどう扱う?🤔🧩
TypeScriptのenumは、TypeScriptの中でも珍しく 実行時(JavaScript)に残る機能 です📦(=型だけじゃない)(TypeScript)
その結果、バンドルが増えたり、ツリーシェイクされにくかったりする話が出がちです🌲✂️(実例解説も多い)(LINE ENGINEERING)
さらに最近は、--erasableSyntaxOnly という「実行時に残る構文(例:enum)を禁止する」オプションも入っています🚫(TypeScript)
なのでこの教材では、“constオブジェクト + union型” を基本形にします✅✨
もちろん
enumが絶対ダメってわけじゃないよ🙂 ただ、ACLの「境界を薄く・安全に」って目的には、constオブジェクト型が相性よし💕
3. 基本形:内側は “意味ある型” を持つ(ドメイン側)📘✨
例:学生区分を、内側ではこう持つ👇
// domain/student/StudentType.ts
export const StudentType = {
Undergrad: "UNDERGRAD",
Grad: "GRAD",
Other: "OTHER",
} as const;
export type StudentType = (typeof StudentType)[keyof typeof StudentType];
ポイント💡
- 内側では
"1"とか一切出てこない🙅♀️ - 使う側は
StudentType.Undergradみたいに読める🎓✨ - 型としては
"UNDERGRAD" | "GRAD" | "OTHER"のunionになる👍
4. ACLでの変換:コード値→ドメイン型(翻訳テーブル)🗃️🔁
外部:stu_kbn が "1" | "2" | "9" で来る想定にします📡
4.1 外部コード型をまず固定(ACL側)🧱
// acl/studentDirectory/codes/ExternalStudentTypeCode.ts
export const externalStudentTypeCodes = ["1", "2", "9"] as const;
export type ExternalStudentTypeCode = (typeof externalStudentTypeCodes)[number];
export function isExternalStudentTypeCode(v: string): v is ExternalStudentTypeCode {
return (externalStudentTypeCodes as readonly string[]).includes(v);
}
4.2 変換テーブル(対応表)を作る📋✨
// acl/studentDirectory/codes/studentTypeMap.ts
import { StudentType } from "../../domain/student/StudentType";
import type { ExternalStudentTypeCode } from "./ExternalStudentTypeCode";
export const STUDENT_TYPE_BY_CODE = {
"1": StudentType.Undergrad,
"2": StudentType.Grad,
"9": StudentType.Other,
} as const satisfies Record<ExternalStudentTypeCode, (typeof StudentType)[keyof typeof StudentType]>;
ここで使ってるsatisfiesは、「型チェックはするけど、推論(as constの細さ)は保つ」 便利機能です🪄✨(TypeScript)
つまり👇
"1","2","9"を 書き漏らすとエラー になりやすい✅as constの良さ(リテラル推論)も失わない✅
5. 未知コードが来たらどうする?(設計の分かれ道)🚦🤔
外部は信用しないので、未知コードは普通に来る 前提で考えます😇

代表的な方針は3つ👇
方針A:即エラー(おすすめ寄り)💥
- データが壊れてる/仕様変更の可能性が高い
- 早めに気づける(監視・テストと相性◎)🚨
方針B:Other/Unknown に丸める🧺
- 表示だけしたい、止めたくない用途で便利
- ただし、問題が静かに隠れる リスクもある😶🌫️
方針C:隔離(扱えないデータとして保留)🧊
- 「後で調査」する仕組みがあるなら強い💪
- ACLでログやメトリクスが必要(20章の観測につながる)📈
この章では分かりやすく 方針A(エラー) の例でいきます🔥
6. 変換関数:DTOのコード値 → ドメイン型 ✅🛡️
// acl/studentDirectory/mappers/toStudentType.ts
import { StudentType } from "../../domain/student/StudentType";
import { STUDENT_TYPE_BY_CODE } from "../codes/studentTypeMap";
import { isExternalStudentTypeCode } from "../codes/ExternalStudentTypeCode";
export class UnknownCodeError extends Error {
constructor(public readonly field: string, public readonly code: string) {
super(`Unknown code for ${field}: ${code}`);
this.name = "UnknownCodeError";
}
}
export function toStudentType(stu_kbn: string) {
if (!isExternalStudentTypeCode(stu_kbn)) {
throw new UnknownCodeError("stu_kbn", stu_kbn);
}
return STUDENT_TYPE_BY_CODE[stu_kbn];
}
これで内側はずっとこう書けます👇(超読みやすい🥹✨)
if (student.type === StudentType.Grad) {
// 院生向け処理🎓
}
7. “表示名”はドメインに入れない(UI用マップで分離)🧼🧱
ドメインは 意味(概念) だけ持てばOK🙆♀️ 「日本語ラベル」はUIの都合なので別に持つのが安全です✨
// ui/labels/studentTypeLabel.ts
import { StudentType, type StudentType as StudentTypeUnion } from "../../domain/student/StudentType";
export const StudentTypeLabel: Record<StudentTypeUnion, string> = {
[StudentType.Undergrad]: "学部生",
[StudentType.Grad]: "院生",
[StudentType.Other]: "その他",
};
こうしておくと👇
- 画面の文言が変わってもドメインは無傷🛡️
- 多言語対応もやりやすい🌍✨
8. 変換テーブルの管理術(増えても地獄にならない)🗃️🧯
コード値変換は増えやすいので、ルールを作るのが勝ちです🏆
おすすめルール👇
- 変換テーブルは 「1種類=1ファイル」 にする(
paymentMethodMap.tsみたいに)📄 - 外部コード型(
ExternalXxxCode)も近くに置く📦 - 仕様書の意味をコメントで最小限残す📝
- 「この変換はACLの責任」って分かる場所に置く(client直後)🚪🧱
9. const enumはどう?(ちょい注意)⚠️
const enumは出力を小さくしやすい一方で、ビルドが「単一ファイル変換」前提の環境だと問題が出ることがあり、isolatedModulesが警告してくれる領域でもあります🧯(TypeScript)
なのでこの教材のACLでは、まずは constオブジェクト を基本形にしておくのが安心です😊
10. ミニ演習(手を動かすと一気に理解できる)🧪🔥
演習①:支払い方法コードを翻訳しよう💳
外部:pay_kbn: "A" | "B" | "Z"
内側:PaymentMethod = "CARD" | "CASH" | "OTHER"
やること👇
PaymentMethodを constオブジェクトで作るExternalPaymentCodeと判定関数を作るPAYMENT_METHOD_BY_CODEを作るtoPaymentMethod(code: string)を作る
演習②:未知コードの方針をBに変えてみよう🧺
throw の代わりに、未知なら Other を返すバージョンを作ってみる✨
(ログは20章で入れる予定でもOK👌)
演習③:ラベルをUI層に分離しよう🪄
PaymentMethodLabel を別ファイルに分けて作る🎨
11. テストの超ミニ例(変換はテスト効率が最強)🧪✨
※テスト環境の話は後の章で本格化するけど、雰囲気だけ先に🌸
// acl/studentDirectory/mappers/toStudentType.test.ts
import { describe, it, expect } from "vitest";
import { toStudentType, UnknownCodeError } from "./toStudentType";
import { StudentType } from "../../domain/student/StudentType";
describe("toStudentType", () => {
it("コード '1' は Undergrad に変換される", () => {
expect(toStudentType("1")).toBe(StudentType.Undergrad);
});
it("未知コードは UnknownCodeError", () => {
expect(() => toStudentType("999")).toThrow(UnknownCodeError);
});
});
変換は「入力→出力」が明確だから、ユニットテストが超ラクです💖🧪
12. AI(Copilot/Codex)に頼むと速いところ🤖⚡
やってほしいことがハッキリしてるので、AIが得意です✨ たとえばこんな依頼が強い👇
- 「外部コード
A/B/ZをPaymentMethodに変換するas constマップと型ガードを書いて」🗺️ - 「未知コードの扱いを “throw” と “Otherに丸める” の2案で関数作って」🔁
- 「この変換マップのテストケース、代表3つと境界値を提案して」🧪
最後にチェックするのはここ✅
- 内側に外部コードが漏れてない?(
"1"がドメインで登場してない?)🧼 - **未知コード方針が決まってる?**🚦
- **変換テーブルが1箇所に集まってる?**🗃️
まとめ(この章で持ち帰る型の形)🎁✨
- 外の
"1"|"2"はACLで翻訳して、内側はStudentTypeだけで生きる🌱 - 変換は テーブル + 型ガード + 方針(未知コード) の3点セット🧰
as const+satisfiesで「書き漏らしにくい」変換表が作れる🪄(TypeScript)- 最近のTypeScriptは
enumを禁止できるオプションもあるので、constオブジェクト運用が相性よし🚫✨(TypeScript)