Axios: Как использовать Axios с TypeScript при использовании перехватчиков ответов (проблема с AxiosResponse)

Созданный на 30 апр. 2018  ·  50Комментарии  ·  Источник: axios/axios

Резюме

В проекте, который я перехожу на TypeScript (TS), у меня есть перехватчик ответа r => r.data . Как мне сообщить TS, что я не ожидаю типа AxiosResponse ? Я пытался переопределить с помощью as Array<... , но это не работает, поскольку AxiosResponse нельзя преобразовать в Array (например, не имеет .length).

Спасибо!

Контекст

  • версия аксиом: 0.16.2
  • Среда: Код Visual Studio

Самый полезный комментарий

Я переопределяю AxiosResponse в моем axios.d.ts :

import axios from 'axios'

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

Все 50 Комментарий

Используйте определение стиля axios.request<T>(...args) .
Последний Response-перехватчик в массиве неявно соответствует интерфейсу типа (currentResponse: any) => T

Итак, если у вас есть data , что-то вроде:

interface ServerResponse {
  data: ServerData
}

interface ServerData {
  foo: string
  bar: number
}

Тогда вы можете сказать:

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 ох, на самом деле это не сработает для глобальных перехватчиков, например, axios.interceptors.response.use

Я думаю, что да - перехватчики в настоящее время имеют очень "расслабленный" тип (он же any ), поэтому вы можете просто прикрепить перехватчик ответа и получить его (r: any): any => r.data (что в основном, как если бы вы не вводите текст в нестрогом режиме).

Только в том месте, где вы вызываете axios.request , вы можете настроить его как общий, который затем передается как тип свойства data .

(На самом деле я допустил ошибку в приведенном выше фрагменте кода, data на самом деле AxiosResponse<ServerData> , в котором есть поле с именем .data с типом ServerData — обновил его)

@zcei привет, извините, но это действительно не работает с моим примером, я создал codeandbox, чтобы выделить эту проблему.

Проверьте это здесь

Имея ту же проблему, перехватчик можно свести к следующему:

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

Несмотря на это, возвращаемый тип

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

AxiosPromise<string> , который ожидает, что данные будут упакованы.

Это намеренно. Вы должны преобразовывать данные в перехватчике, а не поднимать ключи ответа .

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

Я думал, что у вас будет ответ сервера, например

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

и хотел избавиться от вложенности response.data.data . Если вы вернете response.data в перехватчике, вы сможете позже получить к нему доступ через response.data.foo вместо response.data.data.foo .

Но доступ к response.foo не сработает, так как это «корневой» уровень ответа, который отслеживает другие вещи, такие как код ответа и тому подобное.

Извините, но я не согласен с вашей предпосылкой, по крайней мере, не с точки зрения функциональности.
Без использования перехватчика для развертывания свойства данных мне пришлось бы сделать

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

в _каждой_ конечной точке, которую я определяю. Какой смысл во всем этом повторении кода?

Всякий раз, когда запрос выполняется успешно, что означает наличие then , меня не волнует код ответа и т. д. Меня интересуют коды ответов только в том случае, если что-то _не_ работает, и для этого существуют обработчики ошибок. , как на перехватчике, так и на конкретных конечных точках.

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
});

Всякий раз, когда запрос выполняется успешно, что означает наличие then, меня не волнует код ответа.

Ну, вы можете не заботиться о какой-либо другой информации, но ограничение для всех, использующих HTTP-клиент , что вы заботитесь только о теле, на самом деле не является решением.
Существует достаточно законных вариантов использования кодов состояния и заголовков даже при успешных вызовах. Например, различать 204 и 200 , проверять заголовки ограничения скорости, извлекать заголовки Link для дополнительных ресурсов (разбивка на страницы) и т. д. стр.

Если вам все равно, заверните Axios и выбросьте все:

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 => { ... });

Я не понимаю, с чего вы взяли, что все должны делать то, что делаю я, в одном проекте. Я просто пытаюсь решить проблему автоматического развертывания данных при каждом запросе, и, учитывая, что эта проблема была здесь до меня, я не единственный с этой проблемой.

Перехватчики _кажется_ правильным местом для этого, как по их названию, так и по их описанию в файле readme (сделайте что-нибудь с каждым ответом). Обработка 204 против 200 и т. д., а затем передача данных также имеет смысл в перехватчике, потому что тогда вы можете делать это в одном центральном месте.

На мой взгляд, интуитивно понятным решением было бы вернуть то, что вы хотите от перехватчика, а не принудительно использовать библиотеку одним способом.

Если вы хотите преобразовать некоторые поля в данные:

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

Если вы хотите просто развернуть данные Axios:

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

Это оставило бы выбор того, что делать разработчику, что, на мой взгляд, лучше, чем очень самоуверенный перехватчик. Вы вправе не соглашаться, конечно.

Вы рассуждали о «смысле во всем этом повторении кода», поэтому я просто попытался объяснить, зачем нужна эта информация.

Я действительно не могу найти способ, как вы хотели бы сохранить безопасность типов в перехватчиках и ответе, если какой-либо перехватчик может зашифровать весь ответ без общей структуры. :/
Я думаю, что тип ответа для запроса должен стать any , и я приложу усилия к разработчикам, чтобы убедиться, что они делают правильные вещи.

По крайней мере, для меня это кажется более подверженным ошибкам из-за расслабленной типизации, чем доступ к одному свойству.

Если вы сможете придумать PR, который позволяет перехватчикам перезаписывать корневой ответ, сохраняя при этом безопасность типов для «80% случаев использования», я был бы более чем счастлив!

Дело в том, что оба приведенных выше примера, которые я сделал, уже работают так, как я описал, с точки зрения функциональности, это просто типы, которые неверны. Если возврат чего-либо, кроме (модифицированного) ответа в перехватчике, неверен, я думаю, было бы хорошо обновить там ожидаемые типы.

Во-первых, интересно узнать, что вы делаете то же самое, @Etheryte !

В нашем приложении r => r.data является последним перехватчиком ответа в цепочке, и мы используем другие, которые полагаются на коды состояния для обработки токенов обновления и т. д., но на глобальном уровне, поскольку нам не нужно обрабатывать это для конкретных звонки.

Я понимаю, что может быть просто невозможно приспособить этот вариант использования к TS. Тем не менее, @zcei нельзя отрицать, что это законный способ использования Axios, поскольку он использует официальную часть API Axios (перехватчики) :).

Дох! Я все время смотрел не тот код 🤦‍♂️ и был в разделе transformResponse , а не в перехватчике - мне очень жаль!

Как бы вы обновили типизацию, чтобы учесть перехватчик, изменяющий тип возвращаемого значения?

Может быть, что-то вроде этого?

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

Вы бы назвали это так:

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)
  })

Просто попробовал это локально и, кажется, делает то, что вы ищете.
Может быть, я смогу собрать PR вечером, чтобы обновить это для всех методов.

@zcei Выглядит хорошо! Использование по умолчанию AxiosResponse, конечно же, имеет смысл в 99% случаев 👍

Рад слышать! Еще раз извините за мою путаницу 😓

@zcei не проблема! Ради интереса, каков цикл выпуска Axios?

Его нет — у меня все еще есть кое-что на рассмотрении для альфа-версии v1 (#1333), а тем временем @nickuraltsev / @emilyemorehouse выпускают релизы по мере необходимости.
С тех пор, как v0.18 , была некоторая тяга (в том числе моя любимая: параметры области действия для экземпляров), поэтому я думаю, что они могли бы сократить выпуск. Для получения дополнительной информации я просто любезно приглашу вас в Gitter , чтобы мы не упускали из виду проблемы 🙂

Я надеялся выпустить версию 0.19, но в последний раз, когда я проверял, master не выполнял CI. Я определенно хотел бы иметь более плотный/регулярный график выпуска, как только мы выпустим 1.0.

столкнуться с той же проблемой. Есть ли решения?

@qidaneix , вы можете попробовать установить npm install axios/axios#master , пока не выйдет релиз. #1605 должен был это исправить. Было бы неплохо получить обратную связь, действительно ли это помогает в реальных случаях использования, а не только в тестах 🙂

@zcei завтра попробую

@zcei Могу я спросить, когда он выйдет?

+1 когда выйдет 1.0.0?

Это точно будет версия до 1.0. @Khaledgarbaya , тебя тоже Мэтт добавил в NPM? В противном случае мы должны быть добры к оставшимся сопровождающим, выпускающим его 😉

Всем привет. В настоящее время я управляю выпусками NPM. Я хотел бы выпустить еще один выпуск до версии 1.0.0, но тесты Travis не работают, и у меня еще не было возможности их исправить. Как только они будут исправлены, я буду более чем счастлив немедленно выпустить это 😬

@zcei Меня не добавили в репозиторий npm, я могу только объединить изменения

Есть какие-то обновления по этому вопросу, будет ли релиз в ближайшее время, типа месяц-два?

+1

❤️ если бы это можно было выпустить сейчас

Мы в курсе, но в настоящее время заблокированы. 🙁 Вы можете получить больше информации здесь: https://github.com/axios/axios/issues/1657#issuecomment -410766031

Выпущено как часть 0.19.0-beta.1.

Это можно установить с помощью npm install [email protected] или npm install axios@next

Может быть, что-то вроде этого?

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

Вы бы назвали это так:

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)
  })

Просто попробовал это локально и, кажется, делает то, что вы ищете.
Может быть, я смогу собрать PR вечером, чтобы обновить это для всех методов.

Пользователь (T, первый общий параметр), кажется, не используется, если я хочу использовать настраиваемые типы возврата, я выгляжу странно 😕

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

использование void может быть более понятным.

@emilyemorehouse Не хочу показаться неблагодарным, но бета-версия 0.19 открыта уже три месяца, есть ли ETA для выпуска GA? В нашем проекте есть проблема, которая требует одного из этих исправлений. Я спросил об этом на канале Gitter, но похоже, что сопровождающие не отвечают там...

Возможно, лучше было бы сформулировать, какие проблемы необходимо решить перед выпуском, и есть ли место для их отслеживания? Так как, похоже, есть большой интерес, распространение работы и четкое понимание необходимой работы может помочь ускорить процесс.

Уже есть веха проекта для 0.19, но перечисленные билеты не претерпели никаких изменений в течение нескольких месяцев.

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

Я переопределяю AxiosResponse в моем axios.d.ts :

import axios from 'axios'

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

Подняв этот вопрос. Это работает ... хорошо, когда я просто скопировал определение AxiosInstance в локальные типизации, но реализованное решение, на мой взгляд, очень многословно, если только я не делаю что-то не так (не эксперт по Typescript). Поскольку я использую отдельный экземпляр axios, созданный с помощью axios.create и использующий этот перехватчик:

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

где response.data всегда имеет такую ​​форму:

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

похоже, мне нужно использовать AxiosClient.post следующим образом:

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

чтобы иметь правильные типы в .then . Я делаю что-то не так или это действительно должно быть ТАК многословно? Если нет, было бы намного лучше, если бы я мог просто передать схему ожидаемого ответа при создании экземпляра, но я не могу заставить ее работать:

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;
}

Он отлично работает с axios.create() без универсального типа или просто с axios , но если я передам свой интерфейс следующим образом:

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

а затем использовать его следующим образом: AxiosClient.post<string>('/endpoint').then(value => value.data) , value.data имеет тип... T. Кроме того, вышеприведенная версия работает только в том случае, если я фактически заменю эти типы в node_modules, в противном случае он полностью перепутается, и я закончиться какой-то полной катастрофой. Может ли кто-нибудь помочь мне с этим?

Изменить: хорошо, я думаю, это не сработает, потому что таким образом невозможно использовать дженерики (поэтому R<T> , когда R является универсальным типом, не является правильным синтаксисом, но я думаю, что WebStorm по какой-то причине не выделил это для меня); https://github.com/Microsoft/TypeScript/issues/1213 это, я полагаю, решит эту проблему, но не знаю, будет ли она когда-либо реализована.

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;
    });
}

использовать:

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

@zcei Это еще не решено? Не могу заставить работать следующее:

// 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?

такая же проблема, я устанавливаю версию 0.19beta, также ts не может проанализировать правильный тип

Я переопределяю AxiosResponse в моем axios.d.ts :

import axios from 'axios'

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

о ниупи

Я использую перехватчик, чтобы сделать что-то вроде: response => response.data . Поэтому мне нужно полностью удалить оболочку AxiosResponse .

Я, наконец, заканчиваю:

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>;
  }
}

Если кто-то не знает, как им пользоваться:

  1. Запишите типы где-нибудь, например, src/types/axios/axios.d.ts .
  2. Обновите свой tsconfig.json , например
{
  "compilerOptions": {
    "typeRoots": [
        "./node_modules/@types",
        "./src/types/",
    ]
  }
}

Это работает для меня:

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>>
    });

другой способ переопределить

import * as axios from 'axios'

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

я думаю, что нам, возможно, не следует добавлять свойство к AxiosResponse при использовании перехватчиков , потому что перехватчики могут быть извлечены.

// @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)
})

почему мы используем машинопись? потому что мы надеемся, что наш проект будет благополучно.
если когда-нибудь мы удалим свойство из базовых утилит, мы хотели бы, чтобы код, который ссылается на него, выдавал ошибку во время компиляции.

Итак, мы хотим использовать перехватчик , добавить некоторое свойство к AxiosResponse и иметь вывод типа, это противоречиво, потому что нет способа гарантировать, что вывод типа может быть обновлен при извлечении перехватчика.

я думаю, что axios должен сказать пользователю:
перехватчик должен обрабатывать что-то, что не влияет на AxiosResponse , если вы хотите расширить свойство AxiosResponse и иметь вывод типа, вам должен понравиться «плагин»

// @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)

это не 100% безопасность, но это безопасность, чем просто использовать axios.interceptors.response.use

и я рекомендую axios desgin систему плагинов, теперь мы всегда видим, как

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

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

используйте как wrapper для подключения плагина к axios, у каждого плагина нет общего правила, это недостаточно элегантно. я думаю должно понравиться

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

Используйте определение стиля axios.request<T>(...args) .
Последний Response-перехватчик в массиве неявно соответствует интерфейсу типа (currentResponse: any) => T

Итак, если у вас есть data , что-то вроде:

interface ServerResponse {
  data: ServerData
}

interface ServerData {
  foo: string
  bar: number
}

Тогда вы можете сказать:

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
})

Пожалуйста, скажите мне, как я могу добавить заголовок и конфигурацию в запрос Axios с помощью tsx

@MoZiadAlhafez

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

Но это не рекомендуется

Это длинная история, но, похоже, она была решена в #1605. См. https://github.com/axios/axios/issues/1510#issuecomment-396894600 .

Если я что-то не так понял, лучше открыть новую тему со ссылками сюда. Спасибо.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги