Jan 28th, 2022 - written by Kimserey with .
Following up our previous post on generic types, we created a ServiceResponse<T>
type which had a statusCode: string
field to differentiate between the responses. In Typescript, a common pattern to differentiate between success and error is to create a union. In today’s post, we will see how we can leverage typegraphql to translate Typescript unions into graphql schema unions.
Taking back our types from our previous post:
1
2
3
4
5
6
7
8
@ObjectType()
export class Person {
@Field(() => ID)
id: string;
@Field(() => String)
name: string;
}
With the following Error
type:
1
2
3
4
5
6
7
8
@ObjectType()
export class Error {
@Field()
errorMessage: string;
@Field()
statusCode: string;
}
We want to implement a union representing the possible values, Person | Error
. A union type ServiceResponse = Person | Error
can be created easily in Typescript but union can’t directly be translated to graphql, we must use createUnionType()
from typegraphql:
1
2
3
4
const PersonServiceResponse = createUnionType({
name: "PersonServiceResponse",
types: () => [Person, Error] as const,
});
The const
assertion as const
will infer to the most narrow type. It is necessary here as [Person, Error]
would otherwise be inferred as (typeof Person | typeof Error)[]
but what we want here is to signal to Typescript that it is a tuple [typeof Person, typeof Error]
so that typegraphql can translate it to a union.
We can then use the union type in our resolver:
1
2
3
4
5
6
7
@Query(() => PersonServiceResponse)
getPerson(): typeof PersonServiceResponse {
const person = new Person();
person.id = "123";
person.name = "hello";
return person;
}
And we can see that schema generated is a union as expected:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Query {
getPerson: PersonServiceResponse!
}
union PersonServiceResponse = Person | Error
type Person {
id: ID!
name: String!
}
type Error {
errorMessage: String!
statusCode: String!
}
Notice that we had to create an instance of Person
:
1
2
3
4
const person = new Person();
person.id = "123";
person.name = "hello";
return person;
This is required for typegraphql to know the type returned - we must return an instance of the object type rather than a plain object. Else we would get the following error:
1
Cannot resolve type for union PersonServiceResponse! You need to return instance of object type class, not a plain object!
Similarly, the error type should be returned as a concrete instance as well:
1
2
3
4
5
6
7
@Query(() => PersonServiceResponse)
getPerson(): typeof PersonServiceResponse {
const error = new Error();
error.errorMessage = "An error occurred.";
error.statusCode = "missing_data";
return error;
}
With the union in place, we can then query the union as expected:
1
2
3
4
5
6
7
8
9
10
11
12
query Query {
getPerson {
__typename
... on Person {
id
}
... on Error {
errorMessage
}
}
}
If we want to omit the concrete type instances, createUnionType
can also be provided with a function resolving the type:
1
2
3
4
5
6
export const PersonServiceResponse = createUnionType({
name: "PersonServiceResponse",
types: () => [Person, Error] as const,
resolveType: (value) =>
(value as Error).errorMessage === undefined ? Person : Error,
});
With a type assertion, (value as Error)
, we can check if one of the attribute is present on the object and return the type accordingly. We are then able to return a plain object, reducing the risk of omitting to return an instance of the class.
1
2
3
4
5
6
7
@Query(() => PersonServiceResponse)
getPerson(): typeof PersonServiceResponse {
return {
id: "123",
name: "hello",
};
}
ServiceResponse
From there we can generalise our ServiceResponse
union so that we can easily create union type with | Error
. To do that we wrap the createUnionType
into a generic function accepting a ClassType<T>
:
1
2
3
4
5
6
7
8
function ServiceResponse<T>(cls: ClassType<T>) {
return createUnionType({
name: `${cls.name}ServiceResponse`,
types: () => [cls, Error] as const,
resolveType: (value) =>
(value as Error).errorMessage === undefined ? cls : Error,
});
}
This then allow us to create ServiceResponse
for different type with minimal effort:
1
2
export const PersonServiceResponse = ServiceResponse(Person);
export const HomeServiceResponse = ServiceResponse(Home));
and use them in our resolver:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Resolver()
export class PersonResolver {
@Query(() => PersonServiceResponse)
getPerson(): typeof PersonServiceResponse {
return {
id: "123",
name: "hello",
};
}
@Query(() => HomeServiceResponse)
getHome(): typeof HomeServiceResponse {
return {
id: "123",
address: "hello",
};
}
}
And that concludes today’s post!
In today’s post we looked at how we could create union type for object type in graphql. We started by looking at the basic setup, we then moved on to see how it could be simplified by inferring the type through a function and finally we looked at how we could generalise the union construct. I hope you liked this post and I’ll see you on the next one!