RawPassword と HashedPassword
例えば Web アプリを作っているなかで、一言「パスワード」と言っても生のパスワードもあればハッシュ化されたものもある。
名前とかに気をつけて実装していれば、そこを取り違える危険はほとんど無いとはいえど、安易に password
とか命名しちゃって「あれ?どっちだっけ?」って迷う事は良くあると思う。
そういった間違いをなくすために type alias 機能を使って RawPassword
と HashedPassword
を作ってみようと思う。
type RawPassword = string;
type HashedPassword = string;
type Password = RawPassword | HashedPassword;
RawPassword
も HashedPassword
もどちらも string 型で、Password
は RawPassword
と HashedPassword
のどちらにもなれる。
これで生かハッシュ化されているかで迷うことはない!
けれども全ては string
結果、次のコンパイルが成功する。
const passwordX: RawPassword = '1qaz2wsx';
const passwordZ: HashedPassword = passwordX;
.....これはまずい。すこぶるまずい。
型が付いて混乱しなくなると思ったら、型が付いてなおさら混乱した。
関数とかで紛れ込まれたらパスワードを平文で保存するサービスができかねない。。。
Structural Typing な TypeScript
なぜこんな事が起きるかというと、TypeScript は Structural Typing という型システムを採用しているから。
他には Go とかも同じ型システムを採用している。
これは「構造を元に型が決まる」というもので、たとえ違う型として定義されていても、構造が同じであれば同じ型として認めるという面白い仕組み。逆に「型を元に構造が決まる」方式を Nominal Typing と言うらしい。こちらの場合は構造が同じでも型が違えば同じ型としては_認めない_。
今回の場合、どちらも構造としては同じ string がベースになっているので、同じ型として扱われていた。
TypeScript で Nominal Typing
皆同じ事を考えるみたいで、TypeScript で Nominal Typing を実現するテクニックはあるらしい。
TypeScript Deep Dive の Nominal Typing という記事を参考に、自分で型を定義してみた。
それがこちら
type NominalTyping<T extends string> = { type: T };
export type RawPassword = NominalTyping<'RawPassword'> & string;
export type HashedPassword = NominalTyping<'HashedPassword'> & string;
export type Password = RawPassword | HashedPassword;
元の記事では & string
という交差型を使うテクニックは利用していなかったために、文字列への/からの変換で一手間かかっていたけど、交差型を使うことで簡単にキャストできるようになった。
const raw: RawPassword = 'rawpassword' as RawPassword;
const hashed: HashedPassword = 'hashedpassword' as HashedPassword;
const rawToString: string = raw;
const hashedToString: string = hashed;
const tryHashed: RawPassword = hashed; // Compile Error
const tryRaw: HashedPassword = raw; // Compile Error
そして記事を書いている途中でまたしても更に優れた記事にぶち当たってしまった。。。
Nominal typing techniques in TypeScript の4番目の内容が一番スッキリしてる。
Nominal Typing って言葉、前にも見かけた気がするけど今回始めてちゃんと覚えた。