[英]Extending generic function parameter type - Typescript
I have just recently started to play around with advanced types and generics in Typescript so please excuse if my formulation is not entirely correct - at least I hope the title makes sense.我最近才开始在 Typescript 中使用高级类型和泛型,所以如果我的表述不完全正确,请原谅 - 至少我希望标题有意义。
UPDATE -> I have added a full code example here更新-> 我在这里添加了一个完整的代码示例
Assume two (query) functions, where one takes only an object as parameter and the other one takes a query string and an object as parameter.假设有两个(查询)函数,其中一个只接受一个对象作为参数,另一个接受一个查询字符串和一个对象作为参数。
// only object
const query1 = ({ ...params }) => { return ...; }
// query AND object
const query2 = (query, { ...params }) => { return ...; }
Both functions are contained in separate classes where they also get typed这两个函数都包含在单独的类中,它们也被输入
class Q1 {
private query1: Query1;
...
}
class Q2 {
private query2: Query2;
...
}
Basically, what I am trying to achieve is that both function types derive from a single generic type.基本上,我想要实现的是两种函数类型都源自单个泛型类型。 What I have so far is
到目前为止我所拥有的是
// the generic object type --> I guess here I would need to define it differently
type Param<T> = { [key in keyof T]: T[key] }
// the generic query type which contains `params` and `response` types
type QueryType<P = any, R = any> = {
params: Param<P>
response: Promise<R>
}
// the generic function type for my queries
type Query<P = any, R = any> =
<T extends QueryType<P,R>>( params: Pick<T, 'params'>['params'] ) => Pick<T, 'response'>['response']
For the first case query1()
I do not really need to exend anything.对于第一种情况
query1()
我真的不需要扩展任何东西。 I only need to apply the param and response types , eg我只需要应用参数和响应类型,例如
interface Input {
param1: string,
param2: number,
param3: boolean
}
type QuerySomething = QueryType<Input, string>
class Q1 {
private query1: Query;
// just for completeness - I do not wanna type out `query1` here
constructor( query1: Query ) {
this.query1 = query1;
}
// and I would use this somehow like
public async querySomething(input: Input): Promise<string> {
const res = await this.query1<QuerySomething>( input );
return res;
}
}
For the second case I simply can't figure out how to extend type Query
that it will accept a parameter query: string
in addition to the params object, without rewriting type Query
.对于第二种情况,我根本无法弄清楚如何扩展
type Query
以使其接受参数query: string
除了 params 对象,而不重写type Query
。 So所以
interface Input {
param1: string,
param2: number,
param3: boolean
}
// I guess I have to do something here?
type QuerySomething = QueryType<Input, string>
class Q2 {
private query2: Query;
...
public async querySomething(query: string, params: Input): Promise<string> {
const res = await this.query2<QuerySomething>( query, params );
return res;
}
}
What I could do of course我当然可以做什么
type Query2<P = any, R = any> =
<T extends QueryType<P,R>>( query:string, params: Pick<T, 'params'>['params'] ) => Pick<T, 'response'>['response']
I hope this makes sense!我希望这是有道理的! I am still really confused with Typescript and I apologise if this is complete nonsense here!
我仍然对 Typescript 感到困惑,如果这完全是胡说八道,我深表歉意!
Thanks!谢谢!
One thing that may help is to take a step back and try to describe a base abstract class that both Q
classes should implement, you might come up with something like this:可能有帮助的一件事是退后一步并尝试描述两个
Q
类都应该实现的基本抽象类,您可能会想出这样的东西:
abstract class Q_Base {
public abstract querySomething(...args: any[]): Promise<any>
}
However we obviously want the type of arguments and return type to be constrained, we might consider taking a generic that is just the entire call signature of that method:然而,我们显然希望参数类型和返回类型受到约束,我们可能会考虑采用一个泛型,它只是该方法的整个调用签名:
abstract class Q_Base2<Query_signature extends (...args:any[])=>Promise<any>> {
public abstract querySomething(...args:Parameters<Query_signature>): ReturnType<Query_signature>;
}
interface Q1Input {
// I assume this is well defined for you
}
class Q1 extends Q_Base2<(input: Q1Input)=>Promise<string>>{
public async querySomething(input: Q1Input): Promise<string>{
return ""
}
}
Since this type has to be unwrapped with Parameters
and ReturnType
using a single generic for the function type is a little silly (if you were using arrow functions you could use public abstract querySomething: Query_Signature
but that is up to you) so instead perhaps we could use 2 generics to represent the arguments and return type seperately:由于此类型必须使用
Parameters
和ReturnType
对函数类型使用单个泛型进行解包,这有点愚蠢(如果您使用箭头函数,您可以使用public abstract querySomething: Query_Signature
但这取决于您),所以也许我们可以使用 2 个泛型分别表示参数和返回类型:
abstract class Q_Base3<Params extends any[], R extends Promise<any>> {
// could also leave no retriction on R and return type Promise<R> depending on which semantics are easier for you
public abstract querySomething(...args: Params): R
}
class Q1_3 extends Q_Base3<[input: Q1Input], Promise<string>>{
// this was filled in with the quick fix of Q1_3 does not implement necessary abstract methods
public querySomething(input: Q1Input): Promise<string> {
throw new Error("Method not implemented.");
}
}
Either of those will likely work for you, but I think the root aid here is the idea that for polymorphism you can define one type that multiple implementations conform to, even with a generic there is some sense that Q1
and Q2
have similar behaviour and you can try to capture that in a base class, whether or not you actually use that base class.任何一个都可能对你有用,但我认为这里的根本帮助是,对于多态性,你可以定义一个多个实现符合的类型,即使使用泛型,也有某种感觉
Q1
和Q2
具有相似的行为,而你可以尝试在基类中捕获它,无论您是否实际使用该基类。 (it is just as valid to define the generics on Q1 and Q2 respectively) (分别在 Q1 和 Q2 上定义泛型同样有效)
After trying out multiple different approaches over the last couple of days I have a (kind of) satisfying solution for my problem.在过去几天尝试了多种不同的方法后,我为我的问题找到了一个(某种)令人满意的解决方案。 I wan't to thank @Tadhg McDonald-Jensen for taking time and pushing me into the right direction!
我 不想感谢@Tadhg McDonald-Jensen花时间把我推向正确的方向!
So, I have changed and simplified the generic type definitions所以,我改变并简化了泛型类型定义
// the generic object type
type Param<T> = { [key in keyof T]: T[key] }
// the generic query type
type QueryType<P, R> = {
params: Param<P>
response: Promise<R>
}
// generic typecast
type TypeCast<T> = Param<T>[keyof T]
// the generic query type
type Query<P = any, R = any> =
<T extends QueryType<P, R>>( ...args: TypeCast<T['params']>[] ) => T['response']
While type Param
and QueryType
remain the same Query
has changed to accept multiple arguments and I have added an additional type TypeCast
, which simply returns the type of the parameter.虽然类型
Param
和QueryType
保持不变,但Query
已更改为接受多个参数,并且我添加了一个额外的类型TypeCast
, TypeCast
返回参数的类型。 So if we go back to the original example we would get所以如果我们回到最初的例子,我们会得到
// the input parameters
interface Input {
param1: string
param2: number
param3: boolean
}
// the type which we want to apply to the query function
type QuerySomething = QueryType<{ query: string, input: Input }, string>
// testing
type test = TypeCast<QuerySomething['params']>
// resolves to type test = string | Input
Applying this to the query function will give the desired results将此应用于查询功能将给出所需的结果
I probably should mention, that my initial idea was not to have the same class method namesquerySomething
in each class.我可能应该提一下,我最初的想法是不要在每个类中使用相同的类方法名称
querySomething
。 This methods just represent a superset of many different class methods.这些方法只是代表了许多不同类方法的超集。 The distinction between the 2 classes has been implemented since,
query1
represents the fetch method from one external API endpoint andquery2
another API endpoint.这两个类之间的区别已经实现,
query1
代表来自一个外部 API 端点的 fetch 方法,而query2
另一个 API 端点。
type QuerySomething = QueryType<{ input: Input }, string>
type QuerySomethingElse = QueryType<{ query: string, input: Input }, string>
class Q {
// just for simplification, these methods would be in different classes
private query1: Query;
private query2: Query;
constructor( query1: Query ) {
this.query1 = query1;
this.query2 = query2;
}
public async querySomething( query: string, input: Input ): Promise<string> {
const res = await this.query1<QuerySomething>( input );
return res;
}
public async querySomethingElse( query: string, input: Input ): Promise<string> {
const res = await this.query2<QuerySomethingElse>( query, input );
return res;
}
}
This will resolve to这将解决
// Typechecks
const res = await this.query1<QuerySomething>( input ); // OK
const res = await this.query1<QuerySomething>( query ); // ERROR -> OK
const res = await this.query2<QuerySomethingElse>( query, input ); // OK
const res = await this.query2<QuerySomethingElse>( input, query ); // ERROR -> OK
// Argument checks
const res = await this.query1<QuerySomething>(); // OK -> should be ERROR
const res = await this.query1<QuerySomething>( input, input ); // OK -> should be ERROR
const res = await this.query2<QuerySomethingElse>( query, input, query ); // OK -> should be ERROR
const res = await this.query2<QuerySomethingElse>( query, input, input ); // OK -> should be ERROR
So basically, it does the right thing but by type definition does not take care about the number of arguments .所以基本上,它做了正确的事情,但根据类型定义,它不关心参数的数量。 If any of the Typescript wizards knows how to do this, I'd be happy to hear their solution!
如果任何 Typescript 向导知道如何执行此操作,我很高兴听到他们的解决方案! For now I think this is the best I can do.
目前我认为这是我能做的最好的事情。
If anyone is interested, I have updated the TS Playground.如果有人感兴趣,我已经更新了TS Playground。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.