Axios 인터셉터 타입스크립트에서 제대로 쓰기

HYUNGU, KANG·2021년 7월 29일
33

Axios 에는 interceptors 라는 기능이 있다.
이를 통해서 request / response 에 선행,후행 처리를 커스텀하게 할 수 있다.

const client = Axios.create({ baseURL: 'https://my-url.com/v1' });

client.interceptors.response.use(res => {
  return res.data;
})

client.interceptors.request.use(req => {
  req.headers.authorization = TokenManager.accessToken;
  return req;
})

// 원래대로라면, response.data 에 user 가 들어있어야 하지만
// interceptor 를 통해서 response.data 를 건너뛰었다.
const user = await client.get('/users/my-uid');

그취만.. 타입 스크립트에서는 interceptor 를 통해서 한번 포맷팅을 변경했지만
client.get 의 타입은 그대로 response.data 형태로 남아있는걸 겪을 수 있다.

// user 의 타입은 AxiosResponse 로 감싸져서, 실제로는 user 객체를 받아오지만
// 타입상으로는 { data: { name: string; age: number; } } 과 같은 형태로 나온다.
const user = await client.get<{ name:string; age:number; }>('/users/my-uid');

https://github.com/axios/axios/issues/1510 이슈를 통해서 활발한 논의가 진행됐었고
https://github.com/axios/axios/pull/1605 를 통해서 커스텀한 타입을 추가할 수 있게 수정되었다. (뒤쪽 제네릭의 기본값을 AxiosResponse 로 감싼 형태로 처리하고, 해당 제네릭을 return 값으로 처리했다.)

const user = await client.get<any,{ name:string; age:number; }>('/users/my-uid');

흠.. 계속해서 사용하기에 적합한 방식은 아닌 것 같다는 생각이 들었다.


typescript 는 타입선언을 여러가지 방법으로 지원하는데 이 중 인터페이스와 상속을 사용하면 쉽게 해결할 수 있다.

interface CustomInstance extends AxiosInstance

짜잔.. 바로 인터페이스를 상속받은 뒤, 재정의해서 쓰면 된다.
타입의 근본적인 형만 일치한다면, Instance 자체에 Generic 을 할당하거나
얼마든지 원하는 형태로 가공해서 사용할 수 있다.

아래의 예제는, 기본으로 내려주는 형태가 정해진 경우를 가정해서 작성했다.


type CustomResponseFormat<T = any> = {
  response: T;
  refreshedToken?: string;
}
interface CustomInstance extends AxiosInstance {
    interceptors: {
        request: AxiosInterceptorManager<AxiosRequestConfig>;
        response: AxiosInterceptorManager<AxiosResponse<CustomResponseFormat>>;
    };
    getUri(config?: AxiosRequestConfig): string;
    request<T>(config: AxiosRequestConfig): Promise<T>;
    get<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
    delete<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
    head<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
    options<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
    post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
    put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
    patch<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
}

const client:CustomInstance = Axios.create({ baseURL: 'https://my-url.com/v1' });

// res - AxiosResponse<CustomResponseFormat>
client.interceptors.response.use(res => {
  if(res.data.refreshedToken) TokenManager.replace(refreshedToken);
  return res.data.response;
})

client.interceptors.request.use(req => {
  req.headers.authorization = TokenManager.accessToken;
  return req;
})

// user - { name:string; age:number; }
const user = await client.get<{ name:string; age:number; }>('/users/my-uid');

타입을 잘 다루는 분이라면, AxiosInterceptorManager 도 재정의하고, infer 혹은 predicates 를 통해서 interceptor 에서 리턴한 타입을 자동으로 추론되게 할 수 있지 않을까 하는 생각도 해본다.

끝.

profile
JavaScript, TypeScript and React-Native

6개의 댓글

comment-user-thumbnail
2022년 11월 29일

나의 구원자.....❤️‍🔥 감사합니다

1개의 답글
comment-user-thumbnail
2023년 2월 1일

와우 이렇게 하는게 훨씬 보기 좋네요

1개의 답글
comment-user-thumbnail
2023년 2월 19일

마지막 CustomInstance 타입 정의해주시는 부분을 보고 interceptor 타입 문제 해결 할 수 있었습니다! 감사합니다 ㅎㅎ
저는 extends 할 때, 직접 작성하거나 라이브러리가 업데이트 되었을 때 CustomInstance 메소드들에 적어둔 파라미터들에 누락된 것이 생길 위험이 있다고 생각되어 아래와 같이 제네릭을 사용하였습니다!

interface CustomInstance extends AxiosInstance {
  get<T>(...params: Parameters<AxiosInstance["get"]>): Promise<T>;
  delete<T>(...params: Parameters<AxiosInstance["delete"]>): Promise<T>;
  post<T>(...params: Parameters<AxiosInstance["post"]>): Promise<T>;
  put<T>(...params: Parameters<AxiosInstance["put"]>): Promise<T>;
  patch<T>(...params: Parameters<AxiosInstance["patch"]>): Promise<T>;
}
1개의 답글