ねののお庭。

かりかりもふもふ。

【TypeScript】C# における MemberNotNullWhen に相当する type guard の書き方。

C#er 的には 「TypeScript でも [MemberNotNullWhen] に相当する事をやりたいんだけど、どうするの?」でだいたい伝わるお話。

C#er ではないなら「TypeScript の class のメンバ変数が null でない事を保証するメソッドを呼んだあとに、該当のメンバが null でない事をコンパイラに伝えて、それを型として反映させたいのだけど一体それはどうするの?」です。ながいですね。

以下のような Wrap class を TypeScript で記述したとします。

export class Wrap<T> {

    public constructor(
        public readonly value: T | null,
    ) {
    }

    public readonly hasValue = () => this.value ? true : false;
}

この Wrap class で hasValue を呼び出し、それが true を返したなら、value は当然ながら null ではないので型もそれに合わせて null の可能性を排除して欲しいわけです。 しかしコンパイラはそこまでのフロー解析はしないので、以下のようになります。

const obj = new Wrap(99);

// hasValue が true を返すなら obj.value は null でないハズ...
if (obj.hasValue()) {
    // ↓ コンパイルは通るが、null との union は嬉しくない。
    const v1: number | null = obj.value;
    // ↓ hasValue を呼んだところで、value が 非 null かコンパイラは知らないので、number だけだとエラーが出る。
    const v2: number = obj.value;
}

VSCode のスクショの方が分かりやすいかな?赤い波線が出ちゃっています。

ということで、これが大変不便なのでどうにかしたい。

C# だと以下のように [MemberNotNullWhen] という属性を使うことで実現できます。

var obj = new Wrap<string>("nana");

if (obj.HasValue())
{
    // HasValue() に [MemberNotNullWhen(true, nameof(Value))] がついて無かったら
    // Value は null の可能性があるぞ!って怒ってくる。
    var e = obj.Value.ToString();
}

// C#
class Wrap<T>
{
    public T? Value { get; }

    public Wrap(T? value)
    {
        this.Value = value;
    }

    [MemberNotNullWhen(true, nameof(Value))]
    public bool HasValue() => this.Value is not null;
}

C# と TypeScript は共にフロー解析ベースの null 安全な言語です。 C# の場合は [MemberNotNullWhen] 属性を使うことで、コンパイラにこの条件の時はこのメンバは null の可能性はないよ、というのを伝えることが出来ます。

では TypeScript は? TypeScript ではこれを以下のような is キーワードを用いた type guard で実現します。

export class Wrap<T> {

    public constructor(
        public readonly value: T | null,
    ) {
    }

    // this is { value: T } <- これを追加
    public readonly hasValue = (): this is { value: T } => this.value ? true : false;
}

this is { value: T } を追加した事により、hasValuetrue を返した場合は value が null でない事がコンパイラに伝わるようになり、所望の書き心地を得ることが出来ます。

const obj = new Wrap(99);

if (obj.hasValue()) {
    // ↓ 問題なくコンパイルが通る
    const v2: number = obj.value;
}

TypeScript において is キーワードは user-defined type guard で使われる代物。 公式のドキュメントには関数の引数に対する書き方しか乗ってないのですが、実は class のメソッドでは上記のような形で is キーワードを使うことでメンバ変数に対する type guard を実現できるのでした。 めでたしめでたし。