为什么此映射类型会丢失对象文字的类型推断?

时间:2019-10-02 18:42:52

标签: typescript

这是人为的,但是通常我可以将对象文字传递给函数,并以通用的方式捕获文字的值,即:

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

根据我传递给vn的内容,vsnumber都正确地推断为stringmapValues

这甚至适用于索引类型:

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]从第二个片段更改为第三个[key in keyof K]会影响对象文字类型推断的: V一侧的推断。

有什么想法吗?

1 个答案:

答案 0 :(得分:1)

对于这种失败的原因,我没有一个规范的答案。我的直觉是,您正在尝试使mapValue3(o, v)参数的键和值都取决于v参数的键,从而推论o参数的键和值,从而推迟{ {1}},直到为时已晚,编译器放弃了一般的V推论。

通常情况下,您的值unknown的类型为val,并试图从中推断出一个相关的类型U。也就是说,您将T视为某个类型函数U的{​​{1}},并且希望编译器从F<T>推断F。假设这样的推论甚至对任何人都是可行的(像T这样的有损函数会丢弃信息,因此您无法从F<T>来推断type F<T> = T extends object ? true : false),这就是对于编译器并非总是可行。如果可行,那就太好了。

否则,我的经验法则是:直接从T类型的值推断F<T>而不是从类型T的值推断U。然后,将某个类型函数U的{​​{1}}表示为T。也就是说,G<U>类型的函数应该是G函数的 inverse 。 (因此,对于所有GFG<F<T>>)。逆类型函数T的编写可能比T更为复杂,但是如果是这样,那么人类可能会比编译器更好。另外请注意,您可能需要constrainG类型为F才能使映射起作用。

以上内容也适用于多种类型的变量(例如,类型为UF<any>,{{1}的值v1v2v3 },并且您想从中推断出U1 = F1<T1, T2, T3>U2 = F2<T1, T2, T3>U3 = F3<T1, T2, T3>:相反,找到T1T2T3使得G1G2G3并直接进行计算)。

让我们为您的函数做一下,就像这样重写:

T1 = G1<U1, U2, U3>

我们想将其转换为此:

T2 = G2<U1, U2, U3>

请注意,由于第一个参数已经是“推断此参数的值”,因此对于T3 = G3<U1, U2, U3>,我们不需要做任何事情。问题是: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?答案是使用lookup type

U

我们只需确保:Record<keyof O, T>T,它是type G<O, U> = U[keyof O]; ,其结果是G<O, Record<keyof O, T>。我们现在可以消除Record<keyof O, T>[keyof O]{[K in keyof O]: T}[keyof O]并给您:

T

让我们对其进行测试:

F

这些按您想要的方式工作。我们还来看一些可能的边缘情况:

G

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

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

这可能会也可能不会。它没有违反约束。它只是具有额外的属性,通常在TypeScript中是允许的(但是forbidden in some situations)。推论是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 拒绝此类事情,但是它们更加复杂,这不是问题的一部分。


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

Link to code