【问题标题】:Why is this mapped type losing the type inference of the object literal?为什么这个映射类型会丢失对象字面量的类型推断?
【发布时间】:2019-10-03 22:48:45
【问题描述】:

这是做作的,但通常我可以将对象文字传递给函数并在泛型中捕获文字的值,即:

type Values<V> = {
  a: V;
  b: V;
};

function mapValues<V>(v: Values<V>): V {
  return v as any; // ignore
}
const vn = mapValues({ a: 1, b: 2 }); // inferred number
const vs = mapValues({ a: '1', b: '2' }); // inferred string

根据我传递给 mapValues 的内容,vnvs 都被正确推断为 numberstring

这甚至适用于索引类型:

function mapValues2<V>(v: { [key: string]: V }): V {
  return v as any;
}
const v2n = mapValues2({ a: 1, b: 2 }); // inferred number
const v2s = mapValues2({ a: '1', b: '2' }); // inferred string

v 对象字面量知道它的值是 string/number(分别),我能够捕获推断的 V 并在返回类型中使用它。

但是,一旦我使用映射类型,即:

enum Foo {
  a,
  b,
}
function mapValues3<K, V>(o: K, v: { [key in keyof K]: V }): V {
  return v as any;
}
const v3n = mapValues3(Foo, { a: 1, b: 2 }); // inferred unknown
const v3s = mapValues3(Foo, { a: '1', b: '2' }); // inferred unknown

这就像V 忘记了它是否是string/number(分别),我得到unknown 推断。

请注意,如果我明确输入 const v3n: number,那么它会起作用,但我想依靠类型推断来为我计算出 V

我很困惑为什么将 [key: string] 从第二个 sn-p 更改为第三个 [key in keyof K] 会影响对象文字类型推断的 : V 侧的推断。

有什么想法吗?

【问题讨论】:

  • 我不知道如何回答为什么它会失败,除了一般直觉认为T 从函数F&lt;T&gt; 的推断越不可靠,F 越复杂。我的建议是通过推断v 的类型来简化推断,例如mapValues3&lt;O, V extends Record&lt;keyof O, unknown&gt;&gt;(o: O, v: V): V[keyof O];
  • 如果以上内容符合您的用例,我很乐意将其转化为答案。
  • @jcalz 哇,必须将其返回到我的实际/非人为用例,但它确实有效!谢谢!是的,如果你回答-ize,我会接受!

标签: typescript


【解决方案1】:

对于失败的原因,我没有规范的答案。我的直觉是你试图让mapValue3(o, v) 以依赖于o 参数的键的方式推断v 参数的键和值,推迟V 的推断直到它显然也是晚了,编译器放弃了它的一般unknown推理。

您经常遇到这样的情况:您有一个类型为 U 的值 val,并试图从中推断出一个相关的类型 T。也就是说,对于某些类型函数F,您将U 视为F&lt;T&gt;,并且您希望编译器从F&lt;T&gt; 推断T。假设 anyone 甚至可以进行这种推断(像type F&lt;T&gt; = T extends object ? true : false 这样的有损函数会丢弃信息,因此您无法从F&lt;T&gt; 推断出很多关于T 的信息),对于 来说并不总是可能的em>编译器。如果它有效,那就太好了。

否则,我的经验法则是:不要从U 类型的值推断T,而是直接推断U。然后,对于某些类型函数G,将T 表示为G&lt;U&gt;。也就是说,G 类型的函数应该是F 函数的inverse。 (所以对于所有TG&lt;F&lt;T&gt;&gt;T)。逆类型函数G 可能比F 写起来更复杂,但如果是这样,人类在这方面可能会比编译器更好。另请注意,您可能需要将constrain 您的U 类型设置为F&lt;any&gt; 才能使映射工作。

以上内容也适用于多个类型变量(例如,U1 = F1&lt;T1, T2, T3&gt;U2 = F2&lt;T1, T2, T3&gt;U3 = F3&lt;T1, T2, T3&gt; 类型的值 v1v2v3,并且您想推断 T1、@987654364 @ 和 T3 :相反,找到 G1G2G3 使得 T1 = G1&lt;U1, U2, U3&gt;T2 = G2&lt;U1, U2, U3&gt;T3 = G3&lt;U1, U2, U3&gt; 并直接计算它们)。

让我们为你的函数这样做,重写如下:

function mapValues3<O, T>(o: O, v: F<O, T>): T {
  return null!
}
type F<O, T> = Record<keyof O, T>; // same as { [key in keyof O]: T }; 

我们想把它变成这样:

function mapValues3<O, U extends F<O, any>>(o: O, v: U): G<O, U> {
  return null!
}
type G<O, U> = ???;

请注意,由于第一个参数已经只是“推断此参数的值”,因此我们无需为 O 做任何事情。问题是:G 是什么?给定U 类型的值等于Record&lt;keyof O, T&gt;,我们如何得到T?答案是使用lookup type

type G<O, U> = U[keyof O];

让我们确保:G&lt;O, Record&lt;keyof O, T&gt;Record&lt;keyof O, T&gt;[keyof O],即 {[K in keyof O]: T}[keyof O],其计算结果为 T。我们现在可以消除FG 并给你这个:

function mapValues3<O, U extends Record<keyof O, any>>(o: O, v: U): U[keyof O] {
  return null!
}

让我们测试一下:

const v3n = mapValues3(Foo, { a: 1, b: 2 }); // number
const v3s = mapValues3(Foo, { a: "1", b: "2" }); // string

这些都按照你想要的方式工作。让我们也看看一些可能的边缘情况:

const constraintViolation = mapValues3(Foo, { a: "hey" }); // error! "b" is missing

看起来不错,因为您希望第二个参数具有第一个参数中的所有键。还有这个:

const excessProp = mapValues3(Foo, { a: 1, b: 2, c: false }); // number, no error

这可能会也可能不会。它不违反约束;它只是有额外的属性,通常在 TypeScript 中是允许的(但 forbidden in some situations)。推断是number 而不是number | boolean,因为在确定返回类型时不会参考额外的属性。如果这是一个问题,有办法让 mapValues3 拒绝此类事情,但它们更复杂,这不是问题的一部分。


好的,希望对您有所帮助。祝你好运!

Link to code

【讨论】:

  • 再次感谢!如果您对我最终使用您的提示的位置感兴趣,请点击此处:github.com/stephenh/ts-enum-mapper/blob/master/src/index.ts#L15
  • 也非常感谢您对“直接推断 U,然后对其进行映射”的详细理由/解释。我还不能直观地/即席地应用它,但我有点明白了,并且会看到它是如何融入的。:-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-19
  • 2017-06-22
相关资源
最近更新 更多