此代码示例是一个用于对记录进行简洁的不可变编辑以设置布尔值的函数。
函数应该接受布尔值记录和匹配键列表。它应该返回一个将所有这些键设置为 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")
你可以在这个操场上解决这个问题
错误的行表明 Typescript 推断
Key
过于狭窄有困难。通过仅从 keys
数组推断,而不是尝试从 flags
对象推断,它最终会抱怨 flags
对象无效,如果它有任何不在 keys
中的属性名称.. .
激励示例
尽管这是一个非常简单的示例,但我正在处理的更复杂的示例具有相似的性质,其错误条件类似于此处的过剩属性检查 - 换句话说,
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
参数?我错过了什么?
当您调用 generic 函数时,TypeScript 使用各种启发式方法来确定如何推断其类型参数。一般来说,有 推理站点 编译器可能会参考生成 candidates 泛型类型参数(通过一些启发式),然后面对多个候选者,它需要弄清楚如何从中选择或组合其中(通过其他启发式)。这些启发式方法在各种情况下都工作得很好,但当然也有编译器做函数设计者不想要的事情的情况。
对于带有调用签名的函数
function createNextFlags<K extends string>(
flags: Record<K, boolean>,
...keys: [K, ...K[]]
): Record<K, boolean>;
目前实施的启发式优先考虑
keys
中的推理站点而不是flags
中的推理站点,因此您会得到问题中描述的问题行为。
如果作为函数设计者的您可以告诉编译器“请不要尝试从
K
推断出 keys
就好了。从 flags
推断出它,然后只需 check 它与传入的值keys
”。你会说 K
中的 keys
语句是 非推理类型参数用法。这是 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 版本中引入,但现在还没有。
幸运的是,有各种用户级实现至少适用于 some 用例。一个是这里,我们利用编译器的倾向来推迟对泛型条件类型的评估,这对我们有利:
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
看起来不错!编译器从
K
参数推断出 flags
并根据需要检查后续参数。上述NoInfer<T>
的定义并不一定适用于all用例(GitHub问题描述了一些失败)所以它不是万能的。但是,如果它适合您的目的,那么它可能就足够了。