85361


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～`
のような型アサーション関数(?)でクラスを振り分ける方法もあるかと思いますが、できれば他の方法で実現できればと思っています。

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


CC BY-SA 4.0
2021-12-27T17:01:42.830
85359
2022-02-26T01:52:38.250
34559
question
typescript
2つのnullableなプロパティが同時にnullにならない場合の型定義


同じ意図の質問が

  * [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]


CC BY-SA 4.0
2021-12-27T21:29:47.217
85361
2022-02-26T01:52:38.250
2022-02-26T01:52:38.250
2808
2808
85359
answer


classの2つのプロパティを型レベルで制御するのは（おそらく）至難の業なので、オブジェクトのプロパティとして持つ方法として１つ紹介します。

[Playground](https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBDAnmYcBiEIHkB2AbROAXjgG8AoOOAM0wC44BnGKASxwHMBuSuAIwCGUBjgCuePDwC+PcqEiwEyVACEhuAsTK9aEEeMm9Bwpi3bdyM8nPDR4SFOkxqoWilV0NmbTjyrGvM19LWXk7JUdcYCxqLQxsfEIAHzgXDWSnCBdQ20UAYzwBRkY4AFEQAQBbMDxUdzgwUT48VjzTARhgBiiYt14qD3o4MQkAGn6BgLgAImNp8aorKWs8iBxmOHZmARw81BIcYAB3Morq2oAKAEpZVliLrZgdvYA6bc6X42IDgyvtAc26yeu2AbyeH10cAA9FDTD4OJZrHc4A8gc9Qe9QZCiDjhr9-gNHuiwR1QV8YXDzIjyMjUdsQSSPl8cT8JHAAGTswH016Yl7Y3EjPB-
epUIkMvnk2GHABuwCgE3FvPBWMw0OlwDlCuWQA)

[code]

    export type FooOnly = {
      foo: string;
      bar: null;
    };
    
    export type BarOnly = {
      foo: null;
      bar: string;
    };
    
    export type FooBar = {
      foo: string;
      bar: string;
    };
    
    export type OneOf = FooOnly | BarOnly | FooBar;
    
    export class Example {
      public state: OneOf = {
        foo: null,
        bar: "bar",
      };
    }
    
    const instance = new Example();
    
    if (instance.state.bar == null) {
      instance.state.foo; // string
    }
    
    if (instance.state.foo === null) {
      instance.state.bar; // string
    }
    
    if (instance.state.bar === null && instance.state.foo === null) {
      instance.state.bar; // never
      instance.state.foo; // never
    }
    
[/code]

classのmember変数としてバラバラに持つと、型レベルで制御できない部分（内部参照など）で値を変更できてしまうため、型の恩恵を受けることができなくなります。


CC BY-SA 4.0
2021-12-28T00:58:57.843
85362
2021-12-28T00:58:57.843
7997
85359
answer