type-graphql icon indicating copy to clipboard operation
type-graphql copied to clipboard

Extend a typedef in another file

Open Xample opened this issue 6 years ago • 12 comments

Sometimes you want to add a property to whatever model but you want to keep all the logic (typedef and resolver) in a folder appart. This can be done using the ‘extend’ keyword in a typedef. https://www.apollographql.com/docs/graphql-tools/generate-schema.html#extend-types Actually this is more a mixin than an extend. How could you achieve the same using type-graphql ?

Xample avatar Jan 09 '19 17:01 Xample

I am planing to add better modules (decoupling) support but this case is the one where I haven't found a good, typesafe API yet.

You can read this issue about the problem how to have module-scoped typedefs.

The only idea that I can think now is to have extend option in decorator to provide metadata for later merging in schema, rather that duplicating it:

// modules/user/user.ts
@ObjectType()
export default class User {
  @Field()
  email: string;

  @Field()
  password: string;
}

// modules/profile/user.ts
import BaseUser from "@modules/user";

@ObjectType({ extends: BaseUser })
class User extends BaseUser {
  @Field()
  profilePictureUrl: string;
}

But the problem with that approach is that it's hard to create a module that rely on two other modules that modify the User object type - TS has no support for multiple inheritance and multiple implements is a lot of declarations duplication.

MichalLytek avatar Jan 09 '19 20:01 MichalLytek

I personally thought about using a mixin https://www.typescriptlang.org/docs/handbook/mixins.html But then, to what I see, we need to add glue code to explicitly merge 2 classes together. Using modules is "merged" by typescript itself but I also discourage keeping using modules with TS generally speaking.

Xample avatar Jan 10 '19 08:01 Xample

TS mixins are awful and (I hope) a deprecated pattern :confounded:

ES6 class mixins are the only solution: https://youtu.be/j9dOdjBzARo?t=441

I plan adding to TypeGraphQL a type-safe port of mix(Foo).with(Bar) approach: https://github.com/justinfagnani/mixwith.js#mixwith

MichalLytek avatar Jan 17 '19 19:01 MichalLytek

Okay good so this way you can extend a class in another one, however, unlike an internal module (which would be merged by the language) you still need the parent class to know the subclasses to explicitly extend them.

Xample avatar Jan 18 '19 06:01 Xample

unlike an internal module (which would be merged by the language)

TS is not able to merge classes in namespaces (internal modules):

// a.ts
namespace MyApp {
  export class User {
    name!: string;
  }
}

// b.ts
namespace MyApp {
  export class User {
    age!: number;
  }
}

code_2019-01-18_11-04-56

To achieve this, TS would have to support partial classes: https://github.com/Microsoft/TypeScript/issues/563

MichalLytek avatar Jan 18 '19 10:01 MichalLytek

Hi all,

TypeORM solved that already with "@ChildEntity()"

https://github.com/typeorm/typeorm/blob/master/docs/entity-inheritance.md

Would be awesome if Type-Graphql could implement it the same since alot is already inspired by the same stuff.


import { PrimaryGeneratedColumn, Column, Entity, ChildEntity } from "typeorm";
import { Field, ObjectType } from "type-graphql";
import { InvoiceDocument } from "../documents/invoice.model";

@ChildEntity()
@ObjectType({ description: "" })
export class CustomInvoiceFields extends InvoiceDocument {
    
    @Column()
    @Field({ nullable: true })
    CoolesExtraFeld: String;
}

TypeORM creates a Column in the "InvoiceDocument" Table here.

Logic-Bits avatar Feb 25 '19 13:02 Logic-Bits

@Logic-Bits So you propose the same solution as I showed earlier?

// modules/user/user.ts
@ObjectType()
export default class User {
  @Field()
  email: string;

  @Field()
  password: string;
}

// modules/profile/user.ts
import BaseUser from "@modules/user";

@ObjectType({ extends: true })
class User extends BaseUser {
  @Field()
  profilePictureUrl: string;
}

The extends: true means "don't create a new object type, instead add new fields to the parent class".

This solution is great but again - how then create a module that rely on two other modules that modify the User object type? Like User with profile picture and photos, we need them in a blog module.

MichalLytek avatar Feb 25 '19 15:02 MichalLytek

@19majkel94 maybe it can be solved in code by using type unions? It's not perfectly safe but I don't see how it would impact any further improvement if we can find a better way to do type checking here.

quentinus95 avatar Apr 05 '19 16:04 quentinus95

What is the final solution ?

kaousheik avatar Mar 01 '20 16:03 kaousheik

@kaousheik i believe the current "best" solution is to:

example: one Graph type: User two modules: User, Profile

// modules/user/user.ts
@ObjectType()
class User {
  @Field()
  email: string;

  // ideally this would be defined in the Profile module, but its currently not supported
  @Field()
  profilePictureUrl: string;
}

// modules/profile/user.resolver.ts
import User from "@modules/user";

@Resolver((of) => User)
class UserResolver {
  @ResolveField()
  profilePictureUrl() { ...logic }
}
  • define all fields on the singular entity class. In @MichalLytek 's example that would be: // modules/user/user.ts
  • import the singular entity class in various other modules, and define the resolver function in there.

This way you get some separation of concerns (you don't need to pass around your services), but you will need to pass around the entity classes.

For reference, the problem this issue is trying to solve is the equivalent of Nexus' extendType functionality (link).

dcurletti avatar Feb 21 '21 17:02 dcurletti

+1 for this Are there any better ways to do this for input types? Is there similar way for @InputType or should we just use native JavaScript extends to create new InputType based on existing InputType?

valerii15298 avatar Jun 01 '23 16:06 valerii15298

@valerii15298 The purpose of extends in SDL is quite different that your @InputType case.

MichalLytek avatar Jun 02 '23 06:06 MichalLytek