英文:
Depending Generic ResultType in Typescript
问题
Hi everyone!
interface Data {
date: Date;
message?: string;
}
interface DataB {
dateB: Date;
messageB?: string;
}
class DataResolver {
public submit(): Data {
return { date: new Date() };
}
public submitB(): DataB {
return { dateB: new Date() };
}
}
interface StringKeyedObject {
[key: string]: any;
}
class Handler<C extends StringKeyedObject> {
send<
K extends Extract<keyof C, string>,
L extends ReturnType<Extract<keyof K, object>>,
>(eventName: K, arg: L) {
//
}
}
const handler = new Handler<DataResolver>();
handler.send('submit', null);
我真的希望根据第一个value
参数设置第二个arg
参数。在VS Code的自动完成中,建议使用submit
和submitB
,这正是我想要的。如果我选择submit
,我希望arg
的类型为Data
。如果我选择submitB
,类型应该为DataB
。
我的尝试使用泛型类型L extends ReturnType<Extract<keyof K, object>>
完全不起作用。显然。
希望你能找到一个解决方案!谢谢,一切顺利! 😊
英文:
😊 Hi everyone!
interface Data {
date: Date;
message?: string;
}
interface DataB {
dateB: Date;
messageB?: string;
}
class DataResolver {
public submit(): Data {
return { date: new Date() };
}
public submitB(): DataB {
return { dateB: new Date() };
}
}
interface StringKeyedObject {
[key: string]: any;
}
class Handler<C extends StringKeyedObject> {
send<
K extends Extract<keyof C, string>,
L extends ReturnType<Extract<keyof K, object>>,
>(eventName: K, arg: L) {
//
}
}
const handler = new Handler<DataResolver>();
handler.send('submit', null);
I really would like to set the second arg
parameter depending on the first value
paramter. In VS Code autocomplete suggests submit
and submitB
as intended. 😊 Now if I select submit
I would like to be arg of type Data
. If I select submitB
the type should be DataB
.
My attempt with generic type L extends ReturnType<Extract<keyof K, object>>
does not work at all. Obviously.
I hope you'll find a solution! Thanks an all the best! 😊
答案1
得分: 1
这段代码中,Handler
类接受一个泛型参数 C
,其类型必须为拥有字符串键的对象。send
方法接受两个参数,value
是字符串类型的键,arg
的类型依赖于 C[K]
的类型。如果 C[K]
是函数类型,则 arg
的类型为该函数的返回值类型;如果 C[K]
不是函数类型,则 arg
的类型为 never
。这确保了 arg
参数的类型与输入的键对应的属性类型一致。
英文:
I would write it like this:
class Handler<C extends StringKeyedObject> {
send<K extends Extract<keyof C, string>>(
value: K, arg: C[K] extends (...args: any) => infer R ? R : never) {
// do stuff
}
}
Here the arg
parameter type depends on K
. It is essentially the same as ReturnType<C[K]>
, so let's first look at that version first:
class Handler<C extends StringKeyedObject> {
send<K extends Extract<keyof C, string>>(
value: K, arg: ReturnType<C[K]> ) {
// do stuff
}
}
The type C[K]
is an indexed access type meaning "the property type you'd read when indexing into an object of type C
with a key of type K
". It is presumably what you were trying to do with keyof K
, except that the keyof
operator on K
would give you "the keys of the keys of C", which is like, whatever keys a string has... toUppercase
and such. Not what you wanted.
And note that, unless you have a special reason, you don't need to have a separate type parameter for arg
. You could have written L extends ReturnType<C[K]>
and used L
, but we don't need some arbitrary subtype of ReturnType<C[K]>
for this to work, so there's no reason to add another type parameter.
So, given this definition, let's make sure it works:
class DataResolver {
public submit(): Data { return { date: new Date() }; }
public submitB(): DataB { return { dateB: new Date() }; }
a = 2 // I added this
}
const handler = new Handler<DataResolver>();
handler.send("submit", { date: new Date() }); // okay
handler.send("submitB", { dateB: new Date() }); // okay
handler.send("a", "wha") // <-- shouldn't compile but it does
Everything works the way you want, except... if your DataResolver
happens to have a non-function property, we don't want the compiler to accept any call to handler.send()
for that property. One way to do that is to check whether the property value is a function type, and if so, use its return type; and if not, use the impossible never
type. That brings us back to:
class Handler<C extends StringKeyedObject> {
send<K extends Extract<keyof C, string>>(
value: K, arg: C[K] extends (...args: any) => infer R ? R : never) {
// do stuff
}
}
where arg
is a conditional type that depends on C[K]
. The use of infer
lets us extract out the return type as its own type parameter R
and use it.
By the way, this is very similar to how ReturnType<T>
is defined:
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
except that this version gives the any
type when T
isn't a function, and since any
accepts anything, the call to handler.send("a", "wha")
succeeds when it shouldn't.
Okay, let's test it one more time:
const handler = new Handler<DataResolver>();
handler.send("submit", { date: new Date() }); // okay
handler.send("submitB", { dateB: new Date() }); // okay
handler.send("a", "wha") // error!
// -------------> ~~~~~
// Argument of type 'string' is not assignable to parameter of type 'never'.
Looks good. There are other ways to harden send
against bad inputs (we could make it so that you're not even allowed to give it keys corresponding to non-functions) but I don't want to digress even further from the question as asked.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论