TSKaigi 2026 「AI時代に考える、Branded Typesで実現する堅牢な型付け」

13.2K Views

May 23, 26

スライド概要

https://2026.tskaigi.org/talks/62

Docswellを使いましょう

(ダウンロード不可)

関連スライド

各ページのテキスト
1.

AI時代に考える、 Branded Typesで実現する 堅牢な型付け 2026.05.23 TSKaigi 2026 @yuta-ike エムスリー / エンジニアリンググループ

2.

型エラーが出ると 嬉しいですよね 😉

3.

Q. わたしたちは何のために型を書くのか

4.

Q. わたしたちは何のために型を書くのか A. 型エラーを出すため

5.

Q. どんなときに型エラーが出てほしいだろう

6.

Q. どんなときに型エラーが出てほしいだろう A. 実装が間違っているとき

7.

実装があっていて型チェックも通る 嬉しい

8.

実装が間違っていて型チェックが通らない 正常

9.

実装があっていて、型チェックが通らない 複雑な実装で型推論が追いつかない 止むを得ずimmutableな処理をしている (letの利用、再代入、Record<string, any>)

10.

実装が間違っていて、型チェックが通る 一番避けたい

11.

実装が間違っていて、型チェックが通る 型の表現力には制限があり、 完全に避けることはできない function add(a: number, b: number): number { retrun 0 } ※ 明らかに実装が間違っているが、型レベルで検知はできない(型チェックが通る)

12.

実装が間違っていて、型チェックが通る 型の表現力には制限があり、 完全に避けることはできない それでも、 このパターンは可能な限り小さくしたい

13.

そのためには、 より厳格な型を適用する必要がある。 そのための手段の1つが Branded Types。

14.

目標 Branded Typesを実際のアプリケーションで活用することで、 値の種類や属性・状態を型にエンコードすることができ、 堅牢なプログラミングにつながる ....という感覚を共有したい!

15.

Branded Typesとは type User = { id: string // ユーザーID name: string // 氏名 date: Date // 生年月日 } type Book = { id: string // 本のID name: string // 本のタイトル date: Date // 出版年 } 同じ構造の型がある場合、TypeScriptはこれらを同一の型と みなしてしまう(構造的部分型)

16.

Branded Typesとは type User = { id: string // ユーザーID name: string // 氏名 date: Date // 生年月日 __type: "User" } type Book = { id: string // 本のID name: string // 本のタイトル date: Date // 出版年 __type: "Book" } そこで、型の名前に対応する架空のプロパティを追加すること で、2つの構造を区別することができる(Branded Types)

17.

Branded Typesとは function createUser(user: User) { ... } const book: Book = { id: getUUID() name: "吾輩はエンジニアである", date: new Date(2000, 0, 1) } as Book createUser(book) // TypeError: Book型の変数をUser型には代入できない Branded Typesを使うと2つの型は完全に別の型として扱われるの で、当然代入しようとすると型エラーになる(嬉しい)

18.

プロダクト開発における Branded Types の 使いどころ 例1: ドメインモデルを区別するための利用 UserIDとBookID、UserとBookなどのドメインモデルの区別 例2: 値の状態や属性を表現する Unvalidated, Validatedなど、値の状態や属性を表現する

19.

例1: ドメインモデルを区別する export type User = { userId: string; userName: string; } UserIDとUserNameは本来的には全く別の概念 偶々同じデータ構造(文字列)を持っているだけで、同じ型(string)が割り当 てられる では、我々は何を持ってその値がUserIDであること理解するのか?

20.

ドメインモデルを区別する await getUserById(myUserId); // myUserId はユーザーIDであろう

 const id = user.id; // ...
 console.log(id); // この id は、文脈的にユーザーIDっぽい

 const name = user.id; // 変数名は UserName っぽいが、実体は UserID 変数名から推測する(myUserIdはユーザーIDを表しているに違いない) 周辺の処理・コンテキストから推測する(const id = user.id という処理が あれば、idはユーザーIDに違いない) どちらも人間の認知能力を前提とした曖昧なアプローチ。

21.

ドメインモデルを型レベルで表現する export type UserID = string & { __type: "userID" }

 const id = user.id;
 // ^? const id: UserID 型レベルでUserID型であることがわかる(代入しても)
 const hoge = id; // ^? const hoge: UserID 代入しても情報が失われない 
 updateProfileName(userId); // TypeError: UserID is not UserName UserNameを期待しているところにUserIDを指定すると型エ ラーで落とせる UserIDやUserNameをBranded Typesで定義することで、型システムレベルで ドメインモデルを表現できる。

22.

Branded Typesを利用した ドメインモデルの定義 ドメインの識別を型システムに担保させる → 言い換えると、ビジネスドメインを型に埋め込む行為 変数名やコメントなどの曖昧な表現に依存せずにすむ 代入しても型が失われない

23.

例2: 値の状態や属性を表現する

24.
[beta]
お題 Githubのようなユーザー名を指定して登録できるサービス
要件: ユーザー名は英数字のみ
① 入力値が string かチェック
② ユーザー名の要件を満たすか
正規表現をチェック
③ DBにユーザー情報を保存

function isValidUserName(input: string) {

return/[^0-9a-zA-Z]/.test(input);

}

app.post(async (c) => {

const input = c.json().userId; // unknown


// inputがstring型であることをチェック

if (typeof input !== "string") {

throw new ValidationError("文字列以外が指定されました");

}


// inputがユーザー名の要件を満たすかチェック

if (isValidUserName(input)) {

throw new ValidationError("利用できない文字が含まれています");

}


// DBに保存

await userRepo.createUser({ id: getUUID(), name: input });

});


25.
[beta]
お題 Githubのようなユーザー名を指定して登録できるサービス
要件: ユーザー名は英数字のみ
① 入力値が string かチェック
② ユーザー名の要件を満たすか
正規表現をチェック
③ DBにユーザー情報を保存

// input: unknown
① の処理実行
// input: string

app.post(async (c) => {

const input = c.json().userId; // unknown or any


// inputがstring型であることをチェック

if (typeof input !== "string") {

throw new ValidationError("文字列以外が指定されました");

}


26.
[beta]
お題 Githubのようなユーザー名を指定して登録できるサービス
要件: ユーザー名は英数字のみ
① 入力値が string かチェック
② ユーザー名の要件を満たすか
正規表現をチェック
③ DBにユーザー情報を保存

// input: unknown
① の処理実行
// input: string
② の処理実行
// input: string

型に変化がない

= 型レベルでは何もやって
いないと同じ?

function isValidUserName(input: string) {

return/[^0-9a-zA-Z]/.test(input);

}


if (isValidUserName(input)) {

throw new ValidationError("利用できない文字が含まれています");

}

27.
[beta]
お題 Githubのようなユーザー名を指定して登録できるサービス
要件: ユーザー名は英数字のみ
① 入力値が string かチェック
② ユーザー名の要件を満たすか
正規表現をチェック
③ DBにユーザー情報を保存

function isValidUserName(input: string) {

return/[^0-9a-zA-Z]/.test(input);

}


if (isValidUserName(input)) {

throw new ValidationError("利用できない文字が含まれています");

}

実装を見るとちゃんと仕事をしている。

でも仕事内容が関数のインタフェースに表出していない
(なぜ?)

28.

お題 Githubのようなユーザー名を指定して登録できるサービス unknown型 ①の バリデーション unknown型 string型

29.

お題 Githubのようなユーザー名を指定して登録できるサービス string型 ②の バリデーション string型 アルファベット+数字型

30.

お題 Githubのようなユーザー名を指定して登録できるサービス string型 ②の バリデーション string型 アルファベット+数字型 これに対応する型が存在しない

31.

お題 Githubのようなユーザー名を指定して登録できるサービス string型 ②の バリデーション string型 アルファベット+数字型 代わりにstring型で代用している

32.

お題 Githubのようなユーザー名を指定して登録できるサービス string型 ②の バリデーション string型 アルファベット+数字型 この小さい長方形の型を作ってあげる → Branded Types

33.
[beta]
お題 Githubのようなユーザー名を指定して登録できるサービス
今回はValidAsUserNameという名
前で型を定義。

あわせてこの型の値を得るための
ファクトリ関数も定義。
ValidAsUserName型の値は 、ファ
クトリ関数の戻り値からしか得られ
ない

ファクトリ関数の中では必ず英数字
のみの文字列かを検証する(バリ
デーション)


→ ValidAsUserName型は英数字のみ
から成り立つ文字列を表す型である

// Branded Typesの定義

export const ValidAsUserName = string & { __type:
"ValidAsUserName" }


// ValidAsUserNameを得るための関数を定義

export const ValidAsUserName = (input: string) => {

// 関数内でバリデーションを定義

if (/[^0-9a-zA-Z]/.test(input)) {

throw new Error("Invalid")

}

return input as ValidAsUserName

}

34.

お題 Githubのようなユーザー名を指定して登録できるサービス さらに、userRepoは ValidAsUserNameを受け取るように する。 // 元の実装 function createUser( userId: string, userName: string ) { ... } createUser関数の事前条件(引数として与え られる値の範囲)を制限することができ、関数 実装時に考えることが1つ減る。 (= 記号等が含まれるケースを考慮しなくて良 い) // Branded Typesを利用して、受け入れ可能な値をより厳密に表明 function createUser( userId: string, userName: ValidAsUserName ) { ... } そのため、ユニットテストなどを削減できる可 能性がある。

35.

お題 Githubのようなユーザー名を指定して登録できるサービス 要件: ユーザー名はアルファベットと 数字のみ ① 入力値が string かチェック ② ユーザー名の要件を満たすか 正規表現をチェック ③ DBにユーザー情報を保存 // input: unknown ① の処理実行 // input: string ② の処理実行 // input: ValidAsUserName ③の関数はValidAsUserNameのみを 受け入れる → ②の処理を忘れた場合は③の呼び出しで型エラー(嬉しい) → バリデーションをしないと型エラーが出るため、値の検証を強制できる

36.

ValidAsUserName型とは何なのか string型の中でも「英数字のみからなる」という性質を持つ値 言い換えると、「英数字のみからなる」という性質を型で表現している ほかにも、様々な意味を持つ型を定義可能 UserID型は「ユーザーID」という意味を持つ(= 値の意味を型にエン コードする) UnvalidatedUserName型は「検証前のユーザー名」という意味を 持つ(= 値の状態を型にエンコードする)

37.

Branded Typesで型に意味をエンコードする Branded Typesを値に意味を持たせる手段として使う。 変数名やコメントと違い、代入しても追跡が途絶えない。 TypeScriptの型システムのメリットを享受でき、堅牢。 データ構造(string)ではなく、ビジネス上の概念を型にマッピング する。 一部のドメインロジックの検証を型システムに載せるイメージ

38.

実際の開発チームでの活用 ほとんどのEntity + ValueObject、一部のプロパティはBranded Typesで定義 IDやEmailなどPrimary KeyになりうるものはBrand化 一方でそのほかの属性は通常通りプリミティブなtypeで定義 ユーティリティを取っている Branded Typesはzod等の外部ライブラリを使わず自前で定義。データの詰め替え はスプレッド構文を使うとそこまで発生しない。 コードベース全体は、型を全面に押し出したFunctionベースの構成(classはあまり使っ ていない)

39.

現時点での所感 型チェックが通っているならとりあえず安心という感覚がある もちろん全てではないので、テストもちゃんと書く 人間が実装している場合も、型エラーによって処理の漏れや考慮漏れに気づくことが 多く便利 一方で、あくまでランタイムに影響を与えないことを意識することは必要(as等で簡 単に嘘をつける)

40.

まとめ Branded Typesを実際のアプリケーションで活用することで、 値の種類や属性・状態を型にエンコードすることができ、 堅牢なプログラミングにつながる → この堅牢性がAI時代の武器になる(はず)

41.

ちょっとだけ蛇足

42.

なぜTypeScriptを利用するか (私は)ランタイムエラーを防ぐ目的が大きいと思っている ex. オブジェクトだと思ってたらnullだった user.greet() // Uncaught TypeError: Cannot read properties of null TypeScriptはデータ構造を型として扱う ランタイムエラーは防げるが、あくまでデータ構造ベース。 ドメインロジックを検証することはできない

43.

Branded Typesを使う世界線 userIdとuserNameは、データ構造的には同じstring でも実際、我々がそこに見出している意味は異なる。 この意味をそのまま型の世界にエンコードしてあげることで、より堅牢なプロ グラミングが可能になる(はず)

44.

Ask the Speaker or 懇親会でお話しましょう @yuta-ike エムスリー / エンジニアリンググループ