typescript 编辑记录时推断通用键

f8rj6qna  于 2023-03-04  发布在  TypeScript
关注(0)|答案(1)|浏览(116)

此代码示例是一个函数,用于对记录进行简洁的不可变编辑以设置布尔值。
该函数应该接受布尔值记录和匹配键列表。它应该返回一个新记录,其中所有键都设置为true(使用"不可变"更新模式,其中原始记录不被修改)。
我得到的错误是如此的基本,我觉得我需要另一双眼睛。我一定是错过了什么。我如何配置泛型,以获得下面的代码编译和运行明智?

function createNextFlags<Key extends string>(
  flags: Record<Key, boolean>,
  ...keys: [Key, ...Key[]]
) {
  const nextFlags = {
    ...flags
  }
  for (const key of keys) {
    nextFlags[key] = true;
  }
  return nextFlags;
}

createNextFlags({
  vanilla:false,
  chocolate:true, // this line generates a compiler error because flags constraint is too narrow
}, "vanilla")

您可以在this playground中处理该问题
错误行表明Typescript很难推断Key的范围太窄。通过仅从keys数组进行推断,而不是尝试从flags对象进行推断,如果flags对象有任何不在keys中的属性名,它最终会抱怨flags对象无效...
激励性示例
虽然这是一个非常简单的例子,但我正在处理的更复杂的例子具有类似的性质,错误条件类似于这里的多余属性检查-换句话说,keys驱动flags的约束类型,而应该以相反的方式推断它-keys应该从flags的属性推断。
解决方案
您可能认为下面的解决方法会为flags对象的类型创建一个占位符....

// Define Flags explicitly

function createNextFlags<Flags extends Record<Key, boolean>, Key extends string>(
  flags: Flags,
  ...keys: [Key, ...Key[]]
) {
  const nextFlags = {
    ...flags
  }
  for (const key of keys) {
    nextFlags[key] = true; // this assignment is apparently illegal!
  }
  return nextFlags;
}

createNextFlags({
  vanilla:false,
  chocolate:true,
}, "vanilla")

然而,这种方法会产生一个更奇怪的错误。将鼠标悬停在对nextFlags属性的明显错误的赋值上会显示出令人惊讶的错误行(我不骗你)...

const nextFlags: Flags extends Record<Key, boolean>
Type 'boolean' is not assignable to type 'Flags[Key]'

我还尝试过使用keyof直接从flags类型派生密钥,结果与此相同,尽管它完全消除了Key泛型,并使keys类型完全派生自flags

// use keyof to ensure that the property name aligns

function createNextFlags<Flags extends Record<any, boolean>>(
  flags: Flags,
  ...keys: [keyof Flags, ...(keyof Flags)[]]
)

然而,它也有同样的错误

const nextFlags: Flags extends Record<any, boolean>
Type 'boolean' is not assignable to type 'Flags[keyof Flags]'

我还尝试过使用infer直接从flags类型派生密钥,如下所示...

type InferKey<Flags extends Record<any, boolean>> = Flags extends Record<infer Key, boolean> ? Key: never;

function createNextFlags<Flags extends Record<any, boolean>>(
  flags: Flags,
  ...keys: [InferKey<Flags>, ...InferKey<Flags>[]]
)

这导致了一个同样令人惊讶的错误...

const nextFlags: Flags extends Record<any, boolean>
Type 'boolean' is not assignable to type 'Flags[InferKey<Flags>]'

解决这个问题的正确方法是什么,以便Key类型可以从flags对象中推断出来,从而约束keys参数?我遗漏了什么?

2wnc66cl

2wnc66cl1#

调用generic函数时,TypeScript使用各种试探法来确定如何推断其类型参数。通常,编译器可能会参考 * 推断站点 * 来生成泛型类型参数的 * 候选对象(通过一些启发法),然后面对多个候选者,它需要弄清楚如何从它们中进行选择或者它们的组合(通过其他试探法)这些试探法在广泛的情况下工作得相当好,但当然也有编译器做了函数设计者不想做的事情的情况。
对于具有调用签名的函数

function createNextFlags<K extends string>(
  flags: Record<K, boolean>,
  ...keys: [K, ...K[]]
): Record<K, boolean>;

当前实现的启发式算法将优先级给予keys中的推理站点,而不是flags中的推理站点,因此您得到问题中描述的问题行为。
如果你作为函数设计者告诉编译器"请不要试图从keys推断K。从flags推断它,然后仅仅 * 检查 * 它与为keys传入的值",你会说keys中的K语句是 * 非推断类型参数用法 *。这是microsoft/TypeScript#14829的主题,它请求某个NoInfer<T>实用程序类型(可能是intrinsic类型,如microsoft/TypeScript#40580中所述),该实用程序类型的计算结果为T,但会阻塞推理。
如果真的有你会写

declare function createNextFlags<K extends string>(
  flags: Record<K, boolean>,
  ...keys: [NoInfer<K>, ...NoInfer<K>[]]
): Record<K, boolean>;

事情就会按你的意愿发展。
不幸的是,目前还没有这样的内置实用程序类型,也许有一天会在TypeScript发行版中引入,但现在还没有。
幸运的是,至少有一些用户级实现可以在某些用例中使用,这里展示了一个,我们利用了编译器推迟对泛型conditional types求值的倾向:

type NoInfer<T> = [T][T extends any ? 0 : never];

现在我们可以尝试一下:

createNextFlags({
  vanilla: false,
  chocolate: true,
}, "vanilla"); // okay

createNextFlags({
  dog: true,
  cat: false
}, "cat", "dog"); // okay

createNextFlags({
  red: true,
  green: false,
  blue: true
}, "green", "purple", "blue"); // error!
// -------> ~~~~~~~~ // okay

看起来不错!编译器根据flags参数推断K,并根据需要检查后续参数。上面NoInfer<T>的定义并不一定适用于 * 所有 * 用例(GitHub问题描述了一些失败),所以它不是万能药。但如果它适用于您的目的,那么它可能已经足够好了。
Playground代码链接

相关问题