web-dev-qa-db-ja.com

ジェネリック型の識別されたユニオン

ジェネリックで nion discrimination を使用できるようにしたいと思います。ただし、機能していないようです。

コード例( TypeScriptプレイグラウンドのビュー)

interface Foo{
    type: 'foo';
    fooProp: string
}

interface Bar{
    type: 'bar'
    barProp: number
}

interface GenericThing<T> {
    item: T;
}


let func = (genericThing: GenericThing<Foo | Bar>) => {
    if (genericThing.item.type === 'foo') {

        genericThing.item.fooProp; // this works, but type of genericThing is still GenericThing<Foo | Bar>

        let fooThing = genericThing;
        fooThing.item.fooProp; //error!
    }
}

TypeScriptが、汎用のitemプロパティを区別したため、genericThingGenericThing<Foo>

これはサポートされていないのでしょうか?

また、直接割り当てた後、それがfooThing.itemそれは差別を失います。

14
NSjonas

問題

差別化された共用体での型の絞り込みには、いくつかの制限があります。

ジェネリックのアンラップなし

まず、タイプがジェネリックの場合、ジェネリックはアンラップされてタイプを絞り込みません。ナローイングが機能するにはユニオンが必要です。したがって、たとえばこれは機能しません:

let func = (genericThing:  GenericThing<'foo' | 'bar'>) => {
    switch (genericThing.item) {
        case 'foo':
            genericThing; // still GenericThing<'foo' | 'bar'>
            break;
        case 'bar':
            genericThing; // still GenericThing<'foo' | 'bar'>
            break;
    }
}

これはしますが:

let func = (genericThing: GenericThing<'foo'> | GenericThing<'bar'>) => {
    switch (genericThing.item) {
        case 'foo':
            genericThing; // now GenericThing<'foo'> !
            break;
        case 'bar':
            genericThing; // now  GenericThing<'bar'> !
            break;
    }
}

Union型の引数を持つジェネリック型をラップ解除すると、コンパイラチームが満足のいく方法で解決できないあらゆる種類の奇妙なコーナーケースが発生するのではないかと思います。

ネストされたプロパティによる絞り込みなし

タイプの和集合がある場合でも、ネストされたプロパティでテストする場合、ナローイングは発生しません。テストに基づいてフィールドタイプを絞り込むことができますが、ルートオブジェクトは絞り込まれません。

let func = (genericThing: GenericThing<{ type: 'foo' }> | GenericThing<{ type: 'bar' }>) => {
    switch (genericThing.item.type) {
        case 'foo':
            genericThing; // still GenericThing<{ type: 'foo' }> | GenericThing<{ type: 'bar' }>)
            genericThing.item // but this is { type: 'foo' } !
            break;
        case 'bar':
            genericThing;  // still GenericThing<{ type: 'foo' }> | GenericThing<{ type: 'bar' }>)
            genericThing.item // but this is { type: 'bar' } !
            break;
    }
}

ソリューション

解決策は、カスタムタイプガードを使用することです。 typeフィールドを持つ任意の型パラメーターで機能する、かなり一般的なバージョンの型ガードを作成できます。残念ながら、GenericThingに関連付けられるため、ジェネリック型には使用できません。

function isOfType<T extends { type: any }, TValue extends string>(
  genericThing: GenericThing<T>,
  type: TValue
): genericThing is GenericThing<Extract<T, { type: TValue }>> {
  return genericThing.item.type === type;
}

let func = (genericThing: GenericThing<Foo | Bar>) => {
  if (isOfType(genericThing, "foo")) {
    genericThing.item.fooProp;

    let fooThing = genericThing;
    fooThing.item.fooProp;
  }
};

genericThing.itemFooブロック内でifとして表示されるのは良い点です。変数(const item = genericThing.item)に抽出した後にのみ機能すると思いました。おそらくTSの最新バージョンの動作が改善されています。

これにより、 Discriminated Unions の公式ドキュメントの関数areaのようなパターンマッチングが有効になり、C#には実際にありません(v7では、defaultのケースはまだこのようなswitchステートメントで必要です)。

実際、奇妙なのは、genericThingブロック内でifitemが無差別に(GenericThing<Foo | Bar>ではなくGenericThing<Foo>として)見られることです。 _はFooです!その場合、fooThing.item.fooProp;のエラーは私を驚かせません。

TypeScriptチームには、この状況をサポートするためにまだ改善すべき点があると思います。

0
Romain Deneau