Axios: Como usar o Axios com TypeScript ao usar interceptores de resposta (problema do AxiosResponse)

Criado em 30 abr. 2018  ·  50Comentários  ·  Fonte: axios/axios

Resumo

Em um projeto que estou migrando para TypeScript (TS), tenho um interceptor de resposta r => r.data . Como informo ao TS que não estou esperando um tipo de AxiosResponse ? Eu tentei substituir usando as Array<... mas isso não funciona, pois AxiosResponse não pode ser convertido como Array (por exemplo, não tem .length).

Obrigado!

Contexto

  • versão axios: 0.16.2
  • Ambiente: código do Visual Studio

Comentários muito úteis

Eu substituo AxiosResponse no meu axios.d.ts :

import axios from 'axios'

declare module 'axios' {
  export interface AxiosResponse<T = any> extends Promise<T> {}
}

Todos 50 comentários

Use a definição de estilo axios.request<T>(...args) .
O último interceptador de resposta no array obedece implicitamente a uma interface como (currentResponse: any) => T

Então, se você tem data sendo algo como:

interface ServerResponse {
  data: ServerData
}

interface ServerData {
  foo: string
  bar: number
}

Então você pode dizer:

axios.request<ServerData>({
  url: 'https://example.com/path/to/data',
  transformResponse: (r: ServerResponse) => r.data
}).then((response) => {
  // `response` is of type `AxiosResponse<ServerData>`
  const { data } = response
  // `data` is of type ServerData, correctly inferred
})

@zcei ooh, na verdade, isso não funcionaria para interceptadores globais, por exemplo axios.interceptors.response.use

Eu acho que sim - os interceptores são atualmente muito "relaxados" digitados (aka any ), então você pode simplesmente anexar o interceptor de resposta e tê-lo (r: any): any => r.data (que é basicamente como se você tivesse omitir qualquer digitação no modo não estrito).

Somente no ponto em que você chama axios.request você pode configurar o genérico, que então é passado como o tipo da propriedade data .

(Na verdade eu cometi um erro no trecho de código acima, data é na verdade AxiosResponse<ServerData> que tem um campo chamado .data com o tipo ServerData - atualizado)

@zcei oi desculpe, mas isso realmente não funciona com o meu exemplo, criei um codeandbox para destacar esse problema.

Confira aqui

Tendo o mesmo problema, o interceptor pode ser essencialmente resumido em:

this.instance.interceptors.response.use(response => response.data);

Apesar disso, o tipo de retorno de

this.instance.post<string>(...);

é AxiosPromise<string> , que espera que os dados sejam agrupados.

Isso é intencional. Você deve transformar os dados no interceptor, mas não içar as chaves de resposta .

https://github.com/axios/axios/blob/0b3db5d87a60a1ad8b0dce9669dbc10483ec33da/lib/core/dispatchRequest.js#L63 -L67

Eu pensei que você teria uma resposta do servidor como

{
  "data": {
    "foo": 42
  }
}

e queria se livrar do aninhamento response.data.data . Se você retornar response.data no interceptor, poderá acessá-lo posteriormente via response.data.foo em vez de response.data.data.foo .

Mas acessar response.foo não funcionaria, pois este é o nível de resposta "raiz" que acompanha outras coisas, como o código de resposta e similares.

Desculpe, mas não concordo com sua premissa, pelo menos não em termos de funcionalidade.
Sem usar o interceptor para desembrulhar a propriedade data, eu teria que fazer

this.instance.post<string>(...).then(response => response.data);
this.instance.get<number>(...).then(response => response.data);

em _todos_ endpoints que defino. Qual é o ponto em toda essa repetição de código?

Sempre que uma solicitação é bem-sucedida, o que significa then , eu realmente não me importo com o código de resposta etc. Eu só me importo com códigos de resposta se algo _não_ funcionar, e é para isso que existem manipuladores de erro , tanto no interceptor quanto em endpoints específicos.

this.instance.interceptors.response.use(response => {
  // Everything went well, pass only relevant data through
  return response.data;
}, error => {
  // Something went wrong, figure out how to handle it here or in a `.catch` somewhere down the pipe
});

Sempre que uma solicitação é bem-sucedida, o que significa então, eu realmente não me importo com o código de resposta

Bem, você pode não se importar com nenhuma outra informação, mas restringir a todos que usam um cliente HTTP que você só se importa com o corpo não é realmente uma solução.
Existem casos de uso legítimos suficientes para códigos de status e cabeçalhos, mesmo em chamadas bem-sucedidas. Como fazer distinções em 204 vs 200 , verificar cabeçalhos de limite de taxa, extrair cabeçalhos Link para recursos adicionais (paginação), etc. pp.

Se você não se importa, embrulhe o Axios e jogue tudo fora:

this.instance.request = <T>(...args) => {
  return axios.request<T>(...args).then(({ data }) => data)
}

this.instance.request<string>({ method: 'post', ... }).then(data => { ... });
this.instance.request<number>({ method: 'get', ... }).then(data => { ... });

Não vejo de onde você tirou a noção de que todos deveriam fazer as coisas do jeito que eu faço em um projeto. Estou simplesmente tentando resolver um problema de como desempacotar dados automaticamente em todas as solicitações e, como esse problema estava aqui antes de mim, não sou o único com esse problema.

Interceptores _parecem_ como o lugar correto para fazê-lo, tanto pelo nome quanto pela descrição no leia-me (fazer algo com cada resposta). Manipular 204 vs 200 etc e, em seguida, passar os dados também faria sentido em um interceptor, porque você pode fazer isso em um local central.

Na minha opinião, a solução intuitiva seria retornar o que você quiser do interceptor, não ter a biblioteca forçando de uma maneira única.

Se você quiser transformar alguns campos em dados:

this.instance.interceptors.response.use(response => {
  response.data.foo = JSON.parse(response.data.bar);
  return response;
});

Se você quiser apenas desempacotar os dados do Axios:

this.instance.interceptors.response.use(response => response.data);

Isso deixaria a escolha do que fazer para o desenvolvedor, o que na minha opinião é melhor do que um interceptador com opiniões muito fortes. Você é livre para discordar, é claro.

Você estava raciocinando sobre "o ponto em toda essa repetição de código", então tentei explicar por que a informação é necessária.

Eu realmente não consigo encontrar uma maneira de como você gostaria de manter a segurança de tipo nos interceptores e na resposta, se algum interceptador pudesse embaralhar toda a resposta sem uma estrutura comum. :/
O tipo de resposta para uma solicitação precisaria se tornar any eu acho, e colocar o esforço nas mãos dos desenvolvedores para garantir que eles façam a coisa correta.

Isso para mim pelo menos parece mais propenso a erros devido às tipagens relaxadas do que acessar uma propriedade.

Se você puder criar um PR que permita que os interceptores sobrescrevam a resposta do root, mantendo a segurança de tipo para "os 80% de casos de uso", eu ficaria mais do que feliz!

O problema é que ambos os exemplos acima que fiz já funcionam da maneira que descrevi, em termos de funcionalidade, são apenas os tipos que estão errados. Se retornar algo diferente da resposta (modificada) no interceptor estiver errado, acho que seria bom atualizar os tipos esperados lá.

Primeiro é interessante saber que você está fazendo a mesma coisa @Etheryte !

Em nosso aplicativo, o r => r.data é o interceptador de resposta final na cadeia e usamos outros que dependem de códigos de status para lidar com tokens de atualização, etc., mas em um nível global, pois não há necessidade de lidarmos com isso para chamadas.

Entendo que talvez não seja possível acomodar esse caso de uso com o TS. No entanto, @zcei é inegável que esta é uma maneira legítima de usar o Axios, pois está usando uma parte oficial da API do Axios (interceptores) :).

Ah! Eu estive olhando para o código errado o tempo todo 🤦‍♂️ e estava na seção transformResponse , não no interceptor - sinto muito!

Como você atualizaria as tipagens para levar em conta o interceptor alterando o tipo de retorno?

Talvez algo assim?

interface AxiosInstance {
  request<T = any, R = AxiosResponse<T>> (config: AxiosRequestConfig): Promise<R>;
}

Você chamaria assim:

axios.request<User, string>({
  method: 'get',
  url: '/user?id=12345'
})
  .then((foo: string) => { // <-- you could leave out the type annotation here, it's inferred
    console.log(foo)
  })

Apenas tentei isso localmente e parece fazer o que você está procurando.
Talvez eu possa reunir um PR à noite para atualizar isso para todos os métodos.

@zcei Que bom! Default para AxiosResponse, é claro, faz sentido 99% das vezes 👍

Ótimo ouvir! Desculpe novamente pela minha confusão 😓

@zcei não é um problema! Por curiosidade, qual é o ciclo de lançamento do Axios?

Não há nenhum - ainda tenho algumas coisas pendentes para o alfa v1 (#1333) e enquanto isso @nickuraltsev / @emilyemorehouse estão fazendo lançamentos sempre que necessário.
Desde v0.18 tem havido alguma tração (incluindo o meu favorito: opções de escopo para instâncias), então acho que eles poderiam cortar um lançamento. Para mais, gostaria de convidá-lo para o Gitter , para que possamos manter os problemas no ponto 🙂

Eu esperava lançar uma versão 0.19, mas pela última vez que verifiquei, o mestre estava falhando no CI. Eu definitivamente gostaria de um cronograma de lançamento mais sólido/regular assim que chegarmos ao 1.0.

enfrentar o mesmo problema. Existem soluções?

@qidaneix você pode tentar instalar npm install axios/axios#master até que haja um lançamento. #1605 deveria ter consertado. Seria bom receber algum feedback, se isso realmente ajuda em casos de uso da vida real e não apenas em testes 🙂

@zcei vou testar amanhã

@zcei Posso perguntar quando será lançado?

+1 quando 1.0.0 será lançado?

Esta será uma versão pré-1.0 com certeza. @Khaledgarbaya você também foi adicionado ao NPM por Matt? Caso contrário, precisamos ser legais com os mantenedores restantes liberando-o 😉

Olá a todos. Atualmente estou gerenciando os lançamentos do NPM. Eu adoraria lançar outra versão pré-1.0.0, mas os testes do Travis estão falhando e ainda não tive a chance de corrigi-los. Assim que estiverem consertados, ficarei mais do que feliz em liberar isso imediatamente 😬

@zcei não fui adicionado ao repositório npm, só posso mesclar alterações

Existe alguma atualização sobre esse problema, haverá um lançamento em um futuro próximo, como o segundo mês?

+1

❤️ se isso pudesse ser lançado agora

Estamos cientes, mas bloqueados no momento. 🙁 Você pode obter mais informações aqui: https://github.com/axios/axios/issues/1657#issuecomment -410766031

Lançado como parte de 0.19.0-beta.1.

Isso pode ser instalado usando npm install [email protected] ou npm install axios@next

Talvez algo assim?

interface AxiosInstance {
  request<T = any, R = AxiosResponse<T>> (config: AxiosRequestConfig): Promise<R>;
}

Você chamaria assim:

axios.request<User, string>({
  method: 'get',
  url: '/user?id=12345'
})
  .then((foo: string) => { // <-- you could leave out the type annotation here, it's inferred
    console.log(foo)
  })

Apenas tentei isso localmente e parece fazer o que você está procurando.
Talvez eu possa reunir um PR à noite para atualizar isso para todos os métodos.

User(T, o primeiro parâmetro genérico) parece não ser usado, se eu quiser usar tipos de retorno personalizados, fica estranho 😕

axios.request<void, string>({
    ...
})

usando void pode ser mais claro.

@emilyemorehouse Para não parecer ingrato, mas o 0.19-beta está aberto há três meses, existe um ETA para um lançamento GA? Nosso projeto tem um problema que requer uma dessas correções. Eu perguntei isso no canal Gitter, mas não parece que os mantenedores respondem por lá ...

Talvez uma redação melhor seria quais são os problemas que precisam ser resolvidos antes de um lançamento e há um lugar para rastreá-lo? Como parece haver muito interesse, espalhar o trabalho e ter uma compreensão clara do trabalho necessário pode ajudar a acelerar as coisas.

Já existe um marco do projeto para 0,19, mas os tickets listados não sofreram alterações por vários meses.

const func1: qualquer = () => { return axios.request(...) }

Eu substituo AxiosResponse no meu axios.d.ts :

import axios from 'axios'

declare module 'axios' {
  export interface AxiosResponse<T = any> extends Promise<T> {}
}

Batendo essa questão. Funciona... tudo bem quando acabei de copiar a definição AxiosInstance para tipagens locais, mas a solução implementada é muito detalhada na minha opinião, a menos que eu esteja fazendo algo errado (não um especialista em Typescript). Como estou usando uma instância axios separada criada com axios.create e usando este interceptor:

AxiosClient.interceptors.response.use(
  response => response && response.data,
  error => error && error.response && error.response.data
);

onde response.data sempre tem esta forma:

export interface APIResponse<T = any> {
  data: T;
  message: string;
  status: boolean;
}

parece que eu tenho que usar AxiosClient.post assim:

AxiosClient.post<EndpointAPIResponse, APIResponse<EndpointAPIResponse>>('/endpoint', { someData });

ter tipos apropriados em .then . Estou fazendo algo errado aqui ou realmente deveria ser tão detalhado? Caso contrário, seria muito melhor se eu pudesse apenas passar o esquema de resposta esperado ao criar a instância, mas não consigo fazê-lo funcionar:

export interface AxiosInstance<R> {
  <T = any>(config: AxiosRequestConfig): Promise<R<T>>;
  <T = any>(url: string, config?: AxiosRequestConfig): Promise<R<T>>;
  defaults: AxiosRequestConfig;
  interceptors: {
    request: AxiosInterceptorManager<AxiosRequestConfig>;
    response: AxiosInterceptorManager<R>;
  };
  getUri(config?: AxiosRequestConfig): string;
  request<T = any>(config: AxiosRequestConfig): Promise<R<T>>;
  get<T = any>(url: string, config?: AxiosRequestConfig): Promise<R<T>>;
  delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<R<T>>;
  head<T = any>(url: string, config?: AxiosRequestConfig): Promise<R<T>>;
  post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R<T>>;
  put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R<T>>;
  patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R<T>>;
}

export interface AxiosStatic extends AxiosInstance<AxiosResponse> {
  create<T = AxiosResponse>(config?: AxiosRequestConfig): AxiosInstance<T>;
  Cancel: CancelStatic;
  CancelToken: CancelTokenStatic;
  isCancel(value: any): boolean;
  all<T>(values: (T | Promise<T>)[]): Promise<T[]>;
  spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R;
}

Funciona bem com axios.create() sem um tipo genérico ou apenas axios , mas se eu passar minha interface assim:

const AxiosClient = axios.create<APIResponse>({
  // ...
});

e depois use assim: AxiosClient.post<string>('/endpoint').then(value => value.data) , value.data has type... T. Além disso, a versão acima só funciona se eu realmente substituir essas tipagens em node_modules, caso contrário fica totalmente confuso e eu acabar com algum desastre total. Alguém poderia me ajudar com isso?

Edit: ok, acho que não funcionará porque não é possível usar genéricos dessa maneira (então R<T> quando R é um tipo genérico não é a sintaxe correta, mas acho que o WebStorm por algum motivo não destacou pra mim); https://github.com/Microsoft/TypeScript/issues/1213 isso, suponho, resolveria, mas não faço ideia se alguma vez for implementado.

const request = <T>(options: AxiosRequestConfig) => {
    return axios.request<IResponse<T>>(options).then<T>(response => {
        const data = response.data;
        if (data.c !== '0') {
            return Promise.reject(new Error(data.m || 'error'));
        }
        return data.d;
    });
}

usar:

request<IApiGetBrandGoodsInfoByIds>({
        method: 'GET',
        url: '/api/a/b',
    });

@zcei Isso já foi resolvido? Não consigo fazer o seguinte funcionar:

// so I created an axios instance:
const instance = axios.create(someOptions);

// then used interceptors
instance.interceptors.response.use((res) => res.data.data);   // server response will always have 'data'

// then when using the following to make a request
instance.get<string>(someURI);  // suppose server response was {data: 'some message'}

// ^^ the above returns type AxiosPromise<string>. How do I get it to return type Promise<string> instead?

tendo o mesmo problema, eu instalo a versão 0.19beta, também não consigo analisar o tipo correto

Eu substituo AxiosResponse no meu axios.d.ts :

import axios from 'axios'

declare module 'axios' {
  export interface AxiosResponse<T = any> extends Promise<T> {}
}

oh niupi

Eu uso o interceptor para fazer algo como: response => response.data . Então eu preciso remover o wrapper AxiosResponse completamente.

Eu finalmente acabo com:

import axios from 'axios'

declare module 'axios' {
  export interface AxiosInstance {
    request<T = any> (config: AxiosRequestConfig): Promise<T>;
    get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
    delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
    head<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
    post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
    put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
    patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
  }
}

Caso alguém não saiba como usar:

  1. Escreva os tipos em algum lugar, por exemplo, src/types/axios/axios.d.ts .
  2. Atualize seu tsconfig.json , por exemplo
{
  "compilerOptions": {
    "typeRoots": [
        "./node_modules/@types",
        "./src/types/",
    ]
  }
}

Isso funciona para mim:

Api.boards.createBoard = jest.fn((name:string) => {
      const mockBoardResult = {
        created_on: mockBoardData.date,
        name,
        threads: [],
        updated_on: mockBoardData.date,
        _id: mockBoardData._id
      };
      return Promise.resolve({data:mockBoardResult}) as Promise<AxiosResponse<any>>
    });

outra maneira de substituir

import * as axios from 'axios'

declare module 'axios' {
  interface AxiosInstance {
    (config: AxiosRequestConfig): Promise<any>
  }
}

acho que talvez não devêssemos adicionar propriedade a AxiosResponse quando usar interceptors , porque interceptors podem ser ejetados.

// @axios-exts/add-foo-to-resp
declare module 'axios' {
  interface AxiosResponse<T = any> {
    foo: string
  }
}
const addFooToResp = (res: AxiosResponse) => {
  res.foo = 'bar'
  return res
}
export default addFooToResp
// Your Project
import axios from 'axios'
import addFooToResp from '@axios-exts/add-foo-to-resp'

var id = axios.interceptors.response.use(addFooToResp)

axios.get('xx').then(res => {
  // have type defined
  // because we use `declare module 'axios'` ts can infer type
  console.log(res.foo)
})

// but if then eject?
axios.interceptors.response.eject(id)
axios.get('xx').then(res => {
  // also have type defined, it's maybe not reasonable
  console.log(res.foo)
})

por que usamos texto datilografado? porque esperamos que nosso projeto seja seguro.
se algum dia removermos uma propriedade dos utilitários básicos, gostaríamos que o código que faz referência a ela produzisse um erro em tempo de compilação.

então, queremos usar interceptor adicionar alguma propriedade a AxiosResponse e ter inferência de tipo, é contraditório, porque não há como garantir que a inferência de tipo possa ser atualizada quando o interceptor for ejetado

eu acho que axios deve dizer ao usuário:
o interceptor deve lidar com algo que não tem efeito em AxiosResponse , se você quiser estender a propriedade AxiosResponse e ter inferência de tipos, você deve gostar de 'plugin'

// @axios-exts/add-foo-to-resp
declare module 'axios' {
  interface AxiosResponse<T = any> {
    foo: string
  }
}
const addFooToResp = (res: AxiosResponse) => {
  res.foo = 'bar'
  return res
}
export default (axios) => {
  axios.interceptors.response.use(addFooToResp)
}
// Your Project
import axios from 'axios'
import addFooToResp from '@axios-exts/add-foo-to-resp'

addFooToResp(axios)

não é 100% seguro, mas é seguro do que apenas usar axios.interceptors.response.use

e eu recomendo axios desgin um sistema de plugins, agora sempre vemos como

import axios from 'axios'
import wrapper from 'axios-cache-plugin'

let http = wrapper(axios, {
  maxCacheSize: 15
})
export default http

use como wrapper para conectar um plugin ao axios, todo plugin não tem uma regra comum, não é elegante o suficiente. eu acho que deveria gostar

import axios from 'axios'
import axiosCache from 'axios-cache-plugin'

axios.use(axiosCache, {
  // some options
})
export axios
// axios-cache-plugin
export default (axios) => {}
// or
export default {
  install(axios){}
}
// like Vue.use for Vue.js

Use a definição de estilo axios.request<T>(...args) .
O último interceptador de resposta no array obedece implicitamente a uma interface como (currentResponse: any) => T

Então, se você tem data sendo algo como:

interface ServerResponse {
  data: ServerData
}

interface ServerData {
  foo: string
  bar: number
}

Então você pode dizer:

axios.request<ServerData>({
  url: 'https://example.com/path/to/data',
  transformResponse: (r: ServerResponse) => r.data
}).then((response) => {
  // `response` is of type `AxiosResponse<ServerData>`
  const { data } = response
  // `data` is of type ServerData, correctly inferred
})

Por favor, você pode me dizer como posso adicionar cabeçalho e configuração na solicitação do Axios com tsx

@MoZiadAlafez

declare module 'axios' {
  interface AxiosRequestConfig {
    useCache: boolean
  }
}

Mas isso não é recomendado

Esta é uma longa história, mas parece ter sido resolvida em #1605. Consulte https://github.com/axios/axios/issues/1510#issuecomment -396894600.

Se eu entendi algo errado, melhor abrir um novo tópico com referências aqui. Obrigado.

Esta página foi útil?
0 / 5 - 0 avaliações