是否可以在运行时从文字类型的 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.
我正在使用 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.