使用泛型和 keyof 时推断 属性 类型的回调参数
Deduce property type of callback argument when using generics and keyof
我正在努力编写一个代码来推断 if
范围内 args.value
的类型:
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<Foo, "id">(args.value)
或 getValue(args.value, args.field)
我认为如果没有辅助函数就无法完成 - typescript 类型推断没有考虑到 field
和 value
的类型是相互依赖的。
所以你必须使用所谓的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;
}
});
这个问题困扰了我一整天,所以我试了一下,虽然我没有解决方案(很遗憾),但我确实发现了一些有趣的行为,这些行为可能对你有帮助,也可能没有帮助,取决于您的确切用例。
TL;DR:在 Foo 的特定情况下你可以得到你想要的,但一般情况下不行。这似乎是对打字稿的限制。
所以首先,让我们将 ISetEventArgs
的字段和值绑定在一起:
interface ISetEventArgs<T, K extends keyof T> {
field: K;
value: T[K];
}
现在,问题是类型:
ISetEventArgs<Foo, keyof Foo>
解析为:
ISetEventArgs<Foo, "id"|"name|"birth">
但我们希望它是:
ISetEventArgs<Foo, "id"> | ISetEventArgs<Foo, "name"> | ISetEventArgs<Foo, "birth">
因为在第二种情况下我们可以利用 typescript 的可区分联合功能。这些在我看来在语义上是相同的,但打字稿只会缩小第二种情况。所以我们需要做一些类型的恶作剧来把它变成那种形式。
所以,如果我们定义一个类型:
type FooArgs = {[K in keyof Foo]: ISetEventArgs<Foo, K>}[keyof Foo]
这解决了我们想要的问题......但遗憾的是,如果我们尝试扩展此模式以使其适用于任何类型:
type GenericArgs<T> = {[K in keyof T]: ISetEventArgs<T, K>}[keyof T];
type GenricFooArgs = GenericArgs<Foo>;
突然 GenericFooArgs
解析为上面的第一种类型,而不是第二种?!我不知道为什么手动声明 FooArgs
与使用 GenericArgs<Foo>
.
不同
因此,如果您使用 FooArgs
代替 ISetEventArgs<T>
,您将在实现处理程序时得到您想要的。但是...您已经失去了 bind
的通用能力,因此它可能根本不值得交易。
我正在努力编写一个代码来推断 if
范围内 args.value
的类型:
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<Foo, "id">(args.value)
或 getValue(args.value, args.field)
我认为如果没有辅助函数就无法完成 - typescript 类型推断没有考虑到 field
和 value
的类型是相互依赖的。
所以你必须使用所谓的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;
}
});
这个问题困扰了我一整天,所以我试了一下,虽然我没有解决方案(很遗憾),但我确实发现了一些有趣的行为,这些行为可能对你有帮助,也可能没有帮助,取决于您的确切用例。
TL;DR:在 Foo 的特定情况下你可以得到你想要的,但一般情况下不行。这似乎是对打字稿的限制。
所以首先,让我们将 ISetEventArgs
的字段和值绑定在一起:
interface ISetEventArgs<T, K extends keyof T> {
field: K;
value: T[K];
}
现在,问题是类型:
ISetEventArgs<Foo, keyof Foo>
解析为:
ISetEventArgs<Foo, "id"|"name|"birth">
但我们希望它是:
ISetEventArgs<Foo, "id"> | ISetEventArgs<Foo, "name"> | ISetEventArgs<Foo, "birth">
因为在第二种情况下我们可以利用 typescript 的可区分联合功能。这些在我看来在语义上是相同的,但打字稿只会缩小第二种情况。所以我们需要做一些类型的恶作剧来把它变成那种形式。
所以,如果我们定义一个类型:
type FooArgs = {[K in keyof Foo]: ISetEventArgs<Foo, K>}[keyof Foo]
这解决了我们想要的问题......但遗憾的是,如果我们尝试扩展此模式以使其适用于任何类型:
type GenericArgs<T> = {[K in keyof T]: ISetEventArgs<T, K>}[keyof T];
type GenricFooArgs = GenericArgs<Foo>;
突然 GenericFooArgs
解析为上面的第一种类型,而不是第二种?!我不知道为什么手动声明 FooArgs
与使用 GenericArgs<Foo>
.
因此,如果您使用 FooArgs
代替 ISetEventArgs<T>
,您将在实现处理程序时得到您想要的。但是...您已经失去了 bind
的通用能力,因此它可能根本不值得交易。