此代码示例是一个函数,用于对记录进行简洁的不可变编辑以设置布尔值。
该函数应该接受布尔值记录和匹配键列表。它应该返回一个新记录,其中所有键都设置为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
参数?我遗漏了什么?
1条答案
按热度按时间2wnc66cl1#
调用generic函数时,TypeScript使用各种试探法来确定如何推断其类型参数。通常,编译器可能会参考 * 推断站点 * 来生成泛型类型参数的 * 候选对象(通过一些启发法),然后面对多个候选者,它需要弄清楚如何从它们中进行选择或者它们的组合(通过其他试探法)这些试探法在广泛的情况下工作得相当好,但当然也有编译器做了函数设计者不想做的事情的情况。
对于具有调用签名的函数
当前实现的启发式算法将优先级给予
keys
中的推理站点,而不是flags
中的推理站点,因此您得到问题中描述的问题行为。如果你作为函数设计者告诉编译器"请不要试图从
keys
推断K
。从flags
推断它,然后仅仅 * 检查 * 它与为keys
传入的值",你会说keys
中的K
语句是 * 非推断类型参数用法 *。这是microsoft/TypeScript#14829的主题,它请求某个NoInfer<T>
实用程序类型(可能是intrinsic
类型,如microsoft/TypeScript#40580中所述),该实用程序类型的计算结果为T
,但会阻塞推理。如果真的有你会写
事情就会按你的意愿发展。
不幸的是,目前还没有这样的内置实用程序类型,也许有一天会在TypeScript发行版中引入,但现在还没有。
幸运的是,至少有一些用户级实现可以在某些用例中使用,这里展示了一个,我们利用了编译器推迟对泛型conditional types求值的倾向:
现在我们可以尝试一下:
看起来不错!编译器根据
flags
参数推断K
,并根据需要检查后续参数。上面NoInfer<T>
的定义并不一定适用于 * 所有 * 用例(GitHub问题描述了一些失败),所以它不是万能药。但如果它适用于您的目的,那么它可能已经足够好了。Playground代码链接