😃

TypeScript で名前的型付け

2024/03/03に公開

名前的型付けっぽいことができると便利なときがあります。(何番煎じだよって感じですが)


const findById = (id: UserId) => {
    ...
}

const id: TenantId = 'hogee';

// 型エラーになってくれたらチョットうれしい
findById(id);

このコードを見た人の中には「UserId を期待してるところに TenantId を渡すなんてあり得ないでしょ」と思う人もいるかもしれません。

でも出来ればそんな可能性は無くしてしまった方が良いですよね? コンパイラができることを人間がやる必要は無いと思います。コンパイラの方が速いし正確です。それに対して人間は遅いし不正確です。
やらかす可能性が0とは言えません。

そして人間には型チェック以外の重要なタスクが沢山あるはずです。

とはいえコンパイラにチェックさせるために沢山のコードを書くのは避けたいところです。
仕事が増えてしまっては意味がありません。

できるだけ簡単にサクッと書ける必要があるわけです。
そこで提案なんですがこんなのはどうでしょうか。

userId.ts
declare const brand: unique symbol;

export type UserId = string & { [brand]: 'userId' };
export const UserId = {
  create: (value: unknown) => {
    // 何かバリデーション
    return value as UserId;
  },
} as const;

型の区別に使うプロパティの名前は Computed property names を使って unique symbol にします。こうするとコード補完の候補にプロパティ名が表示されなくなります。

値の生成は型と同じ名前のオブジェクトにファクトリメソッドを生やしてそこで行うようにします。
ファクトリメソッドを使うときは型を import して型名の後ろで . を打つだけです。頑張ってファクトリメソッドの名前を思い出す必要はありません。エディタが教えてくれます。

index.ts
import { UserId } from './userId';

const id = UserId.create('hungaa');

[brand] の値はプログラム実行時には存在しません。なのでシリアライズの邪魔をしません。
クラスインスタンスに詰めたりするとシリアライズ/デシリアライズが面倒(or 複雑)になりがちなのでプリミティブやプレーンオブジェクトを使うようにするのがオススメです。

index.ts
import { UserId } from './userId';

const id = UserId.create('hungaa');
console.log(JSON.stringify(id));
// => "hungaa"

以上、 TypeScript で名前的型付けっぽいことするならこんな方法はどうでしょーというお話でした 😃

Discussion