InterfaceName: Generating Composable GraphQL and TypeScript Types

Published On
Code snippets using interfaceName property
Code snippets using interfaceName property

Generating schemas and types can be cumbersome if you want to access deeply nested types. Now we have interfaceName to alleviate that.

InterfaceName in TypeScript

The generation of schemas and types within Payload has always been simple. For GraphQL, you run yarn payload generate:graphqlschema and a GraphQL schema is created. For TypeScript, you run yarn payload generate:types and a type definition file is created.

While this process works well, it can become tedious when working with deeply nested types. With the new interfaceName property, you can create a reusable type that gets hoisted to the top level in your generated TS and GraphQL files.

This new pattern really shines in TypeScript when creating blocks. Here we have an example Pages config, it has a blocks field named layout with two blocks to choose from.

1
import { CollectionConfig } from 'payload/types'
2
3
const Pages: CollectionConfig = {
4
slug: 'pages',
5
fields: [
6
{
7
type: 'blocks',
8
name: 'layout',
9
blocks: [{
10
slug: 'sectionHeader',
11
interfaceName: 'SectionHeaderBlock',
12
/** ^^ new property */
13
fields: [
14
{
15
type: 'text',
16
name: 'heading',
17
}
18
],
19
},
20
{
21
slug: 'form',
22
interfaceName: 'FormBlock',
23
/** ^^ new property */
24
fields: [
25
{
26
type: 'text',
27
name: 'title',
28
}
29
],
30
}],
31
}
32
],
33
}
34

In the past the above collection config would generate the following typescript types:

1
/** generated types file - without using interfaceName */
2
3
export interface Page {
4
layout: ({
5
heading: string;
6
blockType: 'sectionHeader';
7
id?: string;
8
blockName?: string;
9
} | {
10
title: string;
11
blockType: 'form';
12
id?: string;
13
blockName?: string;
14
})[]
15
}
16
17
/**
18
then you would have to create
19
a custom type yourself like so
20
*/
21
type SectionHeaderBlock = Extract<Page['layout'][0], { blockType: 'sectionHeader' }>

You no longer need to create the custom type yourself, you can now use interfaceName and Payload will generate top level types for you. Using this approach means the typescript type is no longer tied to the Pages collection and you can name it uniquely.

1
/** generated types file - using interfaceName */
2
3
export interface Page {
4
layout: (SectionHeaderBlock | FormBlock)[]
5
}
6
7
/**
8
automatically creates types for you
9
using the interfaceName properties
10
*/
11
export interface SectionHeaderBlock {
12
heading: string;
13
id?: string;
14
blockName?: string;
15
blockType: 'sectionHeader';
16
}
17
export interface FormBlock {
18
title: string;
19
id?: string;
20
blockName?: string;
21
blockType: 'form';
22
}

This is super powerful. Using the component based approach to build your frontends should be pretty intuitive using this feature. In the above case you could build a SectionHeadingBlock and a FormBlock component, import the types that you generated and you are set up for success. No more needing to extract the types from the parent collection.

InterfaceName in GraphQL

This new feature is just as useful in GraphQL. Let's say you want to fragment parts of a group field for reusability. The following collection configs, Pages and Posts, both use a shared field:

1
import { CollectionConfig } from 'payload/types'
2
3
const SharedMetaField = {
4
type: 'group',
5
name: 'metadata',
6
interfaceName: 'SharedMetadata',
7
/** ^^ new property */
8
fields: [
9
{
10
type: 'text',
11
name: 'title',
12
},
13
{
14
type: 'text',
15
name: 'description',
16
},
17
{
18
type: 'text',
19
name: 'keywords',
20
}
21
]
22
}
23
24
const Pages: CollectionConfig = {
25
slug: 'pages',
26
fields: [SharedMetaField],
27
}
28
29
const Posts: CollectionConfig = {
30
slug: 'posts',
31
fields: [SharedMetaField],
32
}
33

When interfaceName is not used, the GraphQL Schema generated looks like:

1
/** generated schema file - without using interfaceName */
2
3
type Pages {
4
id: String
5
metadata: Pages_Metadata
6
updatedAt: DateTime
7
createdAt: DateTime
8
}
9
10
type Pages_Metadata {
11
title: String
12
description: String
13
keywords: String
14
}
15
16
type Posts {
17
id: String
18
metadata: Posts_Metadata
19
updatedAt: DateTime
20
createdAt: DateTime
21
}
22
23
type Posts_Metadata {
24
title: String
25
description: String
26
keywords: String
27
}

And when interfaceName is used, the GraphQL Schema will look like:

1
/** generated schema file - using interfaceName */
2
3
type Pages {
4
id: String
5
metadata: SharedMetadata
6
updatedAt: DateTime
7
createdAt: DateTime
8
}
9
10
type Posts {
11
id: String
12
metadata: SharedMetadata
13
updatedAt: DateTime
14
createdAt: DateTime
15
}
16
17
type SharedMetadata {
18
title: String
19
description: String
20
keywords: String
21
}

This unlocks the ability to create fragments to use within your GraphQL queries:

1
const PARTIAL_META = gql`
2
fragment PartialMeta on SharedMetadata {
3
title
4
description
5
}
6
`
7
const query = gql`
8
Pages {
9
metadata: {
10
...PartialMeta
11
}
12
}
13
Posts {
14
metadata: {
15
...PartialMeta
16
}
17
}
18
`
19

Overall I think this is a small but big win pushing Payload’s type systems further. This is nice when you need to hoist a custom type up to the top for any reason. This is now available for block, array, group and named-tab fields. Here is a link to the PR if you want to take a closer look.

Wrap up

The `interfaceName` property gives you more control as a developer. Type composability can be made much simpler, no need to extract deeply nested types.

Learn More