制約を表現する型を書く

414 Views

June 09, 26

スライド概要

TSKaigi2026本編での発表: https://www.docswell.com/s/4136989/Z6NJ78-tskaigi2026/1

Docswellを使いましょう

(ダウンロード不可)

関連スライド

各ページのテキスト
1.

制約を表現する型を書く 2026.06.09 TSKaigi 2026 本編で話せなかったこと、 話し足りなかったこと @yuta-ike エムスリー / エンジニアリンググループ

2.

型 = プログラムの制約を 表現する手段

3.

型がない場合 どんな値が渡されるか分からない → 必然的に任意の値を想定した防御的なプログラミング 命名規則・コメントなどのナイーブな手段しかない function formatDate(x) { let date if(x instanceof Date) { date = x } if(typeof x === "string") { date = new Date(x) } if(typeof x === "number") { date = new Date(x * 1_000) } // ... }

4.

型は事前条件を表明する手段 • この関数はDate型を受け取ることで機能する • 引数の想定範囲を狭める • 関数本来の目的にフォーカスできる // この関数はDate型を期待する function formatDate(date: Date) { // ... }

5.

型は事前条件を表明する手段 • buy関数はプロダクトの配列を期待する。 • 型があることで情報量が増える → 処理内容を読まずとも、 関数の中身が想像できる(意図の明白なインターフェース) function buy(products: any[]) : Promise<OrderDetail> { /*... */ } function buy(products: Product[]): Promise<OrderDetail> { /*... */ }

6.
[beta]
型は事前条件を表明する手段
• buy関数は、購入物が1個以上存在しないとエラー(0個を購入
することはできない)
• 処理内部でバリデーション。0個なら Error throw
function buy(products: Product[]): Promise<OrderDetail> {
if(products.length === 0) {
throw new Error("0件です")
}
/* ... */
}
7.
[beta]
型は事前条件を表明する手段
• buy関数は、購入物が1個以上存在しないとエラー(0個を購入
することはできない)
• 処理内部でバリデーション。0個なら Error throw
function buy(products: Product[]): Promise<OrderDetail> {
if(products.length === 0) {
throw new Error("0件です")
}
/* ... */
}
8.
[beta]
型は事前条件を表明する手段
• buy関数の事前条件:
• ぱっと見 「Productの配列が与えられる」
• 本当は... 「Productの長さ1以上の配列が与えられる」
function buy(products: Product[]): Promise<OrderDetail> {
if(products.length === 0) {
throw new Error("0件です")
}
/* ... */
}
9.
[beta]
型は事前条件を表明する手段
• buy関数の事前条件:
• ぱっと見 「Productの配列が与えられる」
• 本当は... 「Productの長さ1以上の配列が与えられる」
function buy(products: Product[]): Promise<OrderDetail> {
if(products.length === 0) {
throw new Error("0件です")
}
/* ... */
インターフェースに現れていない情報
10.

NonEmptyArray type NonEmptyArray<T> = [T, ...T[]]

11.

NonEmptyArray type NonEmptyArray<T> = [T, ...T[]] 最初のTは必ず存在する

12.

NonEmptyArray type NonEmptyArray<T> = [T, ...T[]] 0以上の要素が続く 最初のTは必ず存在する

13.

NonEmptyArray type NonEmptyArray<T> = [T, ...T[]] 0以上の要素が続く 最初のTは必ず存在する • 1つ以上の要素が必ず存在する配列型を定義できる

14.
[beta]
NonEmptyArray
type NonEmptyArray<T> = [T, ...T[]]
function NonEmptyArray<T>(arr: T[]) {
if(arr.length === 0) {
throw new Error("0件です") }
return arr as NonEmptyArray<T>
}
• NonEmptyArrayを得るためのファクトリ関数を定義
(スマートコンストラクタ)
15.
[beta]
型は事前条件を表明する手段
• buy関数の事前条件:
• ぱっと見 「Productの配列が与えられる」
• 本当は... 「Productの長さ1以上の配列が与えられる」
function buy(products: Product[]): Promise<OrderDetail> {
if(products.length === 0) {
throw new Error("0件です")
}
/* ... */
}
16.

型は事前条件を表明する手段 • buy関数の事前条件: • ぱっと見 「Productの配列が与えられる」 • 本当は... 「Productの長さ1以上の配列が与えられる」 function buy(products: NonEmptyArray<Product>): Promise<OrderDetail> { // バリデーションを丸ごと消せる /* ... */ } NonEmptyArrayを使って 空配列を想定しないことを型レベルで表明

17.

型は事前条件を表明する手段 • buy関数の事前条件: • ぱっと見 「Productの配列が与えられる」 • 本当は... 「Productの長さ1以上の配列が与えられる」 function buy(products: NonEmptyArray<Product>): Promise<OrderDetail> { // バリデーションを丸ごと消せる /* ... */ } バリデーションが不要に

18.

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

19.

全域関数と部分関数 Product[] 型 [Product, Product] [Product, Product, ...] [Product] 空配列型 [] 答えが定まる領域 答えが定まらない領域 (Errorが出る入力パターン)

20.

全域関数と部分関数 Product[] 型 [Product, Product] [Product, Product, ...] [Product] 空配列型 [] 元の実装は、Prodcut[] 全体を 取りうる値として定義していた function buy(products: Product[])

21.

全域関数と部分関数 Product[] 型 [Product, Product] [Product, Product, ...] [Product] 空配列型 [] あとの実装は、答えが存在する範囲に 絞り込んでいた function buy(products: NonEmptyArray<Product>)

22.

なぜ全域関数が嬉しいのか? 全ての情報が型に表出するから(裏を返すと、throw されたエラーは 型に表出しない) • 型に表出すれば、TypeScriptの型システムの恩恵を受けてより堅 牢な静的解析が可能になる。 function buy(products: Product[]) function buy(products: NonEmptyArray<Product>)

23.

なぜ全域関数が嬉しいのか? 先ほどの例は定義域(引数の取りうる値の範囲=型)を狭めることで全 域関数を作る。 逆に、値域(戻り値の範囲=型)を広げることで対応するのが、所謂 Result型と呼ばれるテクニック。 Product[] 型 [Product, Product] [Product, Product, ...] [Product] 空配列型 []

24.
[beta]
Result型
本来であればErrorをthrowするところを、値を返すアプローチ。
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)
}
25.
[beta]
Result型
本来であればErrorをthrowするところを、値を返すアプローチ。
• 入力の範囲を狭めるだけではエラーを防げない(=副作用あり)に使える
• インターフェースにエラーパターンが表出するのは先ほど同様
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)
}
26.

素直なTSの型では表現できない型がたくさんある むしろNonEmptyArrayのように素直に定義できる方が稀かも。 例えば... • 空文字列を許容したくないケース • → 純粋なTypeScriptでは「1文字以上の文字列」を表現できない • 特定のドメインモデルを指定したいケース • → 純粋なTypeScriptでは、パース前のデータとパース後のドメインモ デルを区別できない(構造的部分型)

27.

そんなときには.... Branded Types で解決しよう TSKaigi 2026 Leveragesトラック 30分セッション DAY2 15:10 ~ 15:40 AI時代に考える Branded Typesで実現する堅牢な型付け 池奥裕太/@yuta-ike

28.

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