I have created a Typescript guard to check if a GraphQL object is of a certain type (by checking the __typename
attribute).
type GraphQLObject = { __typename?: string };
type GraphQLTypename<O extends GraphQLObject> = O['__typename'];
export const isType = <O extends GraphQLObject, T extends GraphQLTypename<O>>(
obj: O,
typename: T
): obj is Extract<O, { __typename: T }> => obj.__typename === typename;
So I can correctly have a guard for the following query.
query RetrieveNode($id: ID!) {
node(key: $key) {
__typename
... on Article {
properties {
name
}
}
... on Page {
properties {
name
}
}
}
}
type Node = { __typename: 'Article', properties: { name: string } } | { __typename: 'Person', properties: { email: string } }
const data: { node: Node } = { node: { __typename: 'Article', properties: { name: 'Foo' } } }
isType(data.node, 'Article')
data.node.properties.name // No error as the guard succeeded
I also have an enum of allowed content types.
enum ContentTypes {
Article = 'Article',
Post = 'Post',
}
But I am not allowed to pass the enum directly to the guard.
isType(data.node, ContentTypes.Article) // raises Typescript error
I would like to create a guard which wraps the isType
one.
export const isContentType = <O extends GraphQLObject, T extends GraphQLTypename<O>>(
obj: O,
typename: T & ContentTypes
): obj is Extract<O, { __typename: T }> =>
isType(obj, typename)
But this unfortunately always results in never
.
Typescript raises an error because you expect an Enum (ContentTypes.Article) to match a string literal type 'Article'.
You have to pick one strategy or the other :
Using Enum :
type Node =
{ __typename: ContentTypes.Article, properties: { name: string } }
| { __typename: ContentTypes.Post, properties: { email: string } }
const isContentType = <O extends GraphQLObject, T extends GraphQLTypename<O>>(
obj: O,
typename: T & ContentTypes
): obj is Extract<O, { __typename: T }> =>
isType(obj, typename)
const data: { node: Node } = { node: { __typename: ContentTypes.Article, properties: { name: 'Foo' } } }
if (isContentType(dataA.node, ContentTypes.Article)){
dataA.node.properties.name // No typescript error
}
Using string literal type :
export type ContentTypesUnion = 'Article' | 'Post'
type Node =
{ __typename: 'Article', properties: { name: string } }
| { __typename: 'Post', properties: { email: string } }
export const isContentType = <O extends GraphQLObject, T extends GraphQLTypename<O>>(
obj: O,
typename: T & ContentTypesUnion
): obj is Extract<O, { __typename: T }> =>
isType(obj, typename)
const data: { node: Node } = { node: { __typename: 'Article', properties: { name: 'Foo' } } }
if (isContentType(dataA.node, 'Article')){
dataA.node.properties.name
}
I don't exactly know your use case, but as data will propbably be the result of your query, you might want to use string literal types.
For more information about the differences between enum and string literal types, see Difference between string enums and string literal types in TS