是否可以在运行时从文字类型的 Typescript 联合生成值?

Is it possible to generate the values from a Typescript union of literal types in runtime?

我正在使用 GraphQL code generator 从 graphql sdl 模式定义生成 TypesScript 类型。架构的相关部分从四个 types 定义了一个 union,看起来像这样:

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

随着底层 GraphQL 模式的变化,这使得 tsc 检查所有值是否被覆盖或删除。有点像 runtime/static 检查混合,因为我要让代码在运行时执行,但 tsc 也会静态检查...

问题是:是否有可能在运行时从文字类型的联合中生成 ?或者:是否可以在运行时从文字类型的联合生成 TypeScript Enum

如果这两个 none 是可能的:是否有更简洁的方法来进行类型检查并确保不遗漏任何情况?

更新

根据@dezfowler 的回答并进行一些小改动,这就是我解决问题的方法:

首先从GameState联合类型中提取鉴别器类型:

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

然后构建一个映射类型(这是一种重言式)并在类型安全的情况下将 types 映射到 values方法。该地图强制您使用所有类型作为键并写入所有值,因此除非每个判别式都存在,否则它不会编译:

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

我意识到我忘了回答你关于在运行时生成内容的问题的第一部分。这是不可能的,因为 JavaScript 代码中没有 TypeScript 类型系统的表示。一个解决方法是创建一个虚拟对象(使用下面的映射类型技术),它强制您为所有联合值添加键以使其编译。然后,您只需 Object.keys() 传递该虚拟对象即可获得游戏状态字符串值数组。

至于问题的第二部分关于更简洁的类型检查...

您可以为此使用 mapped type...

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]();

如果您忘记状态,您会收到警告,例如

TypeScript 游乐场示例 here.