【问题标题】:Deduce property type of callback argument when using generics and keyof使用泛型和 keyof 时推断回调参数的属性类型
【发布时间】:2017-10-14 09:21:13
【问题描述】:

我正在努力编写一个代码来推断args.valueif 范围内的类型:

class Foo {
    public id: number;
    public name: string;
    public birth: Date;
}

interface ISetEventArgs<T> {
    field: keyof T;
    value: T[keyof T];
}

function bind<T>(obj: T, event: "set", handler: (args: ISetEventArgs<T>) => void): void {
    // Void
}

let f: Foo = new Foo();

bind<Foo>(f, "set", (args: IArgs<Foo>): void => {
    if (args.field === "id") {
        let id: number = args.value; // Error: Type 'string | number | Date' is not assignable to type 'number'.
    }
    else if (args.field === "name") {
        // ...
    }
    else if (args.field === "birth") {
        // ...
    }
});

我试图通过写这样的东西来解决这种情况,但感觉不对:

function getValue<T, K extends keyof T>(value: T[keyof T], key: K): T[K] {
    return value;
}

// Usage:
if (args.field === "id") {
    let id: number = getValue<Foo, "id">(args.value, args.field); // Correct type.
    // Can also be used as: getValue<Foo, "id">(args.value, "id");
}

有什么想法吗?即使解决方案需要使用辅助函数,我也非常希望能够以更干净的方式使用它,例如(如果可能的话)getValue&lt;Foo, "id"&gt;(args.value)getValue(args.value, args.field)

【问题讨论】:

标签: typescript typescript2.0


【解决方案1】:

我认为没有辅助函数就无法完成 - 打字稿类型推断没有考虑到 fieldvalue 的类型是相互依赖的。

所以你必须使用所谓的user-defined type guard function 来明确表达类型关系:

class Foo {
    public id: number;
    public name: string;
    public birth: Date;
}

interface ISetEventArgs<T> {
    field: keyof T;
    value: T[keyof T];
}

function bind<T>(obj: T, event: "set", handler: (args: ISetEventArgs<T>) => void): void {
    // Void
}

let f: Foo = new Foo();

// type guard
function argsForField<T, F extends keyof T>(args: ISetEventArgs<T>, field: F):
         args is { field: F; value: T[F]} {
    return args.field === field;
}

bind<Foo>(f, "set", (args: ISetEventArgs<Foo>): void => {
    if (argsForField(args, "id")) {
        let id: number = args.value; //no error
    }
    else if (argsForField(args, "name")) {
        let name: string = args.value
    }
    else if (argsForField(args, "birth")) {
        let birth: Date = args.value;
    }
});

【讨论】:

  • 谢谢@artem!这是那些迟到的问题之一:)
【解决方案2】:

这个问题整天困扰着我,所以我一直在玩弄它,虽然我没有解决方案(很遗憾),但我确实发现了一些有趣的行为,这些行为可能对你有帮助,也可能对你没有帮助,具体取决于您的确切用例。

TL;DR:你可以在 Foo 的特定情况下得到你想要的,但不是一般情况下。这似乎是对打字稿部分的限制。

首先,让我们将ISetEventArgs的字段和值绑定在一起:

interface ISetEventArgs<T, K extends keyof T> {
    field: K;
    value: T[K];
}

现在,问题是类型:

ISetEventArgs&lt;Foo, keyof Foo&gt;

解决:

ISetEventArgs&lt;Foo, "id"|"name|"birth"&gt;

但我们希望它是:

ISetEventArgs&lt;Foo, "id"&gt; | ISetEventArgs&lt;Foo, "name"&gt; | ISetEventArgs&lt;Foo, "birth"&gt;

因为在第二种情况下,我们可以利用 typescript 的可区分联合功能。在我看来,这些在语义上是相同的,但打字稿只会缩小第二种情况。所以我们需要做一些类型的恶作剧来把它变成那种形式。

所以,如果我们定义一个类型:

type FooArgs = {[K in keyof Foo]: ISetEventArgs&lt;Foo, K&gt;}[keyof Foo]

这可以解决我们想要的问题...但遗憾的是,如果我们尝试扩展此模式以使其适用于任何类型:

type GenericArgs<T> = {[K in keyof T]: ISetEventArgs<T, K>}[keyof T];
type GenricFooArgs = GenericArgs<Foo>;

突然GenericFooArgs 解析为上面的第一种类型,而不是第二种?!我不知道为什么手动声明 FooArgs 与使用 GenericArgs&lt;Foo&gt; 的结果不同。

因此,如果您使用FooArgs 代替ISetEventArgs&lt;T&gt;,您将在实现处理程序时得到您想要的。但是...你已经失去了bind 的通用能力,所以它可能不值得交易。

【讨论】:

  • 这看起来与this issue 非常相似,应该是fixed,但我不确定哪个版本 - 2.5 或 2.6。
猜你喜欢
  • 1970-01-01
  • 2017-06-02
  • 2019-10-06
  • 2020-03-12
  • 1970-01-01
  • 1970-01-01
  • 2021-06-11
  • 1970-01-01
相关资源
最近更新 更多