【问题标题】:Is it possible to generate the values from a Typescript union of literal types in runtime?是否可以在运行时从文字类型的 Typescript 联合生成值?
【发布时间】:2019-12-05 11:19:22
【问题描述】:

我正在使用 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);
  }
});

这使得tsc 检查所有值是否被覆盖或删除,因为底层 GraphQL 架构发生变化。一种运行时/静态检查混合,因为我将代码留在运行时执行,但tsc 也会静态检查...

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

如果这两个都不可能:有没有更简洁的方法来检查类型并确保没有遗漏任何案例?

更新

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

【问题讨论】:

  • 请注意,IE 不支持Object.values(),因此Object.keys() 与此示例的结果相同,但兼容性更安全。

标签: typescript graphql code-generation


【解决方案1】:

我意识到我忘了回答你关于在运行时生成东西的问题的第一部分。这是不可能的,因为 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

【讨论】:

  • 这是我缺少的一块拼图,我在我的应用程序中进行了测试,并进行了一些小的更改使其正常工作,我将使用最终解决方案更新我的问题,然后批准您的答案.谢谢!
猜你喜欢
  • 2016-07-22
  • 2021-11-20
  • 2023-01-11
  • 1970-01-01
  • 2018-03-01
  • 2022-01-26
  • 2019-12-10
  • 2018-09-06
  • 2017-01-03
相关资源
最近更新 更多