型の表現にこだわってコードに語らせる
この記事は「Medley (メドレー) Advent Calendar 2025」の18日目の記事です。
はじめに
みなさんは型にこだわっていますか?
私はコードを書く時、読む時、常に型のことを考えています。
この記事では、そんな風に型にこだわっているとコードが沢山のことを語り出すということを書いてみます。
なお、私は静的型付け信奉者であるため本稿では型をコード上で記述できない言語のことは一切考慮していないのでご了承ください。
型で何を表現するか
ご存知の通り、プログラミングにおける "型" は単にデータの種類やビット幅を表すだけにはとどまりません。
使用する言語の型システムに依存しますが、様々な情報を付与し文脈や意図を表現することができます。
では何を表現しましょうか?
私はなんでも表現して良いと思っています。
「コードは書く時間より読まれる時間の方が長い」とはよく言ったもので、AIであれ人間であれコードは何度も読まれるものですから、読み手に伝えたいことを沢山コードに語らせましょう。
いくつか私が普段の業務でも比較的よく使う表現について、例を用いて書いていきます。
私が普段の業務で使用していることと世間的にも利用者が多いであろうことを考慮して、サンプルコードはTypeScriptで記述します。
いろんなことを表現してみる
「仕様」を表現する
まずはこんなReactのコードを考えてみます。
このコンポーネントは商品を表示しますが、商品に付帯する情報によっていくつか表示の分岐があるようです。
type Props = {
商品名: string;
価格: number;
セール価格?: number;
軽減税率対象フラグ: boolean;
関連商品?: { id: string; 商品名: string }[];
};
export const 商品: FC<Props> = (props) => {
return (
<>
<div>{props.商品名}</div>
{props.軽減税率対象フラグ ? (
<軽減税率ラベル 価格={props.セール価格 ?? props.価格} />
) : props.セール価格 ? (
<セール価格ラベル 価格={props.セール価格} />
) : (
<価格ラベル 価格={props.価格} />
)}
{!props.セール価格 && props.関連商品 && <関連商品リスト 商品リスト={props.関連商品} />}
</>
);
};
ぱっと見ではどんな商品の場合にどんな表示になるかよく分かりませんね。
パターンを表現する
この商品コンポーネントの表示仕様は
- 軽減税率対象の場合、専用の価格ラベルで表示する
- 軽減税率対象ではない場合(標準税率)、通常価格・セール価格それぞれ専用のラベルで表示する
- 関連商品があればそのリストを表示する
- セール価格商品の場合は関連商品があっても表示しない
となっています。
ではこの表示パターンに関する仕様を型で表現するべく、表示パターンの型と判定関数を作ってみましょう。
type 商品表示パターン =
| '標準税率_通常価格'
| '標準税率_セール価格'
| '軽減税率_通常価格'
| '軽減税率_セール価格';
const 商品表示パターン判定 = (props: Props): 商品表示パターン => {
if (props.軽減税率対象フラグ) {
return props.セール価格
? '軽減税率_セール価格'
: '軽減税率_通常価格';
}
return props.セール価格 ? '標準税率_セール価格' : '標準税率_通常価格';
};
コンポーネント側はこれを使って以下のように記述できます。
export const 商品: FC<Props> = (props) => {
const 商品パターン = 商品表示パターン判定(props);
switch (商品パターン) {
case '標準税率_通常価格':
return (
<>
<div>{props.商品名}</div>
<価格ラベル 価格={props.価格} />
{props.関連商品 && <関連商品リスト 商品リスト={props.関連商品} />}
</>
);
case '標準税率_セール価格':
return (
<>
<div>{props.商品名}</div>
<セール価格ラベル 価格={props.セール価格} />
</>
);
//...
default: {
const never: never = 商品パターン;
return never;
}
}
};
表示のパターンに関する識別子をunion typeで定義し、コンポーネント内のswitch文によりexhaustive checkが可能になったことで商品コンポーネントは4つの表示パターンのみがあり得るということをよく表してくれていますね。
また、型で表現しようと整理したことで判定ロジックとUIが自然と分離されています。
より厳密に絞り込む
上記の実装では、実際には不十分です。
//...
case '標準税率_通常価格':
return (
<>
<div>{props.商品名}</div>
{/* 「props.セール価格」はoptionalなので価格がnumberを期待する場合型ガードが必要になる */}
<セール価格ラベル 価格={props.セール価格} />
</>
);
//...
そこで、先ほどのパターン判定と同時に商品オブジェクトの型も正確に表現してみましょう。
まずは各パターンの商品の型を定義します
type 標準税率_通常価格商品 = {
商品名: string;
価格: number;
軽減税率対象フラグ: false;
関連商品?: { id: string; 商品名: string }[];
}
type 軽減税率_セール価格商品 = {
商品名: string;
セール価格: number;
軽減税率対象フラグ: true;
}
// 他パターンも同様に定義
// ...
次に商品表示パターン判定関数では識別子とともに各パターンのオブジェクト型を返します。
type 商品表示パターン =
| ({ type: '標準税率_通常価格' } & 標準税率_通常価格商品)
| ({ type: '標準税率_セール価格' } & 標準税率_セール価格商品)
| ({ type: '軽減税率対象_通常価格' } & 軽減税率_通常価格商品)
| ({ type: '軽減税率対象_セール価格' } & 軽減税率_セール価格商品);
const 商品表示パターン判定 = (props: Props): 商品表示パターン => {
if (props.軽減税率対象フラグ) {
return props.セール価格
? {
type: '軽減税率_セール価格商品',
商品名: props.商品名,
セール価格: props.セール価格,
軽減税率対象フラグ: true,
}
: {
type: '軽減税率_通常価格商品',
商品名: props.商品名,
価格: props.価格,
軽減税率対象フラグ: true,
関連商品: props.関連商品,
};
}
//...
}
最後にコンポーネント側では判定関数から返ってくる判定済みのオブジェクトを使用します
export const 商品: FC<Props> = (props) => {
//...
const 判定済み商品 = 商品表示パターン判定(props);
switch (判定済み商品.type) {
case '標準税率_通常価格商品':
return (
<>
<div>{判定済み商品.商品名}</div>
<価格ラベル 価格={判定済み商品.価格} />
{判定済み商品.関連商品 && (
<関連商品リスト 商品リスト={判定済み商品.関連商品} />
)}
</>
);
//...
}
}
コード量は少々多くなりましたが、その分「どのようなパターンを取り扱うか」という仕様の詳細が型とともに厳密に表現されるようになりました。
「前提」を表現する
その関数がなんらかの前提の元に成り立っているようなことを表現してみましょう。
所謂 "文脈" というやつですかね。
例として与えられた商品の値段に手数料を加えたあと税率をかけた数値を支払い金額としてDBに保存する一連の処理を考えます。
まずは最低限の型アノテーションのみで書いてみます。
// 手数料.ts
export const 手数料を加える = (値段: number):number => {
return 値段 + 手数料
}
// 税金.ts
export const 税率をかける = (値段: number):number => {
return 値段 * 税率
}
// 支払い情報.ts
export const 支払い金額を保存 = async(値段: number):Promise<void> => {
// SQLとかでinsertなどする
}
簡易なコードですが、このような記述の場合支払い情報.tsだけを読んだ人は引数で受け取る値段は一体どんな数字なのか分かりませんね。
きっとこの機能を実装した人は「支払い金額は手数料を足した値段に税率を掛けたものだ」という前提知識を持っていて、そのつもりで書いたのでしょう。
しかしこのままでは税率を掛けずに支払い金額を保存してしまったり、手数料・税率の足し算と掛け算の順番が入れ替わってしまうなど様々な実装ミスが起き得てしまいそうです。
この一連の処理には順番があり、それぞれの処理は前の処理が行われているという前提があるので、それらをなんとか型で表現するべく、branded typeを使って書いてみましょう。
// 手数料.ts
declare const 手数料込みBrand: unique symbol;
export type 手数料込み価格 = number & { [手数料込みBrand]: never };
export const 手数料を加える = (値段: number):手数料込み価格 => {
return (値段 + 手数料) as 手数料込み価格
}
// 税金.ts
declare const 税込Brand: unique symbol;
export type 税込価格 = number & { [税込Brand]: never };
export const 税率をかける = (値段: 手数料込み価格):税込価格 => {
return (値段 * 税率) as 税込価格
}
// 支払い情報.ts
export const 支払い金額を保存 = async(値段: 税込価格):Promise<void> => {
// SQLとかでinsertなどする
}
このようにすると、支払い金額を保存関数が「私は税込価格を保存します」と自ら宣言してくれるようになりましたね。
税率をかける関数においても、「手数料を足してから税率をかけるよ」ということが引数の型からわかります。
各関数のインターフェースを見るだけで手数料を加える -> 税率をかける -> 支払い金額として保存するという処理フローが理解できるような形になりました。
このようにしておけば、「誤って手数料を足し忘れる」「税率を掛け忘れる」「税込価格に手数料を足してしまった」のような実装ミスも不用意にダウンキャストするような不届きものがいない限り 型レベルで防げて嬉しいですね。
番外
あえて網羅しない
最初の例の商品表示パターンの例ですが、実は あり得る全てのパターンを網羅しているわけではありません。
もし、分岐し得るすべてのパターンを網羅する場合はこのようになります。
// 関連商品有無が加わる
type 商品表示パターン =
| '標準税率_通常価格_関連商品あり'
| '標準税率_通常価格_関連商品なし'
| '標準税率_セール価格'
| '軽減税率_通常価格_関連商品あり'
| '軽減税率_通常価格_関連商品なし'
| '軽減税率_セール価格';
しかし実際のところ関連商品はあれば表示するだけで良く、サンプルに使用したReactでは短絡評価で簡潔に記述できます。
{props.関連商品 && <関連商品リスト 商品リスト={props.関連商品} />}
「関連商品がなければ当然表示されないよね」というのはわざわざ型で表現せずとも上記の記述だけで十分に伝わるため今回のサンプルコードではパターンを分けませんでした。
これはパターンの多さや複雑さにもよるのですが、MECE的にすべての分岐パターンを網羅して型に落とし込めば良いというものではなく、判定した結果のパターンが多すぎると返って把握しづらくなることもあると考えています。
コード上の表現としてどの程度の抽象度で表したいかということを念頭において考えていきましょう。
緩い型を用いる
2つ目の例の branded type のような表現は「より厳密な型に制限する」ような手法です。
一方で、それほど厳密にする必要がなくむしろ緩い型であるべきシーンもあります。
例えば文章から空白文字を削除してDBに保存するようなケースを考えてみましょう。
保存する時にはフォーマット済みであることを保証するように前述のような表現が有効かもしれません。
declare const フォーマット済みBrand: unique symbol;
export type フォーマット済み文章 = string & { [フォーマット済みBrand]: never };
const 文章をフォーマットする = (text: string): フォーマット済み文章 => {
//...
}
const 文章を保存する = (text: フォーマット済み文章) => {
//...
}
一方で取得する場合にはそのような厳密な型を用いると困る場合があります。
const 文章を取得する = (): フォーマット済み文章 => {
const DBの値: string = //selectなどする
// 元々文字列なので文章をフォーマットする関数は使えない。どうしよう(>_<)
}
対処として、フォーマットのバリデーションを行ってbranded typeにアサーションする関数を用意出来るかもしれません。
const 文章のフォーマットを検証する = (date:string): フォーマット済み文章 => {
//バリデーションして成功したらアサーションして返す
}
const 文章を取得する = (): フォーマット済み文章 => {
const DBの値: string = //selectなどする
return 文章のフォーマットを検証する(DBの値)
}
しかしこのバリデーション、本当に必要でしょうか?
バリデーションに失敗した場合はどう扱うのでしょうか。
値の性質によっては検証すべき場合もあるでしょうが、ここはあえてスルーした方が嬉しいケースもあるでしょう。
今回の例で考えると、多少フォーマットが誤っていても他の要素と比較して重要度が低いので全体としては正常に処理したいというモチベーションはありそうです。
そのような時はあえて曖昧なstringのまま扱うことで「ここではフォーマットは気にしてませんよ」と表現する選択肢もあります。
const 文章を取得する = (): string => {
const DBの値: string = //selectなどする
return DBの値
}
まとめ
型の表現をちょっと工夫してみよう。という内容を書いてきました。
今回は実装が簡単かつ有用な場面が比較的多いであろう表現方法を選んで例に挙げてみました。
実際にはケースバイケースでもっと工夫が必要だったり、コメントなどで情報を付与した方が効率的な場面もあると思うので選択肢の一つとして覚えておくと良いでしょう。
個人的には小難しいデザインパターンを覚えたり原則云々を語るよりも(これらも大事ですが)、型の表現と「そのコードがそのように振る舞う理由」の2点にこだわって考えた方が壊れづらく読みやすいプログラムを作ることに直結すると考えています。後者の方もまたの機会に記事にしようと思います。
本稿で記載した例の他にも、型を介して様々な意思表示やコードの整理が出来ると思うので皆さんもぜひ型による表現にこだわっていきましょう。
最後に
メドレーでは様々な職種で人材を募集しているので、興味がある方はぜひお問い合わせください。
Medley Advent Calendar 2025、明日 19日目の担当は@sishoさんです!
Discussion