Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic types support #180

Closed
MichalLytek opened this issue Oct 22, 2018 · 7 comments · Fixed by #255
Closed

Generic types support #180

MichalLytek opened this issue Oct 22, 2018 · 7 comments · Fixed by #255
Assignees
Labels
Enhancement 🆕 New feature or request Solved ✔️ The issue has been solved
Milestone

Comments

@MichalLytek
Copy link
Owner

Right now we can only extend other object type, input type and args type classes. But this doesn't allow us to overwrite a property type so we can't create a truly generic types like the one used for pagination.

So, to the higher order class pattern we have to introduce isAbstract option that works the same way as in resolvers inheritance. Then we can create generic types:

export function Edge<TNodeType>(NodeType: ClassType<TNodeType>) {
  @ObjectType({ isAbstract: true })
  abstract class EdgeType {
    @Field(type => NodeType)
    node: TNodeType;
    @Field()
    cursor: string;
  }
  return EdgeType;
}

We can then easily extend the generic class:

@ObjectType()
export class RecipeEdge extends Edge(Recipe) {
  @Field()
  personalNotes: string;
}

@ObjectType()
export class FriendshipEdge extends Edge(User) {
  @Field()
  friendedAt: Date;
}

So this would generate following GraphQL type:

type RecipeEdge {
  node: Recipe!
  cursor: String!
  personalNotes: String!
}

type FriendshipEdge {
  node: User!
  cursor: String!
  friendedAt: DateTime!
}
@MichalLytek MichalLytek added the Enhancement 🆕 New feature or request label Oct 22, 2018
@MichalLytek MichalLytek added this to the 1.0.0 release milestone Oct 22, 2018
@MichalLytek MichalLytek self-assigned this Oct 22, 2018
This was referenced Feb 16, 2019
@MichalLytek
Copy link
Owner Author

MichalLytek commented Feb 21, 2019

Available in 0.17.0-beta.8 🚀

Docs for next version:
https://19majkel94.github.io/type-graphql/docs/next/generic-types.html

@XBeg9
Copy link

XBeg9 commented Feb 21, 2019

Amazing news ;) thanks @19majkel94

@Veetaha
Copy link

Veetaha commented Feb 26, 2019

@19majkel94 According to your example:

@ObjectType({ name: `Paginated${TItemClass.name}Response` })

This way you propose to generate unique names for generic types. But what if I specified another name for TItemClass when had decorated it with @ObjectType('SomeOtherName')? I would like to set the name of the generated class without having to repeat that 'SomeOtherName' as a factory function parameter. Could you please expose a method to retrieve GraphQL name from the given class constructor that was decorated with @ObjectType() ?

@MichalLytek
Copy link
Owner Author

MichalLytek commented Feb 26, 2019

@Veetaha
I'm not sure what you are referring to 😕

You can have a function that generates a classes (no isAbstract, use name for unique type names):

function Foo(TItem) {
  @ObjectType({ name: "Foo" + TItem.name })
  class Foo {
    @Field()
	bar: string;
  }
  return Foo;
}

const FooBar = Foo(Bar);

or as a factory for base classes (isAbstract: true, no name):

function Foo(TItem) {
  @ObjectType({ isAbstract: true })
  abstract class Foo {
    @Field()
	bar: string;
  }
  return Foo;
}

@ObjectType()
class FooBar extends Foo(Bar) {}

Could you describe your use case in more details, instead of trying to propose the solution for the unknown problem?

@Veetaha
Copy link

Veetaha commented Feb 26, 2019

@19majkel94 Sorry if I was not verbose. The version of typegraphql that I use (namely 0.16.0) exposes ObjectType() function with the following overloads:

function ObjectType(): ClassDecorator;
function ObjectType(options: ObjectOptions): ClassDecorator;
function ObjectType(name: string, options?: ObjectOptions): ClassDecorator;

Thus, if you decorate a class the following way:

@ObjectType()
class UserType { /* ... */  }

the generated GraphQL type name will be type UserType { }.

If you want to leave your TypeScript class name being UserType but have generated GraphQL type to be called User you forward a string with the name as the first parameter to ObjectType() that overrides the default UserType.name that is used when you don't specify the name explicitly.

@ObjectType('User')
class UserType { /* ... */  }

Now the generated GraphQL type name is type User { } rather than type UserType { }.

And when I have the following factory function (taken from your examples):

export default function PaginatedResponse<TItem>(TItemClass: ClassType<TItem>) {

  @ObjectType(`Paginated${TItemClass.name}Response`)
  class PaginatedResponseClass {
 
  }
  return PaginatedResponseClass;
}

I can't be sure that TItemClass.name === respective graphql type name.
Suppose we have the following class:

@ObjectType('Book')
class BookType { }

So when I do this:

const PaginatedBookResponse = PaginatedResponse(BookType);

The generated graphql type will be type PaginatedBookTypeResponse {} rather than wanted type PaginatedBookResponse {}.

Thus I would like a function that returns a respective GraphQL type name for a given type, so that I could correct PaginatedResponse() factory definition:

import { getMetadata } from 'type-graphql';
export default function PaginatedResponse<TItem>(TItemClass: ClassType<TItem>) {

  // should throw if TItemClass was not decorated with either @ObjectType() or @InputType()
  const graphqlTypeName = getMetadata(TItemClass).name;

  @ObjectType(`Paginated${graphqlTypeName}Response`)
  class PaginatedResponseClass {
 
  }
  return PaginatedResponseClass;
}

P.S. I don't know why you invoke @ObjectType() with the object that has name property, as my version of 'typegraphql' declares ObjectOptions with no such property, and I pass the name as the bare first argument.

I think that getMetadata() function should return MetadataStorage or some abstraction over this type as you referenced it here

@MichalLytek
Copy link
Owner Author

@Veetaha
Your factory function may take more arguments, like:

function PaginatedResponse<TItem>(TItemClass: ClassType<TItem>, itemName: string) {}

const PaginatedBookResponse = PaginatedResponse(BookType, "Book");

The TItemClass.name is just a shortcut for the default naming.

Accessing metadata is part of the #134.

@Veetaha
Copy link

Veetaha commented Feb 26, 2019

@19majkel94 As I have said in my first comment,

I would like to set the name of the generated class without having to repeat that 'SomeOtherName' as a factory function parameter

Exposing some interface to access the metadata you define would be way better than reimplementing the wheel. I could wrap ObjectType() with my custom decorator that would store the GraphQL type name along with saving it to your MetadataStorage, so that I could access it myself without referring to your API. Or I could just forward its name as the factory function parameter and repeat myself twice, which was the thing I wanted to avoid initially.

@MichalLytek MichalLytek added the Solved ✔️ The issue has been solved label Jan 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Enhancement 🆕 New feature or request Solved ✔️ The issue has been solved
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants