Проверка правильности типа объединения строк в runtime?

У меня простой тип строковых литералов и нужно проверить его достоверность из-за того, что FFI вызывает "обычный" Javascript. Есть ли способ обеспечить, чтобы определенная переменная являлась экземпляром любой из этих литералов во время выполнения? Что-то вроде

type MyStrings = "A" | "B" | "C";
MyStrings.isAssignable("A"); // true
MyStrings.isAssignable("D"); // false

Ответ 1

Если в вашей программе есть несколько определений объединения строк, которые вы хотите проверить во время выполнения, вы можете использовать универсальную функцию StringUnion для генерации их статических типов и методов проверки типов.

Общая вспомогательная функция

// TypeScript will infer a string union type from the literal values passed to
// this function. Without 'extends string', it would instead generalize them
// to the common string type. 
export const StringUnion = <UnionType extends string>(...values: UnionType[]) => {
  Object.freeze(values);
  const valueSet: Set<string> = new Set(values);

  const guard = (value: string): value is UnionType => {
    return valueSet.has(value);
  };

  const check = (value: string): UnionType => {
    if (!guard(value)) {
      const actual = JSON.stringify(value);
      const expected = values.map(s => JSON.stringify(s)).join(' | ');
      throw new TypeError('Value '${actual}' is not assignable to type '${expected}'.');
    }
    return value;
  };

  const unionNamespace = {guard, check, values};
  return Object.freeze(unionNamespace as typeof unionNamespace & {type: UnionType});
};

Пример определения

Нам также нужна строка шаблона для извлечения сгенерированного типа и объединения его определения с его объектом пространства имен. Если это определение экспортируется и импортируется в другой модуль, они получат объединенное определение автоматически; потребителям не нужно будет повторно извлекать тип самостоятельно.

const Race = StringUnion(
  "orc",
  "human",
  "night elf",
  "undead",
);
type Race = typeof Race.type;

Пример использования

Во время компиляции тип Race работает так же, как если бы мы обычно определяли объединение строк с помощью "orc" | "human" | "night elf" | "undead" "orc" | "human" | "night elf" | "undead" "orc" | "human" | "night elf" | "undead". У нас также есть .guard(...) которая возвращает значение того, является ли значение членом объединения и может ли он использоваться в качестве защитника типов, и .check(...) которая возвращает переданное значение, если это допустимо, иначе выдает TypeError.

let r: Race;
const zerg = "zerg";

// Compile-time error:
// error TS2322: Type '"zerg"' is not assignable to type '"orc" | "human" | "night elf" | "undead"'.
r = zerg;

// Run-time error:
// TypeError: Value '"zerg"' is not assignable to type '"orc" | "human" | "night elf" | "undead"'.
r = Race.check(zerg);

// Not executed:
if (Race.guard(zerg)) {
  r = zerg;
}

Более общее решение: runtypes

Этот подход основан на библиотеке runtypes, которая предоставляет аналогичные функции для определения практически любого типа в TypeScript и автоматического получения средств проверки типов во время выполнения. Это было бы немного более многословно для этого конкретного случая, но подумайте об этом, если вам нужно что-то более гибкое.

Пример определения

import {Union, Literal, Static} from 'runtypes';

const Race = Union(
  Literal('orc'),
  Literal('human'),
  Literal('night elf'),
  Literal('undead'),
);
type Race = Static<typeof Race>;

Пример использования будет таким же, как указано выше.

Ответ 2

Начиная с Typescript 2.1, вы можете сделать это с keyof оператора keyof.

Идея заключается в следующем. Поскольку информация о типе строкового литерала недоступна во время выполнения, вы определите простой объект с ключами в качестве строковых литералов, а затем создадите тип ключей этого объекта.

Следующее:

// Values of this dictionary are irrelevant
const myStrings = {
  A: "",
  B: ""
}

type MyStrings = keyof typeof myStrings;

isMyStrings(x: string): x is MyStrings {
  return myStrings.hasOwnProperty(x);
}

const a: string = "A";
if(isMyStrings(a)){
  // ... Use a as if it were typed MyString from assignment within this block: the TypeScript compiler trusts our duck typing!
}

Ответ 3

использование type - это просто тип Aliasing, и он не будет присутствовать в скомпилированном javascript-коде, из-за чего вы не можете этого сделать:

MyStrings.isAssignable("A");

Что вы можете с ним сделать:

type MyStrings = "A" | "B" | "C";

let myString: MyStrings = getString();
switch (myString) {
    case "A":
        ...
        break;

    case "B":
        ...
        break;

    case "C":
        ...
        break;

    default:
        throw new Error("can only receive A, B or C")
}

Что касается вопроса о isAssignable, вы можете:

function isAssignable(str: MyStrings): boolean {
    return str === "A" || str === "B" || str === "C";
}

Ответ 4

Вы можете использовать enum, а затем проверить, если строка в Enum

export enum Decisions {
    approve = 'approve',
    reject = 'reject'
}

export type DecisionsTypeUnion =
    Decisions.approve |
    Decisions.reject;

if (decision in Decisions) {
  // valid
}

Ответ 5

Вы не можете вызвать метод для типа, потому что типы не существуют во время выполнения

MyStrings.isAssignable("A"); // Won't work — 'MyStrings' is a string literal

Вместо этого создайте исполняемый код JavaScript, который проверит ваш ввод. Это ответственность программиста, чтобы гарантировать, что функция выполняет свою работу должным образом.

function isMyString(candidate: string): candidate is MyStrings {
  return ["A", "B", "C"].includes(candidate);
}