To handle typings of a query resolver that doesn鈥檛 directly return the right JSON, but passes data to other resolvers down the line (as parent data) I needed some weird stuff.
The use case is like this one:
https://github.com/howtographql/howtographql/issues/619#issuecomment-385309639
Problem 1: One cannot use the query resolver type as-is, since it expects a particular return type
Problem 2: One cannot use the parent/root type from the other resolver type as it is whatever the one in Problem 1 returned
I ended up using only the argument types of the query resolver and returning something arbitrary. Then I extracted the type of that return value and used that as the type of the parent argument of the other resolver. So now the return value of the query resolver is the (parent) argument type of another resolver.
// resolvers/Query.ts
export default {
feed: async (...allArgs: ArgumentTypes<QueryResolvers.FeedResolver>) => {
const [, { filter, skip, first, orderBy }, context] = allArgs
...
return {a:1, b:2}
}
}
// resolvers/Feed.ts
import { FeedResolvers, MyContext } from '../generated/resolvers-types'
import queryResolver from './Query'
export default {
links: async (parent, _args, context, info) => {
// typeof parent is now the right one: Promise<{a: number, b: number}>
const p = await parent
return (await parent).links
},
found: async (parent, _args) => {
return (await parent).links.length
},
} as FeedResolvers.Resolvers<MyContext, ReturnType<typeof queryResolver.feed>>
and i defined this two helper types
export type ArgumentTypes<F> = F extends (...args: infer A) => infer R ? A : never
// The default ReturnType generic breaks because of parent being of type never in the query resolver
export type ReturnType<F> = F extends (...args: infer A) => infer R ? R : never
Convoluted but it is super useful to know exactly what the parameter is, instantly found a bug.
Would it be possible to have helpers for that in the generated code somehow?
btw, I'm new to Typescript so be easy on me please
@kamilkisiela :)
I have no clue what is the issue here.
Since resolver has defined type for its parent and it doesn't match what you have, it's possible to overwrite it.
For example:
type Query {
post: Post
}
type Post {
id: String
author: User
}
type User {
id: String
name: String
}
Query has no parentPost has Post as a parentUser has User as a parenttype Query
posts and other fields in Query has never as a parentposts expects Post to be returnedtype Post
author and other fields in Post has Post as a parentauthor expects User to be returnedtype User
name and other fields in User has User as a parentname expects string to be returnedThat's by default, but if you want to overwrite the parent you can use a generic type that each resolver related type has.
For example, if you pass a string to resolve a User (that string is an id or something) you can set string as a parent like this:
const User: UserResolvers.Resolvers<any, string> {
id: (id, args, ctx, info) => id, // this is a string
name: (id, args, ctx, info) => getUserById(id).name
};
The <any, string> sets any as context and string as parent.
There are two tests that might interest you:
Default parents
https://github.com/dotansimha/graphql-code-generator/blob/cdbaa3ca4b46cbdd946a1d38ea426981c95e5a5b/packages/templates/typescript-resolvers/tests/resolvers.spec.ts#L343-L401
Custom parents thanks to Mappers
https://github.com/dotansimha/graphql-code-generator/blob/cdbaa3ca4b46cbdd946a1d38ea426981c95e5a5b/packages/templates/typescript-resolvers/tests/resolvers.spec.ts#L465-L524
Thanks for the very comprehensive answer!
My point is that the Query resolver won't return what is expected (in your example returns a string instead), so the typings for that can't be used directly. Furthermore I wanted to have the return value of that query be automatically _in synch_ with the parent argument of the User resolver.
That is why I extracted the ArgumentsType from QueryResolvers.FeedResolver but let the return type to be inferred, and then extracted that inferred return type and used it as the parent type of the Feed resolver.
// resolvers/Query.ts
export default {
feed: async (...allArgs: ArgumentTypes<QueryResolvers.FeedResolver>) => {
const [, { filter, skip, first, orderBy }, context] = allArgs
....
// resolvers/Feed.ts
import { FeedResolvers, MyContext } from '../generated/resolvers-types'
import queryResolver from './Query'
export default {
links: async (parent, _args, context, info) => {
// typeof parent is now the right one: Promise<{a: number, b: number}>
...
} as FeedResolvers.Resolvers<MyContext, ReturnType<typeof queryResolver.feed>>
(ReturnType is not the "native" one because that one assumes there are no args of type never)
Since that was useful for me, I thought you may have a comment on that approach and if it makes sense to support it directly from the generated code somehow.
@dbuezas
By typings cannot be used directly, you mean that you need to overwrite the generics?
You don't have to force or use as with the resolvers typings, just use the as-is for the entire resolver object.
By default, the generated output of the resolvers plugin is an object matching the structure of your type. You can override it with TypeScript generics, and you are doing it only once, and not for each resolver field, because they should all be the same.
For example:
If you have a a type:
type MyType {
id: ID!
foo: String
bar: Int
}
type Query {
myType: MyType
}
Then, you resolvers with the generated typings should look like that:
import { MyTypeResolvers, QueryResolvers } from './generates.ts';
const myTypeResolvers: MyTypeResolvers.Resolvers = {
id: (obj, args, context) => obj.id,
foo: (obj, args, context) => obj.foo,
bar: (obj, args, context) => obj.bar,
};
const myTypeResolver: QueryResolvers.MyTypeResolver = (root, args, context) => {
return {id: '1', foo: '2', bar: 3};
}
export default {
MyType: myTypeResolvers,
Query: { myType: myTypeResolver },
};
Now, let's say that your actual objects looks a bit different, and you are changing your myType resolver to return it:
const myTypeResolver: QueryResolvers.MyTypeResolver = (root, args, context) => {
return { _id: '1', inner: { foo: '1', bar: 2 }};
}
So declare a TypeScript interface that matches your object:
interface MyTypeCustomStructure {
_id: string;
inner: { foo: string; bar: number };
}
Next, use it in your Query resolver generics to tell the resolver how your Return object looks like:
const myTypeResolver: QueryResolvers.MyTypeResolver<MyTypeCustomStructure> = (root, args, context) => {
return { _id: '1', inner: { foo: '1', bar: 2 }};
}
And also, use it in the type resolvers to tell it how it looks:
const myTypeResolvers: MyTypeResolvers.Resolvers<{}, MyTypeCustomStructure> = {
id: (obj, args, context) => obj._id,
foo: (obj, args, context) => obj.inner.foo,
bar: (obj, args, context) => obj.inner.bar,
};
If you wish to avoid specifying the generics each time, you can use the mappers configuration, and this way you can just tell the plugin to always match between MyType and MyTypeCustomStructure:
schema: schema.graphql
generates:
out.ts:
config:
mappers:
MyType: MyTypeCustomStructure
plugins:
- add:
- "import { MyTypeCustomStructure } from './my-types.ts';"
- typescript-common
- typescript-server
- typescript-resolvers
@dotansimha
I have same problem. As you described above, CustomStructure can be ParentType. However, how can I use multiple ParentType? How can I achieve type information through [Type]Resolvers.Resolvers interface ?
My use-case
is is type guard to distinguish parent type in runtime// for example, MyTypeDBModel is mongodb document.
type MyTypeParentType = MyTypeDBModel | MyType
const myTypeResolvers: MyTypeResolvers.Resolvers<{}, MyTypeParentType> = {
id: (parent) => {
return is<MyTypeDBModel>(parent) ? parent._id : parent.id
}
}
How can I use MyTypeParentType as other Type's response type?
import { MyTypeParentType } from './path-to-my-type'
type OtherParentType = OtherModel | Other
const other: OtherResolvers.Resolver<{}, OtherParentType> = {
myType: (source) => source.myType
}
MyTypeResolver<MyType, TypeParent, Context>MyTypeResolver<MyTypeParentType, TypeParent, Context>Should I do like this?
// interface field override pseudo code. so complicate 馃
interface OtherResolver extends OtherResolvers.Resolver<{}, OtherParentType> {
myType?: OtherResolvers.MyTypeResolver<MyTypeParentType, OtherParentType, {}>
}
const other: OtherResolver = {
myType: (source) => source.myType
}
Most helpful comment
Thanks for the very comprehensive answer!
My point is that the Query resolver won't return what is expected (in your example returns a
stringinstead), so the typings for that can't be used directly. Furthermore I wanted to have the return value of that query be automatically _in synch_ with theparentargument of theUserresolver.That is why I extracted the
ArgumentsTypefromQueryResolvers.FeedResolverbut let the return type to be inferred, and then extracted that inferred return type and used it as the parent type of theFeedresolver.(
ReturnTypeis not the "native" one because that one assumes there are no args of typenever)Since that was useful for me, I thought you may have a comment on that approach and if it makes sense to support it directly from the generated code somehow.