Search code examples
javascriptnode.jstypescriptnestjstypescript-generics

What is Type in @nestjs/common ? What is its role?


NestJS have interface Type in package @nestjs/common . but in two days I could not understand its role in detail. Can you help with this?

As an example, I took this article https://docs.nestjs.com/graphql/resolvers#class-inheritance

https://github.com/nestjs/nest/blob/master/packages/common/interfaces/type.interface.ts

export interface Type<T = any> extends Function {
    new (...args: any[]): T;
}

Short usage example for create Template Class

import { Controller } from "@nestjs/common";
import { CoordinateService as S } from "@location/@coordinate/coordinate.service";

export interface Coordinate {
  Id: string;
  longitude: string;
  latitude: string;
  type: string;
}

export interface CoordinateId {
  id: string;
}

@Controller()
export class CoordinateController extends BaseGrpcService<any, Coordinate, CoordinateId>(S) {}
import { Inject, Type, forwardRef } from "@nestjs/common";
import { GrpcMethod } from "@nestjs/microservices";
import { Metadata, ServerUnaryCall } from "@grpc/grpc-js";
import { Observable } from "rxjs";

export interface IProtoData {}

export interface IProtoDataId {
  id: string;
}

export function BaseGrpcService<
  T1 extends Type<Object>,
  T2 extends IProtoData,
  T3 extends IProtoDataId
>(classService: T1): any {
  class BaseGrpcService extends Base implements IBaseGrpcService<T2, T3> {
    constructor(
      @Inject(forwardRef(() => classService))
      private readonly baseService: IBaseService<T2, T2, T2>
    ) {
      super();
    }

    @GrpcMethod(classService.name, "FindById")
    async findById(request: T3, metarequest: Metadata): Promise<T2> {
      return await this.baseService.findById(request?.id);
    }
  }
}

When I replaced string "T2 extends IProtoData" to "T2 extends Type" TS sended me error

error TS2344: Type 'Coordinate' does not satisfy the constraint 'Type'. Type 'Coordinate' is missing the following properties from type 'Type': apply, call, bind, prototype, and 5 more.


Step 2

What's wrong?

class ClassProtoData implements Coordinate {
    Id: string;
    longitude: string;
    latitude: string;
    type: string;
}

@Controller()
export class CoordinateController extends BaseGrpcService<any, ClassProtoData, CoordinateId>(S) {}

export function BaseGrpcService<
    T1 extends Type<Object>,
    T2 extends Type<IProtoData>,
    T3 extends IProtoDataId
>(classService: T1): any {

}

Therefore, I had a misunderstanding of this design. What does it do?

export interface Type<T = any> extends Function {
    new (...args: any[]): T;
}

Step 3:

Yes, I understand that I shouldn't use it. I'm trying to understand the mechanism itself. What does this mechanism do?

I understand correctly that type.interface.ts does the following:

  1. We take a list of properties described in the interface
  2. Inherit the Function object
  3. Call the function creation method "new (...args: any[]): T;"
  4. As a result, we get not an interface, but an instance of a Javascript function.

Or I'm wrong? I am completely confused with this code.

==================================

ANSWER

EXAMPLE: https://codesandbox.io/s/od6deu

You're wrong. We don't get an instance of a class at all. It's still Typescript, it's all just type values. What the Type type helper is for is to say we expect to receive a class reference instead of a class instance here. Whether that is a class reference that satisfies an interface or a class directly is irrelevant. Think of the imports array in @Module(). We don't pass new AuthModule() to that, we only pass AuthModule and Nest takes care of the rest. That's the kind of thing here. In your example you use forwardRef which should point to another class.That's a class reference

Think of the Type interface as something like newable. We are able to call new Thing() on a parameter of type Type, but do it dynamically.

function createClass<T>(classRef: Type<T>): T { 
    return new classRef();
} 

is valid because classRef is typed as Type. Now if we were to call createClass(AuthService) we'd get an instance of AuthService back even though we passed a class reference, and Typescript is happy with this because classRef is typed as Type and knows it returns T, the instance type


Solution

  • Type is a Typescript type that says we are expecting a class reference not a class instance here. This is helpful in places like Nest's @Module()'s imports and providers arrays so that users can pass the class itself without having to instantiate it. Your T1 makes sense to stay as a Type<> because you pass the class reference to @Inject(forwardRef(() => T1)) which should be a type reference. Everything else though, it looks like you need the direct T2 instead of Type<T2> so that you know to return instances of that class type (or objects that satisfy the type)