Можно ли сгенерировать значения из объединения буквенных типов Typescript во время выполнения?

Я использую Генератор кода GraphQL для генерации типов TypesScript из определения схемы graphql sdl. Соответствующая часть схемы определяет union из четырех types и выглядит примерно так:

union Game = GameLobby | GamePlaying | GameOverWin | GameOverTie

type GameLobby {
  id: ID!
}

type GamePlaying {
  id: ID!
  player1:String!
  player2:String!
}

type GameOverWin {
  id: ID!
  winner:String!
}

type GameOverTie {
  id: ID!
}

И генерирует следующие определения типов TypeScript:

export type Game = GameLobby | GamePlaying | GameOverWin | GameOverTie;

export type GameLobby = {
  __typename?: "GameLobby";
  readonly id: Scalars["ID"];
};

export type GameOverTie = {
  __typename?: "GameOverTie";
  readonly id: Scalars["ID"];
};

export type GameOverWin = {
  __typename?: "GameOverWin";
  readonly id: Scalars["ID"];
  readonly winner: String;
};

export type GamePlaying = {
  __typename?: "GamePlaying";
  readonly player1: String;
  readonly player2: String;
};

Теперь я хочу иметь возможность использовать объединение типов во время выполнения, чтобы позволить мне различать, в каком состоянии находится игра в данный момент. Я могу определить такой союз следующим образом:

// assume this gives back the generated types:
import { Game } from "./generated/models";

// we only want the actual discriminants
type GameStatus = Exclude<Game["__typename"], undefined>;

С этим типом я могу строго ввести любое значение, которое может нуждаться в GameStatus, например:

class GameModel {
  public readonly id!: number;
  public readonly status!: GameStatus;
}

Наконец, я хочу иметь возможность преобразовать карта статус игры в постоянное состояние, и для этого мне нужно перечислить все возможные ценности, которые GameStatus действительно может принять. Для этого в идеале я бы не хотел повторно вводить значения, но также, если мне нужно, я хотел бы, по крайней мере, быть уверенным, что не пропустил ни одного из них.

Прямо сейчас я гарантирую, что покрываю все возможные значения, которые может принимать GameStatus:

function assertNever(value: never): never {
  throw new Error(`unexpected value ${value}`);
}

export const GameModelLobby: GameModelState = "GameLobby";
export const GameModelPlaying: GameModelState = "GamePlaying";
export const GameModelOverWin: GameModelState = "GameOverWin";
export const GameModelOverTie: GameModelState = "GameOverTie";

const gameStatus = [
  GameModelLobby, 
  GameModelPlaying, 
  GameModelOverWin, 
  GameModelOverTie
];

// ensure we didn't forget any state
gameStatus.forEach(status => {
  switch (status) {
    case GameModelLobby:
      break;
    case GameModelPlaying:
      break;
    case GameModelOverWin:
      break;
    case GameModelOverTie:
      break;
    default:
      assertNever(status);
  }
});

Это заставляет tsc проверять, что все значения покрыты или удалены по мере изменения базовой схемы GraphQL. Своего рода гибрид времени выполнения и статической проверки, потому что я оставляю код для выполнения во время выполнения, но tsc также будет проверять статически...

И вопрос: возможно ли как-то сгенерировать ценности из объединения литеральных типов во время выполнения? В качестве альтернативы: возможно ли сгенерировать TypeScript Enum из объединения литеральных типов во время выполнения?

Если ни один из этих двух вариантов невозможен: существует ли более краткий способ проверки типов и обеспечения отсутствия случаев?

Обновлять

Следуя ответу @dezfowler и с некоторыми незначительными изменениями, я решил проблему следующим образом:

Сначала извлеките типы дискриминатора из типа объединения GameState:

import { GameState } from "./generated/models";
export type GameStateKind = Exclude<GameState["__typename"], undefined>;

Затем создайте сопоставленный тип (что является своего рода тавтологией) и сопоставьте типы с ценности безопасным для типов способом. Карта заставляет вас использовать все типы в качестве ключей и записывать все значения, поэтому, если не будет каждого дискриминанта, она не будет компилироваться:

export const StateKindMap: { [k in GameStateKind]: k } = {
  GameLobby: "GameLobby",
  GameOverTie: "GameOverTie",
  GameOverWin: "GameOverWin",
  GamePlaying: "GamePlaying"
};

Экспортируйте все типы в виде массива, который затем можно использовать для создания перечислений в модели базы данных:

export const AllStateKinds = Object.values(StateKindMap);

И, наконец, я написал небольшой тест, чтобы убедиться, что я могу напрямую использовать StateKindMap для различения GameStateKind (этот тест избыточен, потому что все необходимые проверки выполняются tsc):

import { StateKindMap, AllStateKinds } from "./model";

describe("StateKindMap", () => {
  it("should enumerate all state kinds", () => {
    AllStateKinds.forEach(kind => {
      switch (kind) {
        case StateKindMap.GameLobby:
          break;
        case StateKindMap.GameOverTie:
          break;
        case StateKindMap.GameOverWin:
          break;
        case StateKindMap.GamePlaying:
          break;
        default:
          assertNever(kind);
      }
    });
  });
});

function assertNever(value: never): never {
  throw new Error(`unexpected value ${value}`);
}

Обратите внимание, что IE не поддерживает Object.values(), поэтому Object.keys(), который дает тот же результат для этого примера, более безопасен для совместимости.

dezfowler 03.08.2019 16:13
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
1
629
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я понял, что забыл ответить на первую часть вашего вопроса о создании вещей во время выполнения. Это невозможно, так как в коде JavaScript нет представления системы типов TypeScript. Обходной путь для этого — создать фиктивный объект (используя описанную ниже технику сопоставленного типа), который вынуждает вас добавлять ключи для всех значений объединения, чтобы его можно было скомпилировать. Затем вы просто выполняете Object.keys() передачу этого фиктивного объекта, чтобы получить массив строковых значений статуса игры.

Что касается второй части вопроса о более краткой проверке типов...

Вы можете использовать отображаемый тип для этого...

type StrategyMap<T> = { [K in GameStatus]: T };

Который вы затем можете использовать вот так...

const title: StrategyMap<string> = {
    GameLobby: "You're in the lobby",
    GameOverTie: "It was a tie",
    GameOverWin: "Congratulations",
    GamePlaying: "Game on!"
};

или это...

const didYouWin = false;
const nextStatusDecision: StrategyMap<() => GameStatus> = {
    GameLobby: () => "GamePlaying",
    GameOverTie: () => "GameLobby",
    GameOverWin: () => "GameLobby",
    GamePlaying: () => didYouWin ? "GameOverWin" : "GameOverTie"
};

const currentStatus: GameStatus = "GamePlaying";
const nextStatus = nextStatusDecision[currentStatus]();

Если вы забудете статус, вы получите предупреждение, например.

Property 'GamePlaying' is missing in type '{ GameLobby: string; GameOverTie: string; GameOverWin: string; }' but required in type 'StrategyMap<string>'.

Пример игровой площадки TypeScript здесь.

Это часть головоломки, которую мне не хватало, я провел тест в своем приложении и с некоторыми незначительными изменениями заставил его работать, я обновлю свой вопрос окончательным решением, а затем одобрю ваш ответ. Спасибо!

Grancalavera 03.08.2019 14:50

Другие вопросы по теме