简体   繁体   English

扩展泛型函数参数类型 - Typescript

[英]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更新-> 我在这里添加了一个完整的代码示例

What I want to do我想做的事

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;

  ...
}

Type definitions类型定义

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:由于此类型必须使用ParametersReturnType对函数类型使用单个泛型进行解包,这有点愚蠢(如果您使用箭头函数,您可以使用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.任何一个都可能对你有用,但我认为这里的根本帮助是,对于多态性,你可以定义一个多个实现符合的类型,即使使用泛型,也有某种感觉Q1Q2具有相似的行为,而你可以尝试在基类中捕获它,无论您是否实际使用该基类。 (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.虽然类型ParamQueryType保持不变,但Query已更改为接受多个参数,并且我添加了一个额外的类型TypeCastTypeCast返回参数的类型。 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 names querySomething 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 and query2 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM