web-dev-qa-db-fra.com

Union discriminée de type générique

Je voudrais pouvoir utiliser discrimination syndicale avec un générique. Cependant, cela ne semble pas fonctionner:

Exemple de code ( vue sur le terrain de jeu 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!
    }
}

J'espérais que TypeScript reconnaîtrait que puisque j'ai discriminé la propriété générique item, que genericThing doit être GenericThing<Foo>.

Je suppose que cela n'est tout simplement pas pris en charge?

Aussi, un peu bizarre qu'après une affectation directe, il fooThing.item perd sa discrimination.

14
NSjonas

Le problème

Le rétrécissement du type dans les syndicats discriminés est soumis à plusieurs restrictions:

Pas de déballage des génériques

Premièrement, si le type est générique, le générique ne sera pas déballé pour restreindre un type: le rétrécissement a besoin d'une union pour fonctionner. Ainsi, par exemple, cela ne fonctionne pas:

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;
    }
}

Bien que cela:

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

Je soupçonne que le déballage d'un type générique comportant un argument de type union entraînerait toutes sortes d'étranges cas de coin que l'équipe du compilateur ne peut pas résoudre de manière satisfaisante.

Pas de rétrécissement par les propriétés imbriquées

Même si nous avons une union de types, aucun rétrécissement ne se produira si nous testons sur une propriété imbriquée. Un type de champ peut être restreint en fonction du test, mais l'objet racine ne sera pas restreint:

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;
    }
}

La solution

La solution consiste à utiliser un protecteur de type personnalisé. Nous pouvons créer une version assez générique de la protection de type qui fonctionnerait pour tout paramètre de type qui a un champ type. Malheureusement, nous ne pouvons le faire pour aucun type générique car il sera lié à 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;
  }
};

Il est bon que l'expression genericThing.item est considéré comme un Foo à l'intérieur du bloc if. Je pensais que cela ne fonctionne qu'après l'avoir extrait dans une variable (const item = genericThing.item). Probablement un meilleur comportement des dernières versions de TS.

Cela active la correspondance de modèle comme dans la fonction area dans la documentation officielle sur nions Discriminées et qui est réellement manquante en C # (en v7, un cas default est toujours nécessaire dans une instruction switch comme celle-ci).

En effet, la chose étrange est que genericThing est toujours vu sans discrimination (comme un GenericThing<Foo | Bar> au lieu de GenericThing<Foo>), même à l'intérieur du bloc ifitem est un Foo! Ensuite, l'erreur avec fooThing.item.fooProp; ne me surprend pas.

Je suppose que l'équipe TypeScript a encore quelques améliorations à faire pour prendre en charge cette situation.

0
Romain Deneau