Why can't TypeGraphQL determine the type of a generic?
I'm trying to define generic type definitions for building connections and edges in GraphQL. I'm keeping with the Relay spec, except that I'm also including the conventional nodes
connection property for convenience. The TypeGraphQL docs show how to do something very similar, but I'm getting the following error when I try to run the server:
Cannot determine GraphQL output type for 'nodes' of 'Connection' class. Is the value, that is used as its TS type or explicit type, decorated with a proper decorator or is it a proper output value?
Typically, this shows up when the @ObjectType
decorator is missing from a class definition which is then used as a field type somewhere else; however, I've confirmed that the types I'm passing have definitely been defined with this decorator, and I've also tried with a few different types from my schema. The docs demonstrate using a generic as a field definition, so that doesn't seem to be the issue either.
My type definitions are as follows:
interface RawEdge<NodeType> {
node: NodeType
}
interface Edge<NodeType> extends RawEdge<NodeType> {
cursor: string
}
function Edge<NodeType>(NodeClass: ClassType<NodeType>) {
@ObjectType({ isAbstract: true })
abstract class Edge {
constructor(identifier: string, node: NodeType) {
this.cursor = Buffer.from(identifier).toString('base64')
this.node = node
}
@Field()
cursor: string
@Field(type => NodeClass)
node: NodeType
}
return Edge
}
interface Connection<NodeType, EdgeType extends Edge<NodeType>> {
totalCount: number
edges: EdgeType[]
nodes: NodeType[]
pageInfo: PageInfo
}
function Connection<NodeType, EdgeType extends Edge<NodeType>>(
NodeClass: ClassType<NodeType>,
EdgeClass: ClassType<Edge<NodeType>>
) {
@ObjectType({ isAbstract: true })
abstract class Connection {
constructor(edges: EdgeType[], page: PageQuery) {
this.totalCount = edges.length
this.edges = edgesToReturn<EdgeType>(edges, page)
this.nodes = this.edges.map(edge => edge.node)
this.pageInfo = pageInfo(this, edges)
}
@Field(type => Int)
totalCount: number
@Field(type => [EdgeClass])
edges: EdgeType[]
@Field(type => [NodeClass])
nodes: NodeType[]
@Field()
pageInfo: PageInfo
}
return Connection
}
EDIT: The following workaround resolves the type issue, which proves that the types being passed in have the proper decorators in their definition. However, this is extremely clunky, so I would like to avoid having to use it if possible.
function Edge<NodeType>(NodeClass: ClassType<NodeType>) {
@ObjectType({ isAbstract: true })
abstract class Edge {
constructor(identifier: string, node: NodeType) {
this.cursor = Buffer.from(identifier).toString('base64')
this.node = node
}
@Field()
cursor: string
// @Field decorator removed
node: NodeType
}
return Edge
}
function Connection<NodeType, EdgeType extends Edge<NodeType>>(
NodeClass: ClassType<NodeType>,
EdgeClass: ClassType<Edge<NodeType>>
) {
@ObjectType({ isAbstract: true })
abstract class Connection {
constructor(edges: EdgeType[], page: PageQuery) {
this.totalCount = edges.length
this.edges = edgesToReturn<EdgeType>(edges, page)
this.nodes = this.edges.map(edge => edge.node)
this.pageInfo = pageInfo(this, edges)
}
@Field(type => Int)
totalCount: number
@Field(type => [EdgeClass])
edges: EdgeType[]
// @Field decorator removed
nodes: NodeType[]
@Field()
pageInfo: PageInfo
}
return Connection
}
type RawDepartmentProductEdge = RawEdge<Product>
@ObjectType()
class DepartmentProductEdge extends Edge(Product) {
// Define the field type here instead of in the generic
@Field(type => Product)
node: Product
}
@ObjectType()
class DepartmentProductConnection extends Connection(Product, DepartmentProductEdge) {
// Define the field type here instead of in the generic
@Field(type => [Product])
nodes: Product[]
}
Context, for anyone who's curious:
The purpose of all this is to generate connections using something like this...
type RawDepartmentProductEdge = RawEdge<Product>
@ObjectType()
class DepartmentProductEdge extends Edge(Product) {}
@ObjectType()
class DepartmentProductConnection extends Connection(Product, DepartmentProductEdge) {}
...and then populate them like this...
const products = [] // Retrieve the products here
const edges = products.map(node => new DepartmentProductEdge(node.id, node)
const connection = new DepartmentProductConnection(edges, pagination)
...so that everything is as DRY as possible, but I can still add meta to the edges as needed.
Solution 1:
As it turns out, the code above works just fine: the problem was in sequencing my imports. I'm using a "bucket" file for exporting all of my GraphQL types, and while that works fine for bringing them into resolvers or middleware, importing from the bucket within type definitions causes weird circular dependency issues.
The solution is just to import types directly from their source files when doing type declarations. So, specifically as it applies to the example above:
// File system
schema
|- connection
| |- Connection.ts
| |- DepartmentProductConnection.ts
| |- Edge.ts
| |- PageInfo.ts
|- department
| |- Department.ts
|- product
| |- Product.ts
|- index.ts
// index.ts
export * from './connection/Connection'
export * from './connection/Edge'
export * from './connection/PageInfo'
export * from './connection/DepartmentProductConnection'
export * from './department/Department'
export * from './product/Product'
// DepartmentProductConnection.ts
import { ObjectType } from 'type-graphql'
// Don't do this!
// import { Connection, Edge, Product, RawEdge } from '..'
// Instead, do this.
import { Product } from '../product/Product'
import { Connection } from './Connection'
import { Edge, RawEdge } from './Edge'
type RawDepartmentProductEdge = RawEdge<Product>
@ObjectType()
class DepartmentProductEdge extends Edge(Product) {}
@ObjectType()
class DepartmentProductConnection extends Connection(Product, DepartmentProductEdge) {}