如何传递一个泛型?

huangapple go评论71阅读模式
英文:

How to pass along a generic?

问题

我相对于TypeScript还很新,所以我仍然在努力理解泛型。

我有一个名为Client的类,其中有一个AxiosInstance属性。这用于将标头附加到每个请求。

我希望Client类具有与AxiosInstance相同的getpost方法。

这很容易。我会尽量简单化这个问题:

import Axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';

class Client {
  constructor(){
    this.axios = Axios.create();

    // Attach authorization to each request if it does not already exist.
    this.axios.interceptors.request.use((requestConfig: InternalAxiosRequestConfig) => {
      requestConfig.headers = {
        authorization: 'basic foobar',
        ...requestConfig.headers
      };

      return requestConfig;
    });
  }

  axios: AxiosInstance;

  get<T = any, R = AxiosResponse<T>>(...args: Parameters<AxiosInstance['get']>) {
    return this.axios.get<T, R>(...args);
  }

  post<T = any, R = AxiosResponse<T>>(...args: Parameters<AxiosInstance['post']>) {
    return this.axios.post<T, R>(...args);
  }
}

我的问题是,AxiosInstance.get()AxiosInstance.post 各自具有泛型参数 - 我是否说得正确?我如何将这些泛型参数传递给 Client.get()Client.post(),类似于我在args中所做的方式?

英文:

I'm fairly new to TypeScript, so I'm still trying to wrap my head around generics.

I have a class, Client, that has an AxiosInstance property. This is used to attach headers to each request.

I'd like the Client class to have the exact same get and post methods that an AxiosInstance has.

This is easy enough. I'll make it as simple as I can for this question:

import Axios, { type AxiosInstance, type InternalAxiosRequestConfig } from &#39;axios&#39;;

class Client {
  constructor(){
    this.axios = Axios.create();

    // Attach authorization to each request if it does not already exist.
    this.axios.interceptors.request.use((requestConfig: InternalAxiosRequestConfig) =&gt; {
      requestConfig.headers = {
        authorization: &#39;basic foobar&#39;,
        ...requestConfig.headers
      };

      return requestConfig;
    });
  }

  axios: AxiosInstance;

  get/*&lt;What do I put here?&gt;*/(...args: Parameters&lt;AxiosInstance[&#39;get&#39;]&gt;) {
    return this.axios.get/*&lt;What do I put here?&gt;*/(...args);
  }

  post/*&lt;What do I put here?&gt;*/(...args: Parameters&lt;AxiosInstance[&#39;post&#39;]&gt;) {
    return this.axios.post/*&lt;What do I put here?&gt;*/(...args);
  }
}

My question is, AxiosInstance.get() and AxiosInstance.post each have generics — am I even saying that correctly? How do I pass the generics, similar to how I've done with the args, from the Client.get() and Client.post() to the AxiosInstance.get() and AxiosInstance.post()?

答案1

得分: 1

TypeScript缺乏执行任意高级类型操作的能力,以便轻松编写此类操作所需的代码。特别是在不丢失信息的情况下,无法在泛型函数类型上使用类似Parameters<T>实用程序类型的方式。当您执行此操作时,泛型类型参数将被其约束替换:

type GenFn = <A extends string, B extends number>(a: A, b: B) => [A, B];

type P = Parameters<GenFn>;
// type P = [a: string, b: number] 👎

哎呀,AB已完全被擦除。而且您不能“传递”泛型类型参数给GenFn,因此不能直接编写类似以下内容的代码:

type P<A extends string, B extends number> = 
  Parameters<GenFn<A, B>> // 错误,这是无效的TypeScript。

TypeScript确实具有所谓的实例化表达式,您可以在其中获取泛型函数类型的并传递泛型类型参数,而无需调用它。但您需要让编译器认为您拥有该类型的值;它无法纯粹在类型级别上进行(如在microsoft/TypeScript#47607的此评论中提到的):

declare const genFn: GenFn; // 假装我们拥有此类型的值
type P<A extends string, B extends number> = Parameters<typeof genFn<A, B>>;
// type P<A extends string, B extends number> = [a: A, b: B] 👍

因此,我们可以查找在axios的index.d.ts文件中定义的AxiosInstanceget()post()方法签名:

class Axios {
  get<T = any, R = AxiosResponse<T>, D = any>(
    url: string, config?: AxiosRequestConfig<D>
  ): Promise<R>;

  post<T = any, R = AxiosResponse<T>, D = any>(
    url: string, data?: D, config?: AxiosRequestConfig<D>
  ): Promise<R>;
}

然后假装创建一个AxiosInstance的虚拟实例_dummyAxios,并使用实例化参数,例如typeof _dummyAxios.get<T, R, D>,如下所示:

declare const _dummyAxios: AxiosInstance; // 假装

class Client {
  axios!: AxiosInstance;

  get<T = any, R = AxiosResponse<T>, D = any>(
    ...args: Parameters<typeof _dummyAxios.get<T, R, D>>
  ) {
    return this.axios.get<T, R, D>(...args);
  }

  post<T = any, R = AxiosResponse<T>, D = any>(
    ...args: Parameters<typeof _dummyAxios.post<T, R, D>>
  ) {
    return this.axios.post<T, R, D>(...args);
  }
}

这就可以了。

但这并不是一个很好的方法。您仍然需要查看Axios的类型定义,以确定类型参数的数量、约束和默认值(即<T = any, R = AxiosResponse<T>, D = any>)因为没有更高级的类型操作来提供这些信息。而且您必须假装拥有_dummyAxios。如果您可以使用typeof this.axios而不是typeof _dummyAxios,那将更好,但this不允许出现在那里。即使...args: Parameters<typeof this.axios.post<T, R, D>> 也不是特别有帮助的文档。

由于您仍然需要从Axios的调用签名中复制一些内容,因此最好直接复制完整的调用签名并在不试图强制编译器执行它只能部分实现的类型操作的情况下使用它们。也就是说,只需复制并粘贴调用签名:

class Client {
  axios!: AxiosInstance;

  get<T = any, R = AxiosResponse<T>, D = any>(
    url: string, config?: AxiosRequestConfig<D>
  ) {
    return this.axios.get<T, R, D>(url, config);
  }

  post<T = any, R = AxiosResponse<T>, D = any>(
    url: string, data?: D, config?: AxiosRequestConfig<D>
  ) {
    return this.axios.post<T, R, D>(url, data, config);
  }
}

是的,当您编写它时会更繁琐,但当您阅读它或其他后来者阅读它时,它会更直观。这段代码需要什么样的输入更加明显,比任何涉及“虚拟泛型函数上的实例化表达式”的东西更容易解释。

[Playground链接到代码](https://www.typescriptlang.org/play?ts=5.0.4#code/JYWwDg9gTgLgBAQQB7AgZwDRwN5xgTzAFNEV0BJAOzRgENKBjIrA4uKmIqS2gG2VRoASkQCOAVyI0AwhEoAzYAHMWhEgPQi0kaszxrSgkRKkxZC5XAC+ceVAgg4Aclpk0TgNwAoLzxBSwWiY4ACkGPgAvHB8v

英文:

TypeScript lacks the ability to perform arbitrary higher-level type manipulation of the sort you'd need to write this easily. In particular there's no way to use something like the Parameters&lt;T&gt; utility type on a generic function type without losing information. The generic type parameters will be replaced with their constraints when you do that:

type GenFn = &lt;A extends string, B extends number&gt;(a: A, b: B) =&gt; [A, B];

type P = Parameters&lt;GenFn&gt;;
// type P = [a: string, b: number] &#128546;

Oops, A and B have been erased completely. And you can't "pass" generic type arguments to GenFn, so you can't directly write something like

type P&lt;A extends string, B extends number&gt; = 
  Parameters&lt;GenFn&lt;A, B&gt;&gt; // error, this is invalid TypeScript.

TypeScript does have what's called instantiation expressions, where you can take a value of a generic function type and pass generic type arguments to it without calling it. But you need the compiler to think you have a value of that type; it can't happen purely at the type level (as mentioned in this comment on microsoft/TypeScript#47607):

declare const genFn: GenFn; // pretend we have a value of this type
type P&lt;A extends string, B extends number&gt; = Parameters&lt;typeof genFn&lt;A, B&gt;&gt;;
// type P&lt;A extends string, B extends number&gt; = [a: A, b: B] &#128077;

So we could look up the get() and post() method signatures of AxiosInstance as defined in the index.d.ts file from axios, which are

class Axios {
  get&lt;T = any, R = AxiosResponse&lt;T&gt;, D = any&gt;(
    url: string, config?: AxiosRequestConfig&lt;D&gt;
  ): Promise&lt;R&gt;;

  post&lt;T = any, R = AxiosResponse&lt;T&gt;, D = any&gt;(
    url: string, data?: D, config?: AxiosRequestConfig&lt;D&gt;
  ): Promise&lt;R&gt;;
}

And then pretend to make a dummy instance _dummyAxios of AxiosInstance and use instantiation parameters like typeof _dummyAxios.get&lt;T, R, D&gt;, like this:

declare const _dummyAxios: AxiosInstance; // pretend

class Client {
  axios!: AxiosInstance;

  get&lt;T = any, R = AxiosResponse&lt;T&gt;, D = any&gt;(
    ...args: Parameters&lt;typeof _dummyAxios.get&lt;T, R, D&gt;&gt;
  ) {
    return this.axios.get&lt;T, R, D&gt;(...args);
  }

  post&lt;T = any, R = AxiosResponse&lt;T&gt;, D = any&gt;(
    ...args: Parameters&lt;typeof _dummyAxios.post&lt;T, R, D&gt;&gt;
  ) {
    return this.axios.post&lt;T, R, D&gt;(...args);
  }
}

And that works.


But it's not a wonderful approach. You still need to consult the Axios typings in order to determine the number, constraints, and defaults of the type parameters (i.e., &lt;T = any, R = AxiosResponse&lt;T&gt;, D = any&gt;) because there are no higher order type operations to give those to you. And you have to pretend to have _dummyAxios. It would be nice if you could at least use typeof this.axios instead of typeof _dummyAxios, but this isn't allowed to appear there. And even ...args: Parameters&lt;typeof this.axios.post&lt;T, R, D&gt;&gt; isn't particularly helpful as documentation.

Since you need to copy some things from the Axios call signatures anyway, you might as well just copy the full call signatures and use them without trying to force the compiler to perform type operations it can only sort of achieve. That is, just copy and paste the call signatures:

class Client {
  axios!: AxiosInstance;

  get&lt;T = any, R = AxiosResponse&lt;T&gt;, D = any&gt;(
    url: string, config?: AxiosRequestConfig&lt;D&gt;
  ) {
    return this.axios.get&lt;T, R, D&gt;(url, config);
  }

  post&lt;T = any, R = AxiosResponse&lt;T&gt;, D = any&gt;(
    url: string, data?: D, config?: AxiosRequestConfig&lt;D&gt;
  ) {
    return this.axios.post&lt;T, R, D&gt;(url, data, config);
  }
}

Yes, it's more tedious for you when you're writing it, but it's a lot more straightforward for you when you're reading it, or others who come later. It's much more obvious what this code expects as input, and much easier to explain than anything involving "instantiation expressions on phantom generic functions".

Playground link to code

huangapple
  • 本文由 发表于 2023年5月29日 07:42:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/76353997.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定