I am not sure how best to ask this question, so I hope this simplest reproduction makes sense.
This code
// SDL
// type User {
// name: String!
// post: Post!
// }
// type Post {
// body: String!
// author: User!
// }
// type Query {
// user: User!
// }
const User = objectType({
name: 'User',
definition: t => {
t.string('name');
t.field('post', {
type: Post,
resolve: (): any => ({ body: 'hello' }),
});
},
});
const Post = objectType({
name: 'Post',
definition: t => {
t.string('body');
t.field('author', {
type: User,
resolve: (): any => ({ name: 'Johannes' }),
});
},
});
const Query = objectType({
name: 'Query',
definition: t => {
t.field('user', {
type: User,
resolve: () => ({ name: 'Johannes' }),
});
},
});
produces these ts type definitions
declare global {
interface NexusGen extends NexusGenTypes {}
}
export interface NexusGenInputs {
}
export interface NexusGenEnums {
}
export interface NexusGenRootTypes {
Post: { // root type
body: string; // String!
}
Query: {};
User: { // root type
name: string; // String!
}
String: string;
Int: number;
Float: number;
Boolean: boolean;
ID: string;
}
export interface NexusGenAllTypes extends NexusGenRootTypes {
}
export interface NexusGenFieldTypes {
Post: { // field return type
author: NexusGenRootTypes['User']; // User!
body: string; // String!
}
Query: { // field return type
user: NexusGenRootTypes['User']; // User!
}
User: { // field return type
name: string; // String!
post: NexusGenRootTypes['Post']; // Post!
}
}
export interface NexusGenArgTypes {
}
export interface NexusGenAbstractResolveReturnTypes {
}
export interface NexusGenInheritedFields {}
export type NexusGenObjectNames = "Post" | "Query" | "User";
export type NexusGenInputNames = never;
export type NexusGenEnumNames = never;
export type NexusGenInterfaceNames = never;
export type NexusGenScalarNames = "Boolean" | "Float" | "ID" | "Int" | "String";
export type NexusGenUnionNames = never;
export interface NexusGenTypes {
context: any;
inputTypes: NexusGenInputs;
rootTypes: NexusGenRootTypes;
argTypes: NexusGenArgTypes;
fieldTypes: NexusGenFieldTypes;
allTypes: NexusGenAllTypes;
inheritedFields: NexusGenInheritedFields;
objectNames: NexusGenObjectNames;
inputNames: NexusGenInputNames;
enumNames: NexusGenEnumNames;
interfaceNames: NexusGenInterfaceNames;
scalarNames: NexusGenScalarNames;
unionNames: NexusGenUnionNames;
allInputTypes: NexusGenTypes['inputNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['scalarNames'];
allOutputTypes: NexusGenTypes['objectNames'] | NexusGenTypes['enumNames'] | NexusGenTypes['unionNames'] | NexusGenTypes['interfaceNames'] | NexusGenTypes['scalarNames'];
allNamedTypes: NexusGenTypes['allInputTypes'] | NexusGenTypes['allOutputTypes']
abstractTypes: NexusGenTypes['interfaceNames'] | NexusGenTypes['unionNames'];
abstractResolveReturn: NexusGenAbstractResolveReturnTypes;
}
In my client I am trying to give types to the following query
{
user {
name
post {
body
}
}
}
the closest I can get is NexusGenFieldTypes['Query']['user'] but that returns NexusGenRootTypes['User'] which does not have a body. Is there a reason that NexusGenFieldTypes is not the following? (linking to field types instead of root types)
export interface NexusGenFieldTypes {
Post: { // field return type
author: NexusGenFieldTypes['User']; // User!
body: string; // String!
}
Query: { // field return type
user: NexusGenFieldTypes['User']; // User!
}
User: { // field return type
name: string; // String!
post: NexusGenFieldTypes['Post']; // Post!
}
}
Yeah, so the thought here is that nexus can't know whether fields with resolvers have that actual field on the "root type" or not, for instance you could have this:
const User = objectType({
name: 'User',
definition: t => {
t.string('firstName');
t.string('lastName');
t.string('fullName', (root) => `${root.firstName} ${root.lastName}`)
},
});
What should the typing of root be? Nexus (rightly) assumes that it's:
{
firstName: string
lastName: string
}
rather than:
{
firstName: string
lastName: string
fullName: string
}
because fullName does not actually exist on the root object. However nexus can only infer so much... inferring the types of the root is really meant as a backup option for types that aren't provided via the typegenAutoConfig. See the star wars or ghost for examples of that.
I have a separate ticket open to make something which would allow you to define the root types in runtime code rather than mapping them. Still need to think more about this - interested to hear feedback.
It's a good question though, and something that needs to be much better documented since the type auto generation/inference is probably 90% of what makes Nexus really powerful. I think maybe if typegenAutoConfig or similar isn't specified we should add a runtime warning with a link to documentation about how to make it work correctly.
Let me know if this makes sense or if I could clarify further.
Thank you @tgriesser. I agree that rootTypes should not include fields that have resolvers defined.
It's possible I don't understand the role of NexusGenFieldTypes. In your example fullName would _not_ exist on rootTypes but it _would_ exist on fieldTypes.
NexusGenFieldTypes is so nexus can know what the return type/backing type of the field should be based on the GraphQL definition.
// type error on fullName because NexusGenFieldTypes['user']['fullName'] = 'string'
const User = objectType({
name: 'User',
definition: t => {
t.string('firstName');
t.string('lastName');
t.string('fullName', (root) => 1)
},
});
Ah, that 100% clears up what NexusGenFieldTypes is for. Thank you.
The answer may very well be that it is out of scope for this project, but what would you think of adding another NexusGen*Types that represents the types of the values returned by executing a query on the schema. I know I can use Apollo codegen or other similar tools, but it would be nice to have those types generated automatically without having to set up another step in the build process (and nexus already has all of the information available without having to run a schema introspection).
@ecwyne I don't think that's even enough. After tinkering with the code, I was able to make it generate an exported interface called NexusGenResponseTypes. This interface is similar to NexusGenFieldTypes, but this one maps fields to NexusGenFieldTypes instead mapping them to NexusGenRootTypes, fixing our problem with nested relationships.
But that's not enough. Even with that, it is still necessary for me to declare the mapping of response type explicitly, and that's not much better than simply semi-duplicating the schema myself in the frontend.
// Semi-duplicated schema.
user: NexusGenFieldTypes['User']
users: Array<NexusGenFieldTypes['User']>
// This is not really that much better.
user: NexusGenResponseTypes['user']
users: NexusGenResponseTypes['users']
We want the code above to be generated for us. For that, Nexus.js would have to parse the client query, which means expanding the scope of the Nexus.js project into the front-end.
@tgriesser Would you reconsider expanding the scope of Nexus.js to the front-end as well? Or perhaps it could be developed as part of a plugin system.
For now, the proper solution is using one of these tools:
@tgriesser in my plugin library I'm adding a few fields based on some definition provided by the library user: https://github.com/JCMais/nexus-plugins/blob/6150e9793a48d93ec6a2e8501019cf1306b99266/plugins/relay-global-id/src/index.ts#L115-L132
@passionkind noticed (in this PR) that those fields are not added to the root type. Is there any way for me, as a plugin author, to add some fields to the root type? In my example above, I need the source object to have an id property when the user creates a field using t.relayGlobalId('id'). Currently, this is not type-safe because the user is able to omit the id field in the source object.