typing.NewTypeは新規コードでも有効な選択肢だ
導入
2025/09現在、Python NewTypeでGoogle検索すると、「NewTypeをこれから書くコードでは使わないほうがいい」という記事がトップでヒットします。記事では bake() メソッドの例を通じて「ランタイムで気付けないエラーが発生する」「NewTypeを使うより設計を見直すべき」といった指摘がされています。
以下元記事より抜粋
import enum
from typing import NewType
class Doneness(enum.Enum):
RARE = enum.auto()
MEDIUM = enum.auto()
WELL = enum.auto()
class Mochocho:
def __init__(self, batter: Batter, filling: Filling) -> None:
self.batter = batter
self.filling = filling
def bake(self, doneness: Doneness) -> "BakedMochocho":
self.doneness = doneness
return self
def __repr__(self) -> str:
return f"{type(self).__name__}(doneness={self.doneness})"
BakedMochocho = NewType("BakedMochocho", Mochocho)
確かに、設計段階で未完成のオブジェクトを作らせないようにすれば多くのバグは防げます。
しかし私は、これをNewType自体の欠点ではなく、元記事でも書かれているようにコンストラクタや設計自体の誤りが問題だと捉えます。これを理由に「新規コードで NewType を避けるべき」とまで言うのは行き過ぎだと考えています。
この記事では私がこれから書くコードでもNewTypeを使っていきたい理由を挙げてみます。
NewTypeを積極的に使いたい理由
軽量な型安全性の検証方法になる
NewType はランタイムでの保証をしません。
一方クラスや関数を使った検証はランタイムでエラーを出すことができます。
それではNewTypeは使えないかというとそうではなく、ゼロランタイムコストで型安全性と可読性を向上させられるメリットがあります。
ランタイムではありませんが、CI/CDなどでmypyによる型チェックを実行することでデプロイ前に型エラーに気づくこともできます。
typingのドキュメントにも書かれている通り、論理的な誤りを最小の実行コストで防げることがNewTypeの利点です。
NewType はある型をもう一方の型の サブタイプ として宣言します。 Derived = NewType('Derived', Original) とすると静的型検査器は Derived を Original の サブクラス として扱います。つまり Original 型の値は Derived 型の値が期待される場所で使うことが出来ないということです。これは論理的な誤りを最小の実行時のコストで防ぎたい時に有用です。
型レベルで設計を明示化できる
UserId = NewType("UserId", int) と書くだけで、int と UserId の混同を型レベルで防げます。これにより設計上の値の区別を型レベルで表現することができます。
いわゆる「値オブジェクト」のパターンです。
レビューや補完時に「ここはただの int ではなくユーザーIDだ」とすぐに分かるのは、新規コードでも十分な価値があります。
状態遷移の表現に使える
「Unvalidated → Validated」「RawJson → Parsed」など、状態遷移を伴う処理に NewType を導入すれば、順序ミスを静的に防げます。
書籍「関数型ドメインモデリング」で紹介されているようなメールアドレスの検証も、NewTypeを使うことで簡潔に書くことができます。
from typing import NewType
UnverifiedEmail = NewType('UnverifiedEmail', str)
VerifiedEmail = NewType('VerifiedEmail', str)
def validateEmail(email: UnverifiedEmail) -> VerifiedEmail:
...
このような、Always-vaild domain modelの考え方を実装するのに適しており、これは新規コードでもNewTypeを使いたくなる理由です。
具体的なユースケース
以下のようなドメインモデルを表現するのにNewTypeは有用と考えます。
- ID混同防止:UserId, OrderId など
- 通貨や単位:JPY, USD, Cents など
- サニタイズ済み文字列:SafeHTML, SqlIdentifier
- 検証済みデータ:ValidatedPayload, JwtToken
特にロジックを持たないプリミティブ型をラップするのには便利です。
これらは「設計をきちんとした上でさらに境界を守る」ための強力な手段です。
結論
- NewType はゼロランタイムコストで型を区別できる
- 軽量に導入できるため、新規コードでも現実的に「型の取り違い」を防ぐ手段として役に立つ
- IDや通貨、検証済みデータなど、混同や誤用を防ぎたいユースケースで可読性・安全性を大きく高められる
- 「設計上の区別を簡単に型で表現する」ことができる
したがって、NewType は新規コードでも積極的に使う価値があると結論付けます。
Discussion