web-dev-qa-db-fra.com

Analyser la chaîne comme TypeScript Enum

Étant donné une énumération qui ressemble à ceci:

export enum UsedProduct {
    Yes = 'yes',
    No = 'no',
    Unknown = 'unknown',
}

J'aimerais écrire une fonction qui prend un ensemble de chaînes de caractères et renvoie une instance de UsedProduct. Jusqu'ici, j'ai écrit une fonction comme celle-ci:

export function parseUsedProduct(usedProdStr: 'yes' | 'no' | 'unknown'): UsedProduct {
    switch (usedProdStr) {
        case 'yes':
            return UsedProduct.Yes;
        case 'no':
            return UsedProduct.No;
        case 'unknown':
            return UsedProduct.Unknown;
        default:
            return unknownUsedProductValue(usedProdStr);
    }
}

function unknownUsedProductValue(usedProdStr: never): UsedProduct {
    throw new Error(`Unhandled UsedProduct value found ${usedProdStr}`);
}

Cette implémentation n'est pas géniale car je dois redéfinir les valeurs possibles de l'énum. Comment puis-je réécrire cette fonction afin de ne pas avoir à définir 'yes' | 'no' | 'unknown'

6
Jim Englert

TypeScript ne vous facilite pas la tâche. La réponse n’est donc pas unilatérale. 

Une valeur enum telle que UsedProduct.Yes est simplement une chaîne ou un littéral numérique au moment de l'exécution (dans ce cas, la chaîne "yes"), mais au moment de la compilation, elle est traitée comme un sous-type de la chaîne ou du nombre littéral. Donc, UsedProduct.Yes extends "yes" est vrai. Malheureusement, étant donné le type UsedProduct.Yes, il n'existe aucun moyen de programmation pour élargir le type à "yes"... ou, étant donné le type UsedProduct, il n'existe aucun moyen de programmation pour l'élargir à "yes" | "no" | "unknown". Il manque au langage quelques fonctionnalités dont vous auriez besoin pour cela.

Il y a est un moyen de créer une signature de fonction qui se comporte comme parseUsedProduct, mais elle utilise génériques et types conditionnels pour y parvenir:

type Not<T> = [T] extends [never] ? unknown : never
type Extractable<T, U> = Not<U extends any ? Not<T extends U ? unknown : never> : never>

declare function asEnum<E extends Record<keyof E, string | number>, K extends string | number>(
  e: E, k: K & Extractable<E[keyof E], K>
): Extract<E[keyof E], K>

const yes = asEnum(UsedProduct, "yes"); // UsedProduct.yes
const no = asEnum(UsedProduct, "no"); // UsedProduct.no
const unknown = asEnum(UsedProduct, "unknown"); // UsedProduct.unknown
const yesOrNo = asEnum(UsedProduct, 
  Math.random()<0.5 ? "yes" : "no"); // UsedProduct.yes | UsedProduct.no

const unacceptable = asEnum(UsedProduct, "oops"); // error

Fondamentalement, il prend un type d'objet enum E et un type chaîne ou nombre K et tente d'extraire la ou les valeurs de propriété de E qui étendent K. Si aucune valeur de E extend K (ou si K est un type d'union où l'une des pièces ne correspond à aucune valeur de E), le compilateur générera une erreur. Les détails du fonctionnement de Not<> et Extractable<> sont disponibles sur demande.

En ce qui concerne l’implémentation de la fonction, vous devrez probablement utiliser un type assertion . Quelque chose comme:

function asEnum<E extends Record<keyof E, string | number>, K extends string | number>(
  e: E, k: K & Extractable<E[keyof E], K>
): Extract<E[keyof E], K> {
  // runtime guard, shouldn't need it at compiler time
  if (Object.values(e).indexOf(k) < 0)
    throw new Error("Expected one of " + Object.values(e).join(", "));
  return k as any; // assertion
}

Cela devrait fonctionner. Dans votre cas particulier, nous pouvons coder en dur UsedProduct:

type Not<T> = [T] extends [never] ? unknown : never
type Extractable<T, U> = Not<U extends any ? Not<T extends U ? unknown : never> : never>
function parseUsedProduct<K extends string | number>(
  k: K & Extractable<UsedProduct, K>
): Extract<UsedProduct, K> {
  if (Object.values(UsedProduct).indexOf(k) < 0)
    throw new Error("Expected one of " + Object.values(UsedProduct).join(", "));
  return k as any;
}

const yes = parseUsedProduct("yes"); // UsedProduct.yes
const unacceptable = parseUsedProduct("oops"); // error

J'espère que cela pourra aider. Bonne chance!

5
jcalz

Vous pouvez utiliser la méthode getKeyOrThrow- de ts-enum-util. Vous ne savez pas comment cela est mis en œuvre, mais vous pouvez le regarder ici

Voici un stackblitz j'ai fait pour démontrer l'utilisation dans votre cas.

0
ShamPooSham