Skip to main content

Structural vs Nominal Typing

Type systems can be classified into two categories: structural and nominal. In a structural type system, types are considered equivalent if they have the same structure or shape. In a nominal type system, types are considered equivalent if they have the same name. For example:

  • TypeScript is structurally typed
  • GraphQL is a nominally typed

This misalignment presents a challenge for us in Grats where we are trying to allow you to use structural TypeScript constructs to express nominal GraphQL types.

To illustrate the problem, consider the following example:

/** @gqlType */
type User = {
/** @gqlField */
name: string;
};

/** @gqlType */
type Post = {
/** @gqlField */
author: { name: string };
};

In TypeScript, Post.author is a valid User since the type literal { name: string } has the same structure as the User type. But, in order to construct the GraphQL definition for Post we need to know the name of Post.author's type.

Grats' solution

In grats we get around this by enforcing that any type annotation that Grats needs to infer as a GraphQL type must be a direct reference to a declaration. That declaration can be a type alias, interface, or class, but it must be a declaration. If you don't do this, Grats will report a, hopefully helpful, error.

To correct the above error, we might write:

/** @gqlType */
type User = {
/** @gqlField */
name: string;
};

/** @gqlType */
type Post = {
/** @gqlField */
author: User;
};

When Grats is inferring the type of Post.author it asks TypeScript to locate the declaration of the type User, which is the declaration where the type User is defined. From that, Grats can "see" the @gqlType tag and know exactly which nominal GraphQL type is being referenced.

Coincidentally, this ends up working out well, since the TypeScript library does not yet expose the ability to check if two types are type compatible as far as TypeScript is concerned (i.e. structurally equivalent) but it does expose the ability find the declaration of a type.

Limitations imposed

As a user of Grats, this means there are some things which might feel intuitive to write in TypeScript which Grats will not be able to understand. Grats knows about these cases and tries to report helpful errors when it encounters them.

For example, this means you cannot use intermediate type aliases, since the definition of the Admin type is not annotated as a GraphQL type.

/** @gqlType */
type User = {
/** @gqlField */
name: string;
};

type Admin = User;

/** @gqlType */
type Post = {
/** @gqlField */
author: Admin; // <-- Grats cannot infer the name of this type
};