【问题标题】:Programatically create a typed Record / Object from the keys of another one以编程方式从另一个键创建一个类型化的记录/对象
【发布时间】:2021-04-15 17:12:07
【问题描述】:

在我的打字稿程序(下面的代码)中,我定义了两个基本类型(PlayerState),然后是一些嵌套的 Record 类型用作映射。

然后,我在一个类型化函数中根据嵌套记录的现有实例创建这些记录之一的实例。


type Player = "1" | "2";
type State = "A" | "B" | "C";
type StateMapping = Record<State, State>;
type PlayerStateMappings = Record<Player, StateMapping>
type PlayerStates = Record<Player, State>;

const playerStateMappings: PlayerStateMappings = {
    "1": {
        "A": "B",
        "B": "C",
        "C": "A"
    },
    "2": {
        "C": "B",
        "B": "A",
        "A": "C"
    },
}

function nextStates(currentState: State): PlayerStates {
    var nextStates = {};
    for(const player of Object.keys(playerStateMappings)){
        nextStates[player] = playerStateMappings[player][currentState]
    }
    return nextStates;
}

console.log(nextStates("A"))

此代码在 return 语句中引发以下类型错误,因为我创建了没有所需键的对象,并且之后才添加了这些: TS2739: Type '{}' is missing the following properties from type 'PlayerStates': 1, 2.

我的问题是,是否有一种方法可以避免满足以下要求的此类错误

  1. 类型系统并没有特别放松,它仍然强制nextStates 函数返回一个完整且有效的PlayerStates 对象。
  2. nextStates 对象是基于playerStatesMapping 对象的键以编程方式创建的,这意味着我不必再次对所有播放器进行硬编码。

在对 SO 进行一些研究后,我发现了一些避免错误的选项,但所有这些选项都违反了上述两个要求之一:

违反条件 1 的方法:

  1. 将 PlayerStates 类型设为局部:type PlayerStates = Partial&lt;Record&lt;Player, State&gt;&gt;;
  2. 使用as关键字强制类型:var nextStates = {} as PlayerStates; (来自this问题)

违反条件 2 的方法:

  1. 在对象创建中为每个玩家设置一个默认值:var nextStates = {"1": "A", "2": "B"}

我知道在上面的例子中整个打字有点过头了,但这是我在一个更复杂的项目中遇到的问题的高度简化/简化版本,上面的要求/期望更有意义。

PS:来自 python 背景,我想我正在寻找类似 @​​987654322@ 的东西,它允许我基于一些迭代来初始化一个新字典。

【问题讨论】:

  • 为什么“使用关键字强制类型”违反第 1 点? IMO 没有。
  • @hackape:因为那时类型系统不会强制我返回完整的PlayerStates 记录。例如,下面的代码会进行类型检查:``` function nextStates(currentState: State): PlayerStates { var nextStates = {} as PlayerStates;返回下一个状态; } ```

标签: javascript typescript


【解决方案1】:

根据@hackape 的回答,我能够生成以下内容,但需要重新定义Object.fromEntriesObject.entries

type Player = "1" | "2";
type State = "A" | "B" | "C";
type StateMapping = Record<State, State>;
type PlayerStateMappings = Record<Player, StateMapping>
type PlayerStates = Record<Player, State>;

const playerStateMappings: PlayerStateMappings = {
    "1": {
        "A": "B",
        "B": "C",
        "C": "A"
    },
    "2": {
        "C": "B",
        "B": "A",
        "A": "C"
    },
}

// stricter version of Object.entries
const entries: <T extends Record<PropertyKey, unknown>>(obj: T) => Array<[keyof T, T[keyof T]]> = Object.entries

// stricter version of Object.fromEntries
const fromEntries: <K extends PropertyKey, V>(entries: Iterable<readonly [K, V]>) => Record<K, V> = Object.fromEntries

function nextStates(currentState: State): PlayerStates {
  return fromEntries(
    entries(playerStateMappings).map(([player, mapping]) =>
      [player, mapping[currentState]]
    )
  )
}

Playground link

【讨论】:

  • 或者懒人的as Player 技巧也有效。我认为这是合法的,因为我们知道这是事实。
  • 当然 - 虽然我个人认为在实际/可能的情况下避免覆盖编译器总是更好
  • 非常感谢大家,这似乎确实有效,并且符合我所有的要求。您能否就重新定义entriesfromEntries 背后的想法提供一个提示。我有点难以理解它(我是打字稿的新手)。
  • 不幸的是,我刚刚意识到这个答案似乎在某种程度上放松了打字系统,因为它并没有真正强制 nextStates 函数返回一个完整的 PlayerStates 对象。如果我声明 PlayerStateMappings 部分并从 playerStateMappings 中删除一个播放器条目,即使 nextStates 的返回值不再是有效的 PlayerStates 对象,代码仍然会进行类型检查。
  • 有趣的是,在上面描述的 Typescript Playground 中,更改确实会引发错误,但是当我使用 ts-node 运行文件时,它的类型检查没有错误。 (即使nextStates 会返回一个无效的PlayerState 对象)
【解决方案2】:

尝试函数式编程方式。您可以完全避免使用 nextStates 变量。问题消失了。

function nextStates(currentState: State): PlayerStates {
  return Object.fromEntries(
    Object.entries(playerStateMappings).map(([player as Player, mapping]) =>
      [player, mapping[currentState]]
    )
  )
}

【讨论】:

  • 这不会为我编译。我收到一条错误消息:Type '{ [k: string]: State; }' is missing the following properties from type 'PlayerStates': 1, 2
  • 我只是在手机上输入。可以分享一个TS游乐场吗?我很难设置。
  • 啊,我明白了。这是由于标准库的 TS 库存类型实际上有点保守。 Object.entries&lt;T&gt; 可以推断出entry[0]keyof T,但事实并非如此,它只是推断string 是安全的。
  • 这应该很容易解决,让我试试
【解决方案3】:

类型错误来自您的 nextStates 变量,因为您没有指定它的类型,因此当您将其定义为空对象时,它被推断为 {}

您可以只为您的nextStates 变量使用Partial 类型以允许它从空开始,但是您需要一种方法来告诉编译器何时使用保护创建了完整的PlayerStates 对象。这是一个例子:

type Player = "1" | "2";
type State = "A" | "B" | "C";
type StateMapping = Record<State, State>;
type PlayerStateMappings = Record<Player, StateMapping>
type PlayerStates = Record<Player, State>;

const playerStateMappings: PlayerStateMappings = {
    "1": {
        "A": "B",
        "B": "C",
        "C": "A"
    },
    "2": {
        "C": "B",
        "B": "A",
        "A": "C"
    },
}

// Stricter version of keys which returns Array<keyof T> instead of string[]
const keys = <T extends Record<PropertyKey, unknown>>(obj: T): Array<keyof T> =>
  Object.keys(obj);

// Type guard to convert Partial<PlayerStates> to PlayerStates
const isCompletePlayerStatesObj = (
  obj: Partial<PlayerStates>
): obj is PlayerStates => 
  obj["1"] !== undefined && obj["2"] !== undefined;

function nextStates(currentState: State): PlayerStates {
    var nextStates: Partial<PlayerStates> = {};

    for(const player of keys(playerStateMappings)){
        nextStates[player] = playerStateMappings[player][currentState]
    }

    if (!isCompletePlayerStatesObj(nextStates)) {
      throw new Error("Whoops this function was badly implemented")
    }

    return nextStates;
}

console.log(nextStates("A"));

【讨论】:

  • 感谢您的建议。我想避免像您在添加的功能中所做的那样再次对所有玩家进行硬编码/列出(我可能应该更好地制定要求 2)。此外,我在这里使用打字系统在编译/类型检查时捕获更多错误。据我了解,您的解决方案会放宽打字系统并用运行时检查替换它,这并不理想。
  • 我认为那是不可能的。 IMO 的最佳选择是拥有一个初始 PlayerStates 对象,您可以将其用作起点而不是空对象。
  • 是的,这就是我目前实现它的方式。它只是感觉非常冗长/次优。本质上,我只想将玩家列表放在一个地方,因为这些基本类型包含更多可能会经常更改的值,这就是为什么我想避免再次将它们全部写出来。
  • 我同意它很冗长。如果您在初始 PlayerStates 对象中缺少玩家,至少编译器会向您抱怨。因此,一旦您添加了一个播放器,编译器就会告诉您需要更新该对象。
  • 我已经根据@hackape 的建议添加了一个新答案:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多