Skip to main content
Skip to main content

Luke Howsam

Software Engineer

Extending multiple classes in TypegraphQL

Logo of Apollo GraphQL, Node & Apollo client
Published
Share

In the past, I've worked on side projects that use TypegraphQL and Node.js as the backend API. Quite often when working on these projects, I've run into the problem of having a single Input type that needed to extend multiple classes.

Input types in TypegraphQL is used to define what a query or mutation argument's type is. When working in TypegraphQL, we pass an @Arg decorator in order to specify what this Query or Mutation accepts as arguments. An example of this would be:

@Mutation(() => Boolean) 
  async register(
    @Arg('email') email: string,
    @Arg('password') password: string,
): Promise<boolean> { 
  // mutation logic removed from brevity's sake
}

Here we have a register mutation that is responsible for registering a user. It returns a boolean and takes 'email' & 'password' as arguments. Put simply, we are specifying that we expect an email & password to be sent when a user calls this mutation.

A good way to manage input types in TypegraphQL that are used in multiple places in your project is to abstract that input type into its own file (i.e. 'RegisterInput.ts', 'LoginInput.ts' etc.)

It's common for input types to become more and more complex as your project grows in size. This is where I ran into the multiple class problem. Unfortunately, Typescript doesn't support multiple inheritance so we cannot extend two classes natively.

So, how do we get around this problem?

Mixins!

To get around this issue, we need to use a Mixin. Mixins rely on using generics with class inheritance to extend a base class

Here is an example that solves the multiple class inheritance problem:

 // ConfirmedMixin.ts 
import { ClassType, Field, InputType } from "type-graphql";

export const ConfirmedMixin = <T extends ClassType>(BaseClass: T) => {
  @InputType()
  class ConfirmedInput extends BaseClass {
    @Field()
    confirmed: boolean; 
  } 
  return ConfirmedInput;
}; 

Here we have a function that has an input type (ConfirmedInput). It has a single field called 'confirmed' which is a boolean. In short, all this Mixin does is take in a class and return the ConfirmedInput. So, how do we implement this mixin in the inputs we wish to extend?

Let's refactor the original register example to inherit from a Mixin. First, we shall create a simple register input without a mixin.

export class RegisterInput {
  @Field()
  email: string;

  @Field()
  password: string;
}

Now let's create two mixins that have some additional fields that you would typically include when you register a new user:

export const NotificationMixin = <T extends ClassType>(BaseClass: T) => {
  @InputType({ isAbstract: true }) 
  class NotificationInput extends BaseClass {
    @Field()
     enabled: boolean;
  } 
  return NotificationInput 
}; 
export const UserMixin = <T extends ClassType>(BaseClass: T) => {
  @InputType({ isAbstract: true })
  class RegisterInput extends BaseClass {
    @Field()
    acceptMarketingEmails: boolean;
   
    @Field()
    emailConfirmed: boolean;

    @Field()
    twoFactorEnabled: boolean;
  } 
  return userInput;
}; 

Note: I've included 'isAbstract: true' to get around the following error:

Error: Schema must contain uniquely named types but contains multiple types named "RegisterInput".

This happens because we have created multiple input types with the same name. You could rename the mixin input to a different name to avoid this error.

Now with the errors taken care of, we can go ahead and use the mixin in the RegisterInput input type.

export class RegisterInput extends UserMixin(NotificationMixin(class {})) {
  @Field()
   email: string;

  @Field()
  password: string;
}

Now RegisterInput will inherit all the fields from the UserMixin and NotificationMixin!