null に想いを込めたコードベースでその想いが込められたものだけ区別するためにブランド型にできないか試したのですが、できないようです
まず、基本的なブランド型の使い方を Branded Type ベストプラクティス 検索 #TypeScript - Qiita で確認してみます
const userIdBrand = Symbol();
const postIdBrand = Symbol();
type UserId = string & { [userIdBrand]: unknown };
type PostId = string & { [postIdBrand]: unknown };
// 説明用にUserIdを作る
const myId = "uhyo" as UserId;
// これは型エラーになる
const postId: PostId = myId;
これを string ではなく null に適用してみます
const PoBrand = Symbol();
const GaBrand = Symbol();
type Po = null & { [PoBrand]: unknown };
type Ga = null & { [GaBrand]: unknown };
// Po と Ga の型は別のブランド型だがエラーなく代入できる
const po = null as Po;
const nullPo: Ga = po;
この理由は単純で null & {} が never と判断されるためです。そのため Po と Ga の型はどちらも never となり相互に代入が可能になります
ではなぜこのように変換されてしまうのでしょうか?
その前になぜ string & {} が有効な記述なのかを考えてみます。これはシンプルに String が Object を継承しているからですね
const str = new String()
str instanceof String // true
str instanceof Object // true
const obj = new Object()
obj instanceof String // false
obj instanceof Object // true
そのため、プロトタイプ拡張が可能でその場合の方の表現が string & {} になるわけです
String.prototype.hoge = 'hoge'
console.log(''.hoge) // 'hoge' が出力される
// 上記のコードから実際に推論されるわけではないが、この定義と等しい
type HogeString = string & { hoge: 'hoge' }
これに対して null は Object を継承しておらず当然のことながらプロトタイプ拡張はできません
null instanceof Object // false
null.prototype.hoge = 'hoge' // エラーになる
ということで null & {} は発生することがあり得ず、そのためあり得ないを表現する never 型になる、というわけでした
学び
とりあえず、 null に想いを込めるのはやめましょう
おまけ
null は Object を継承していないんですが、 typeof で確認するとなぜか 'object' になるみたいです。面白いですね