なぜ型にこだわるか TSKaigi Sendai 2026

5.5K Views

November 01, 26

スライド概要

スライド中で参照している過去の登壇資料

TSKaigi 2026 「AI時代に考える、Branded Typesで実現する堅牢な型付け
https://www.docswell.com/s/4136989/Z6NJ78-tskaigi2026/1

制約を表現する型を書く
https://www.docswell.com/s/4136989/KN79JV-tskaigi-side-event-20260609/1

Docswellを使いましょう

(ダウンロード不可)

関連スライド

各ページのテキスト
1.

なぜ型にこだわるのか 2026.11.01 TSKaigi Sendai 2026 @yuta-ike エムスリー / エンジニアリンググループ

2.

制御フロー解析

3.

制御フロー解析による型の絞り込み 分岐やReturnなどの制御の流れに応じて、変数の型を絞り込む。 function format(value: string | number) { if(typeof value === "string") { return value.toUpperCase(); // value: string } return value.toFixed(2); // value: number }

4.

制御フロー解析による型の絞り込み 分岐やReturnなどの制御の流れに応じて、変数の型を絞り込む。 function format(value: string | number) { if(typeof value === "string") { return value.toUpperCase(); // value: string } return value.toFixed(2); // value: number }

5.

制御フロー解析による型の絞り込み 分岐やReturnなどの制御の流れに応じて、変数の型を絞り込む。 function format(value: string | number) { if(typeof value === "string") { return value.toUpperCase(); // value: string } return value.toFixed(2); // value: number }

6.

TypeScriptのif文 function format(value: string | number) { if(typeof value === "string") { return value.toUpperCase(); // value: string } return value.toFixed(2); // value: number } 型の絞り込みをするif 通常のif

7.

今回のお題 管理者だけがアプリ全体の設定を変更できる、という仕様を考える type User = AdminUser | NormalUser function updateConfig(actor: User) { if (actor.role === "admin") { throw new Error("権限がありません"); } // アプリ全体の設定を変更する }

8.

今回のお題 管理者だけがアプリ全体の設定を変更できる、という仕様を考える type User = AdminUser | NormalUser function updateConfig(actor: User) { if (actor.role === "admin") { throw new Error("権限がありません"); } // アプリ全体の設定を変更する }

9.

今回のお題 管理者だけがアプリ全体の設定を変更できる、という仕様を考える type User = AdminUser | NormalUser function updateConfig(actor: User) { if (actor.role === "admin") { throw new Error("権限がありません"); } // アプリ全体の設定を変更する }

10.

今回のお題 管理者だけがアプリ全体の設定を変更できる、という仕様を考える type User = AdminUser | NormalUser function updateConfig(actor: User) { if (actor.role === "admin") { throw new Error("権限がありません"); } // アプリ全体の設定を変更する }

11.

今回のお題 管理者だけがアプリ全体の設定を変更できる、という仕様を考える type User = AdminUser | NormalUser function updateConfig(actor: User) { if (actor.role === "admin") { throw new Error("権限がありません"); } // アプリ全体の設定を変更する }

12.
[beta]
権限チェックの書き忘れ
権限チェックのif文を書き忘れても型エラーにならない。
function updateConfig(actor: User) {
// if (actor.role === "admin") {
// throw new Error("権限がありません");
// }
// アプリ全体の設定を変更する
}
const actor: User = { id: "1", role: "Normal" };
updateConfig(actor); // 型エラー無し
13.
[beta]
権限チェックの書き忘れ
権限チェックのif文を書き忘れても型エラーにならない。
function updateConfig(actor: User) {
// if (actor.role === "admin") {
// throw new Error("権限がありません");
// }
// アプリ全体の設定を変更する
}
const actor: User = { id: "1", role: "Normal" };
updateConfig(actor); // 型エラー無し
14.

せっかくTypeScriptを使うのであれば、型エラーが出てほしい...

15.

型の絞り込みに着目 if文は型の絞り込みをしているものの、絞り込まれたことを検知するロジックはない type User = AdminUser | NormalUser function updateConfig(actor: User) { if (actor.role === "admin") { throw new Error("権限がありません"); } // アプリ全体の設定を変更する }

16.

型の絞り込みに着目 if文は型の絞り込みをしているものの、絞り込まれたことを検知するロジックはない type User = AdminUser | NormalUser function updateConfig(actor: User) { if (actor.role === "admin") { throw new Error("権限がありません"); } // アプリ全体の設定を変更する }

17.

型が絞り込まれていることが検証できるようにすると良さそう

18.

リファクタ function updateConfig(actor: AdminUser) { // アプリ全体の設定を変更する } function handler(actor: User) { if(actor.role === "admin") { updateConfig(actor) // const actor: AdminUser } }

19.

リファクタ function updateConfig(actor: AdminUser) { // アプリ全体の設定を変更する } function handler(actor: User) { if(actor.role === "admin") { updateConfig(actor) // const actor: AdminUser } }

20.

リファクタ function updateConfig(actor: AdminUser) { // アプリ全体の設定を変更する } function handler(actor: User) { if(actor.role === "admin") { updateConfig(actor) // const actor: AdminUser } }

21.

if文の漏れが検知できる if文を書き忘れると...型エラーになる(!!) function updateConfig(actor: AdminUser) { // アプリ全体の設定を変更する } function handler(actor: User) { // if(actor.role === "admin") { updateConfig(actor) // const actor: User // } // Argument of type 'User' is not assignable to parameter of type 'AdminUser'. }

22.

考察 一見等価なロジックなのに、 なぜ型エラーの有無が変わるのか?

23.

考察 一見等価なロジックなのに、 なぜ型エラーの有無が変わるのか? function updateConfig(actor: User) { if (actor.role === "admin") { throw new Error("権限がありません"); } // アプリ全体の設定を変更する } function handler(actor: User) { updateConfig(actor) } リファクタ前 function updateConfig(actor: AdminUser) { // アプリ全体の設定を変更する } function handler(actor: User) { if(actor.role === "admin") { updateConfig(actor) } } リファクタ後

24.

if文が型解析に影響を与える リファクタ後のコード function updateConfig(actor: AdminUser) { // アプリ全体の設定を変更する } function handler(actor: User) { if(actor.role === "admin") { updateConfig(actor) // const actor: AdminUser } }

25.

if文が型解析に影響を与える リファクタ後のコード function updateConfig(actor: AdminUser) { // アプリ全体の設定を変更する } function handler(actor: User) { if(actor.role === "admin") { updateConfig(actor) // const actor: AdminUser } }

26.

if文が型解析に影響を与える リファクタ後のコード function updateConfig(actor: AdminUser) { // アプリ全体の設定を変更する } function handler(actor: User) { if(actor.role === "admin") { updateConfig(actor) // const actor: AdminUser } }

27.

型の絞り込みが発生するif文か • if文が型の絞り込みの役割を果たしている • updateConfigが絞り込み後の型しか受け取らない → if文の漏れを型レベルで検知できた → if文が型レベルでちゃんと仕事をしている状態

28.

最初からリファクタ後の実装を出すにはどう考えれば良いか?

29.

さらに考察 インターフェースに着目する

30.

リファクタ前後のインターフェースを考える リファクタ前 function updateConfig(actor: User): Promise<void> リファクタ後 function updateConfig(actor: AdminUser): Promise<void>

31.

リファクタ前後のインターフェースを考える リファクタ前 function updateConfig(actor: User): Promise<void> リファクタ後 function updateConfig(actor: AdminUser): Promise<void> → リファクタ後の方が、「管理者しか変更できない」という "意図"をよりよく表現している

32.

「意図の明白なインターフェース」 関数の名前やシグネチャから、その関数の目的や振る舞いが明白に分かること。

33.

意図とは何か? プログラムの意図 = アプリケーションの仕様

34.

プログラマの仕事 アプリケーションの仕様 翻訳 TypeScript(ドメインロジック)

35.

意図が明示的であることのメリット • 可読性が高まる • 実装の中身を読まなくても関数の処理を把握できる(適切なカプセル化) TypeScriptでは、、、

36.

意図が明示的であることのメリット • 可読性が高まる • 実装の中身を読まなくても関数の処理を把握できる TypeScriptでは、、、 • 意図(= 仕様)を型で表現することで、我々が書いた 実装が仕様を満たすかのチェックをTypeScriptに委譲 することができる

37.
[beta]
意図が明示的であることのメリット
「updateConfigという操作は管理者しか呼び出せない」という仕様を型で表
現することで、updateConfigを管理者以外のユーザーが呼び出すコードを型検
査でエラーにできる。
function updateConfig(actor: AdminUser): Promise<void> {
// ...
}
const user = { id: "1", role: "Normal" }
updateConfig(user)
// Argument of type 'User' is not assignable
to parameter of type 'AdminUser'.
38.

仕様を型に"エンコード"する 仕様:「updateConfigという操作は管理者しか呼び出せない」 function updateConfig(actor: AdminUser): Promise<void>

39.

なぜ型で担保したい? • 他にも色々な方法はある • 自動テスト • QA • 動作確認 → これらは、高々N個のケースしか網羅できない (テストケースやQAシナリオの正確性に依存する) 一方で、型は任意の値に対して成立することを保証できる ※完璧ではないけど

40.

なぜ複雑な型を書くのか? 仕様を過不足なく型で表現するために必要だから。 そして、仕様を型で表現することで、実装が仕様を満たして いることの保証(の一部)をTypeScriptコンパイラに委譲 できるから。

41.

まとめ • 型は単なるデータの型だけでなく、仕様を表現するための 手段になる • 良い型を書くことで、実装が仕様を満たしているかを TypeScriptにチェックさせることが可能。 • 型で保証することで、自動テストやQAが苦手な側面(網 羅性)をカバーすることができる

42.

まとめ • 型は単なるデータの型だけでなく、仕様を表現するための 手段になる • 良い型を書くことで、実装が仕様を満たしているかを TypeScriptにチェックさせることが可能。 • 型で保証することで、自動テストやQAが苦手な側面(網 羅性)をカバーすることができる → 型を味方につけよう

43.

補足:型とユニットテスト User型 AdminUser型 updateConfigが エラーをthrowする updateConfigが正常に終了する

44.

補足:型とユニットテスト そのほかの任意の値 User型 AdminUser型 updateConfigが 型エラーになる updateConfigが エラーをthrowする updateConfigが正常に終了する

45.

補足:型とユニットテスト updateConfigの成功の定義(事後条件)と、その前提条件(事前条件) 事前条件 actorが AdminUserである ...が成立するならば... 事後条件 設定変更の処理が 正常に完了する

46.

補足:型とユニットテスト updateConfigの成功の定義(事後条件)と、その前提条件(事前条件) 事前条件 actorが AdminUserである ...が成立するならば... 事後条件 設定変更の処理が 正常に完了する 事前条件が成立していることを、どうやって保証するか?

47.

補足:型とユニットテスト リファクタ前 function updateConfig(actor: User): Promise<void> ok Unit Test で弾く 型で弾く AdminUser User その他の値

48.

補足:型とユニットテスト リファクタ前 function updateConfig(actor: User): Promise<void> ok Unit Test で弾く 型で弾く AdminUser User その他の値 ok 型で弾く リファクタ後 function updateConfig(actor: AdminUser): Promise<void>

49.
[beta]
補足:型とユニットテスト
型の表現力を高めることで、ユニットテストで保証する範囲を狭めることが
できる。
• リファクタ前の実装では、NormalUserを受け付けないことをランタイム
のテストでチェックする必要があった
const user = { id: "1", role: "normal" }
expect(() => updateConfig(user)).toThrowError()
• リファクタ後は、そもそもNormalUserを引数に渡せなくなった。
• そのようなコードを書いた時点で型エラーになる(シフトレフト)
function updateConfig(actor: AdminUser): Promise<void>
50.

蛇足① とはいえ、世の中の仕様に対して型の表現力は非常に小さいので、 うまく型で表せないものは多数ある。 • 「0以上の整数」を表す型 → TSでは表現できない • 「空ではない文字列」を表す型 → TSでは表現できない • 「validなメールアドレス」を表す型 → めっちゃ頑張れば可能?

51.
[beta]
蛇足①
Branded Typesを使うと、型の自作のようなことが可能。これにより任意の
制約を表現できる。(ただしランタイム処理に依存する)
type PositiveInt = number & { __brand: "positive" }
const PositiveInt = (val: number) => {
if(0 < val && Number.isInteger(val)) {
return val as PositiveInt
}
throw new Error("条件を満たしません")
}
52.

詳しくはこちら TSKaigi 2026 Leveragesトラック 30分セッション DAY2 15:10 ~ 15:40 AI時代に考える Branded Typesで実現する堅牢な型付け 池奥裕太/@yuta-ike https://www.docswell.com/s/4136989/Z6NJ78-tskaigi2026/1

53.

蛇足② 引数の型を「User → AdminUser」に変更することで、実はthrow Errorを消 すことができている。 function updateConfig(actor: User) { if (actor.role === "admin") { throw new Error("権限がありません"); } // アプリ全体の設定を変更する } function updateConfig(actor: AdminUser) { // アプリ全体の設定を変更する } function handler(actor: User) { if(actor.role === "admin") { updateConfig(actor) } }

54.

蛇足② エラーのthrowは型に表出しないため、「明白なインターフェース」からは遠 ざかる。 理想はエラーをthrowしない関数 => 全域関数

55.

全域関数と部分関数 全域関数: 任意の入力について答えが定まる関数 (必ずreturnする) 部分関数: 答えが定まらない入力が存在する関数 (Errorが出る入力パターンがある) ex. 足し算はオーバーフローを考慮しなければ全域関数 割り算はN÷0が定義されないので部分関数

56.

リファクタ前後のインターフェースを考える User型 AdminUser型 updateConfigを実行可能 updateConfigを実行できない

57.

リファクタ前後のインターフェースを考える User型 AdminUser型 元の実装は、User全体を 引数として取っていた function updateConfig(actor: User): Promise<void>

58.

リファクタ前後のインターフェースを考える User型 AdminUser型 リファクタ後は、確実に処 理を実行できる範囲に引数 を狭めている function updateConfig(actor: AdminUser): Promise<void>

59.

リファクタ前後のインターフェースを考える • 処理が定義できない値を入力範囲(定義域)から外すことで、関数内部で発生 する異常系を潰すことができる User型 AdminUser型 updateConfigを実行可能 updateConfigを実行できない

60.

定義域を狭めても発生するエラーの対処 DBの接続エラーなど、アプリケーションの外部に由来するエラーは排除できない • 汎化して言うと、入力値(引数)以外に依存して引き起こされるエラーは排除 できない → そのようなエラーが発生し得る環境でも全域化を行う手段がResult型 → 入力範囲(定義域)を狭めるのではなく、戻り値の範囲(値域)を広げる。

61.
[beta]
Result型も同じアプローチ
エラーの代わりに値を返すことで、エラー情報を型に表出させる
async function getUserByUserId(userId: string)
: Result<User, UserNotFoundError> {
const user = await db.user.findUnique({ id: userId })
if(user == null) {
return err(new UserNotFoundError("User not found"))
}
return ok(user)
}
62.
[beta]
Resultを使う前に、引数を絞れないか考える
例えばこのように、PermissionErrorをResult型で返すのは悪手。
function updateConfig(actor: User)
: Result<void, PermissionError>{
if (actor.role === "admin") {
return err(new PermissionError())
}
// アプリ全体の設定を変更する
}
63.
[beta]
Resultを使う前に、引数を絞れないか考える
なぜなら、PermissionErrorが起きえない入力でもResultが帰ってしまうため。
const actor: AdminUser = { id: "1", role: "admin" }
const res = updateConfig(actor)
// ^ const res: Result<void, PermissionError>
if(!res.ok) console.error("権限エラーが発生しました")
// ^ 絶対にtrueにならないifを書く羽目に...
updateConfigに、AdminUserを渡せば処理は必ず成功するはず。
しかしResult型が返ってしまうためエラーハンドリングをしなければならない。
→ 型の誤魔化しや虚無のエラーハンドリングを行う羽目になる
64.

なぜこうなるのか? 理論上、updateConfigの出力の型は入力の型に依存して変化する。 しかし現状のTypeScriptはこの絞り込みをサポートしていない。 NormalUser → 必ずError 入力 AdminUser → 必ずSuccess 出力 User (AdminUser | NormalUser) → Success | Error

65.
[beta]
FYI: 迂回策①
オーバーロードを利用する。
function updateConfig(actor: AdminUser): Result<Config, never>
function updateConfig(actor: NormalUser): Result<never, PermissionError>
function updateConfig(actor: User): Result<Config, PermissionError>
function updateConfig(actor: User): Result<Config, PermissionError> {
// ...
return { ok: true, data: { color: "green" } }
}
playground
66.
[beta]
FYI: 迂回策②
Conditional Typeで型レベルで条件分岐を表現する
function updateConfig<T extends User>(actor: T):
T["role"] extends "admin"
? Result<Config, never>
: T["role"] extends "normal"
? Result<never, PermissionError>
: Result<Config, PermissionError> {
// ...
}
playground
67.

FYI: 迂回策 迂回策①:オーバーロードを利用する 迂回策②:型レベルで条件分岐を表現する どちらもシンプルさには欠ける。ここまでやるのであれば、 素直に引数の型をしぼるべき(と思います)

68.

詳しくはこちら 型の力を引き出す TypeScriptの書き方 2026.06.09 TSKaigi 2026 本編で話せなかったこと、 話し足りなかったこと @yuta-ike エムスリー / エンジニアリンググループ https://www.docswell.com/s/4136989/KN79JV-tskaigi-side-event-20260609/1

69.

Appendix 元の実装でも、ifの漏れを防ぐ仕組みは可能ではある(一応) type User = AdminUser | NormalUser function updateConfig(actor: User) { if (actor.role === "admin") { throw new Error("権限がありません"); } const _: AdminUser = actor } actorがAdminUser でない場合は型エラー