2つのnullableなプロパティが同時にnullにならない場合の型定義


TypeScriptの型定義に関する質問です。

ここに両者ともnullableなプロパティを持ったクラスがあったとします。

[code]

    class Example {
      foo: string | null;
      bar: string | null;
    }
    
[/code]

このクラスでは、`foo` と `bar` が同時に `null` になることはなく、また逆に同時に `string` 型になることはあるとします。

しかしこの型定義だと、例えば以下のようなコードを記述すると当然 `string` 型に確定することはなく、エラーとなります。

[code]

    const str: string = <Example>.foo ?? <Example>.bar;
    
[/code]

この問題を解決するために、両プロパティがnullableでありつつも、同時に `null` とはならない型を定義したいのですが、解決方法が見つけられません。

もちろん、どちらかがnullableではない新しいクラスを2つ用意し、`is～`
のような型アサーション関数(?)でクラスを振り分ける方法もあるかと思いますが、できれば他の方法で実現できればと思っています。

よろしくお願い致します。




同じ意図の質問が

  * [Check that at least one interface member is non-null in typescript](https://stackoverflow.com/q/57572437/4506703)

にあるようです。

また、型エイリアスが利用可能であれば、

  * [How to create a Partial-like that requires a single property to be set](https://stackoverflow.com/a/48244432/4506703)

の回答がスマートかと考えます。

(TypeScriptに習熟していないのでより良い書き方があるかも知れませんが、)こんな感じになるかと思います:

[code]

    type Draft<T> = { [K in keyof T]: T[K] | null };
    type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Draft<T> & U[keyof U];
    
    type FullExampleType = {
      foo: string;
      bar: string;
    };
    
    type ExampleType = AtLeastOne<FullExampleType>;
    
    const v1: ExampleType = { foo: "foo_val", bar: "bar_val" }; // OK
    const v2: ExampleType = { foo: "foo_val", bar: null }; // OK
    const v3: ExampleType = { foo: null, bar: "bar_val" }; // OK
    const v4: ExampleType = { foo: null, bar: null }; // NG
    
[/code]

([Playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAIgTgQwGbADwBUB8UC8UDeUA2gNJQCWAdlANYQgD2SU6AugFwumtQA+UlAK4AbYVAC+AbgBQoSFACCwADIQEAZ2AB5ShAwAaKAFVcBYmSq16TFhygAFcgGMaBqCWzjseeMjRYoADJjIjpGZiNWGVlwaAAxEWEAUQAPBABbMGEIdFjTfGkoKCQGBk5NOCoAcxkigCMEOHLgSsoa6SlpGPlUjKycvLwlVQ1tXVQE0V7M7NzITGinBkpNKAA3AEZOaf656DxCErKoACIjgH01hGETwwam0-vL65OJSSgAeg+oLRJpJZWwHWACZtmkZgN5AdiqVOGdSs8bndGpwhKI3p9vr9-stVmsAMxgvqzQZmI6oxLIh4nJ5XG4Yr4-P4AvEAFiJEL2+RhxzRwipFPRUkxUAAcgBxaRAA))

* * *

コメントにある、 "他にもプロパティやメソッドが実装されている"
については、[交差(intersection)](https://www.typescriptlang.org/docs/handbook/2/everyday-
types.html#differences-between-type-aliases-and-interfaces)で対応できるかと考えます。

[code]

    type AllProps = {
      baz: string;
    } & ExampleType;
    
    const v5: AllProps = { foo: null, bar: null, baz: "baz_val" }; // NG
    
[/code]




同じ意図の質問が

  * [Check that at least one interface member is non-null in typescript](https://stackoverflow.com/q/57572437/4506703)

にあるようです。

また、型エイリアスが利用可能であれば、

  * [How to create a Partial-like that requires a single property to be set](https://stackoverflow.com/a/48244432/4506703)

の回答がスマートかと考えます。

(TypeScriptに習熟していないのでより良い書き方があるかも知れませんが、)こんな感じになるかと思います:

[code]

    type Draft<T> = { [K in keyof T]: T[K] | null };
    type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Draft<T> & U[keyof U];
    
    type FullExampleType = {
      foo: string;
      bar: string;
    };
    
    type ExampleType = AtLeastOne<FullExampleType>;
    
    const v1: ExampleType = { foo: "foo_val", bar: "bar_val" }; // OK
    const v2: ExampleType = { foo: "foo_val", bar: null }; // OK
    const v3: ExampleType = { foo: null, bar: "bar_val" }; // OK
    const v4: ExampleType = { foo: null, bar: null }; // NG
    
[/code]

([Playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAIgTgQwGbADwBUB8UC8UDeUA2gNJQCWAdlANYQgD2SU6AugFwumtQA+UlAK4AbYVAC+AbgBQoSFACCwADIQEAZ2AB5ShAwAaKAFVcBYmSq16TFhygAFcgGMaBqCWzjseeMjRYoADJjIjpGZiNWGVlwaAAxEWEAUQAPBABbMGEIdFjTfGkoKCQGBk5NOCoAcxkigCMEOHLgSsoa6SlpGPlUjKycvLwlVQ1tXVQE0V7M7NzITGinBkpNKAA3AEZOaf656DxCErKoACIjgH01hGETwwam0-vL65OJSSgAeg+oLRJpJZWwHWACZtmkZgN5AdiqVOGdSs8bndGpwhKI3p9vr9-stVmsAMxgvqzQZmI6oxLIh4nJ5XG4Yr4-P4AvEAFiJEL2+RhxzRwipFPRUkxUAAcgBxaRAA))

* * *

コメントにある、 "他にもプロパティやメソッドが実装されている"
については、[交差(intersection)](https://www.typescriptlang.org/docs/handbook/2/everyday-
types.html#differences-between-type-aliases-and-interfaces)で対応できるかと考えます。

[code]

    type AllProps = {
      baz: string;
    } & ExampleType;
    
    const v5: AllProps = { foo: null, bar: null, baz: "baz_val" }; // NG
    
[/code]

