samchon / nestia

NestJS Helper Libraries + TypeScript OpenAPI generator
https://nestia.io/
MIT License
1.85k stars 95 forks source link

Type inference error in SDK when transform interceptor is present #279

Closed kakasoo closed 1 year ago

kakasoo commented 1 year ago

https://docs.nestjs.com/interceptors#response-mapping

// transform.interceptor.ts in nest.js document
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Response<T> {
  data: T;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(map(data => ({ data })));
  }
}

This code is an interceptor in the nest.js official document. When transforming with an interceptor, the SDK in nestia deduces a different type from the actual response.

// in my code...
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Req } from '@nestjs/common';
import { NON_PAGINATION } from '../config/constant';
import { Request } from 'express';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ExtendedResponse, ListOutputValue } from '../types';

export const calcListTotalCount = (totalCount = 0, limit = 0): { totalResult: number; totalPage: number } => {
  const totalResult = totalCount;
  const totalPage = totalResult % limit === 0 ? totalResult / limit : Math.floor(totalResult / limit) + 1;
  return { totalResult, totalPage };
};

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ExtendedResponse<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<ExtendedResponse<T>> {
    const request: Request & { now: number } = context.switchToHttp().getRequest();

    return next.handle().pipe(
      map((value) => {
        const requestToResponse = `${Date.now() - request.now}ms`;

        if (value instanceof Object && 'count' in value && 'list' in value) {
          const { list, count, ...restData } = value;

          const limit = request.query['limit'] ? request.query['limit'] : NON_PAGINATION;
          const page = request.query['page'];
          const search = request.query['search'];

          return {
            result: true,
            code: 1000,
            requestToResponse,
            data: {
              ...restData,
              list,
              ...(limit === NON_PAGINATION
                ? { totalResult: count, totalPage: 1 }
                : calcListTotalCount(count, Number(limit))),
              ...(search ? { search } : { search: null }),
              ...(page && { page: Number(page) }),
            } as ListOutputValue,
          };
        }

        return { result: true, code: 1000, requestToResponse, data: value };
      }),
    );
  }
}

I'm actually using an interceptor for transform in my code. 1) The logic is to assign additional properties to the response type and 2) If controller's return type have the properties named list and count, the server should consider it a pagination and assign additional properties such as totalCount and totalPage. All I'm saying is, I want to hear your thoughts on what to do if you format the response form in the interceptor.

I didn't have an idea to solve this, so I defined a separate pagination function and various utility types to help it, and I removed transform.interceptor. But I'm thinking about whether there was really no other way.

kakasoo commented 1 year ago

I thought about it after writing the question, but it seems that it can be solved by using the internal type of Observable<ExtendedResponse> defined as the response type of transform.interceptor.ts.

Is it difficult to consider the case of returning the generic T of the intercept method in a different form?

samchon commented 1 year ago

Show me demonstration project occuring the type inference error

kakasoo commented 1 year ago

I was a little late writing the example code. Feel free to answer slowly. I'm going to go to bed now. :) There are two problems here.

  1. When mapping a response object in transform, it is returned to the JSON format.
  2. SDK cannot infer from the actual transformed value.
git clone https://github.com/kakasoo/nestia-demo
cd nestia-demo
git switch nestia#279
npm i
npm run build
npm run test:watch
image image image
samchon commented 1 year ago

Ah, understood what you want, and it is not possible through nestia.

Such type transformation can't be detected in the compilation level.

kakasoo commented 1 year ago

OK, I see. I have now implemented separate utility types and functions to solve this problem on the controller. Even if your answer doesn't solve my problem, I fully understand it and still love this project. Thank you.