通用keyof和属性getter函数

时间:2017-11-20 19:24:52

标签: typescript generics keyof

我正在尝试观察数组中对象的属性。为了使其安全,我使用getter函数来获取包含要观察的实际属性的数组对象的子对象(如果需要)。

propertyName / key必须是一个字符串,因为我正在使用的观察库需要它。

getter应该能够接受一个返回与传入的类型相同的函数,比如o => o

您可以在下面找到简明示例:

function foo<A, B>(
    array: A[], 
    getter: (ao: A) => B, 
    key: keyof B, 
    callback: (value: B[keyof B]) => void
) {
    callback(getter(array[0])[key]);
}

foo([{b: {c: 1}}], a => a.b, "c", v => {});

这会引发Argument of type '"c"' is not assignable to parameter of type 'never'.

但以下确实有效:

function bar<A>(
    array: A[], 
    key: keyof A, 
    callback: (value: A[keyof A]) => void
) {
    callback(array[0][key]);
}

bar([{b: 1}], "b", v => {});

为什么编译器无法推断B的类型,是否有可以使用的解决方法?

2 个答案:

答案 0 :(得分:2)

关于为什么编译器不推断B,我没有明确的答案。我的直觉是,编译器更容易推断实际传递给函数的参数的类型,而不是仅仅推断与传入的参数的相关的类型。在这种直觉上,我将用B重写要替换Record<K,V>的类型,其中K是您实际传入的键,V是值的类型与该键关联的属性。

如果你不知道,Record<K,V>是TypeScript标准库的一部分,定义如下:

type Record<K extends string, T> = {
    [P in K]: T; 
}

然后该功能变为:

function foo<A, K extends string, V>(
    array: A[], 
    getter: (ao: A) => Record<K,V>, 
    key: K, // easier to infer K because you directly pass it in
    callback: (value: V) => void
) {
    callback(getter(array[0])[key]);
}

这似乎有效:

foo([{ b: { c: 1 } }], a => a.b, "c", v => { });

根据需要将参数推断为<{ b: { c: number; }; }, "c", number>

希望有所帮助;祝你好运!

更新

@TitianCernicovaDragomir指出以下呼叫:

foo([{ b: { c: 1, d: "" } }], a => a.b, "c", v => { });

被推断为

foo<{ b: { c: number; d: string; }; }, "c" | "d", string | number>(...)

此处的问题是,您希望K只是"c",而不是"c" | "d",但显然array参数在key之前已经过检查参数。 (我不认为Vstring | number的事实是实际问题,因为callback实际上取任何值。如果你传递的callback函数只是花了number然后失败了,那就是个问题。)

如果您发现某些类型参数以错误的顺序推断(意味着编译器使用参数来推断某些内容,但您希望它使用不同的参数),您可以通过相交来推断lower the priority输入{}

在这种情况下,我们可以这样做:

function foo<A, K extends string, V>(
  array: A[],
  getter: (ao: A) => Record<K & {}, V>, // delay inference of K
  key: K, 
  callback: (value: V) => void
) {
  callback(getter(array[0])[key]);
}

现在我们打电话

foo([{ b: { c: 1, d: "" } }], a => a.b, "c", v => { });

推断为

foo<{ b: { c: number; d: string; }; }, "c", {}>(...)

工作正常,因为回调没有做任何事情。如果我们打电话

foo([{ b: { c: 1, d: "" } }], a => a.b, "c", (v: number): void => { });

然后推断为

foo<{ b: { c: number; d: string; }; }, "c", number>(...)

哪个好,对吧?

我们需要支持哪些其他用例?

答案 1 :(得分:1)

与@jcalz类似,我不一定有理由,只是一种解决方法。如果您采用双呼叫方式,则可以在第一次通话中修复AB,并在第二次通话中发送keycallback,然后这些类型将被正确推断。

function foo2<A, B>(
    array: A[],
    getter: (ao: A) => B,
) {
    return function <K extends keyof B>(key: K, callback: (value: B[K]) => void) {
        callback(getter(array[0])[key]);
    }
}

foo2([{ b: { c: 1, d: "" } }], a => a.b)("d", v => { }); // v is string
foo2([{ b: { c: 1, d: "" } }], a => a.b)("c", v => { }); // v is number

就语法而言,它有点丑陋,但你可以获得完整的类型安全性。