question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Upgrade issues with TypeScript enums and nominal typing

See original GitHub issue

Hullo! I’m upgrading some Apollo projects to GraphQL 16 and running into slight difficulties with enums. The context is that we have utilities which create or modify AST nodes:

diff --git a/src/utilities/graphql/transform.ts b/src/utilities/graphql/transform.ts
index 2a5128d05..80db6d202 100644
--- a/src/utilities/graphql/transform.ts
+++ b/src/utilities/graphql/transform.ts
@@ -2,6 +2,7 @@ import { invariant } from '../globals';
 
 import {
   DocumentNode,
+  Kind,
   SelectionNode,
   SelectionSetNode,
   OperationDefinitionNode,
@@ -53,9 +54,9 @@ export type RemoveVariableDefinitionConfig = RemoveNodeConfig<
 >;
 
 const TYPENAME_FIELD: FieldNode = {
-  kind: 'Field',
+  kind: 'Field' as Kind.FIELD,
   name: {
-    kind: 'Name',
+    kind: 'Name' as Kind.NAME,
     value: '__typename',
   },
 };

The code is semantically sound, but TypeScript requires the assertion because you must reference enums directly wherever they are required (nominal typing).

enum Foo {
  a = "A",
  b = "B",
}

// This is a type error because we’re not referencing the enum directly.
const foo: Foo = "A";
// This is fine
const foo1: Foo = Foo.a;

The disadvantages of this change in GraphQL 16 are:

  1. We have to add runtime dependencies on the files which contain the enums.
  2. It’s slightly tricky to support ranges of graphql-js versions.

As a specific example of an enum-based difficulty, OperationTypeNode is an enum in GraphQL 16, but it’s not exported as a value in GraphQL 15, so there is no way to reference the enum value across 15 and 16, and you get type errors if you assign strings like "query" or "mutation" to the enum.

I think the above two concerns are reason enough to switch to using string unions, which are structurally typed, and don’t require direct reference. We can make this a non-breaking change by exporting a namespace instead of an enum, and shadowing the identifier on the type-level with a string union.

export type Kind = "Name" | "Document" /* ... */;
export namespace Kind {
  export const NAME = 'Name';
  export const DOCUMENT = 'Document';
  /* ... */
}

Thanks for reading. Let me know your thoughts!

Issue Analytics

  • State:open
  • Created 2 years ago
  • Comments:7 (4 by maintainers)

github_iconTop GitHub Comments

3reactions
benjiecommented, Nov 3, 2021

Could we do something like this, I wonder?

export const Kind = {
  OBJECT: 'ObjectType' as const,
  OBJECT_FIELD: 'ObjectField' as const
};
export type Kind = typeof Kind[keyof typeof Kind];

const a: Kind = Kind.OBJECT;
const b: Kind = 'ObjectType';

export interface ASTNode {
  kind: typeof Kind.OBJECT
}

https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBAYwgOwM7wNIEtkBM4C8cA3gFBxwDyAQgFICiAwgCoBccA5JQEYBWwCGMwCeYYBzgBDVIhToANOSp0mzAPoAxAJL0AMgBF2XPgJgaswADa4J02WhikAvgG5SoSLDgxRwONjxCb18IADN-HFwAbQBrYGEw4LFEgNwAXTdSJAcpdlSg1IA6GgYWN2z0OG48yKDjfkERMQ5Mj2h4HBhgKFDJBD8AQQBlZgA5CFw-MgoYyPYfZPCiktVnIA

1reaction
brainkimcommented, Nov 3, 2021

@IvanGoncharov It’s not a blocker! Thankfully with TypeScript there are workarounds. Just surfacing the struggle in case others having it.

@benjie keyof typeof is a new one to me! That’s fantastic.

You can also use the const on the object literal, and freeze the Object to prevent unwanted mutations too.

export const Kind = Object.freeze({
  OBJECT: 'ObjectType',
  OBJECT_FIELD: 'ObjectField',
} as const);
export type Kind = typeof Kind[keyof typeof Kind];

const a: Kind = Kind.OBJECT;
const b: Kind = 'ObjectType';

export interface ASTNode {
  kind: typeof Kind.OBJECT
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Handbook - Enums - TypeScript
Using enums can make it easier to document intent, or create a set of distinct cases. TypeScript provides both numeric and string-based enums....
Read more >
Why can't brand enums be unioned in Typescript 3?
I am trying to use brand enums to get nominal typing in Typescript 3 (See TypeScript Deep Dive for a description of this...
Read more >
Nominal Typing - TypeScript Deep Dive - Gitbook
The TypeScript type system is structural and this is one of the main motivating benefits. However, there are real-world use cases for a...
Read more >
“Experimental Beta” State | UI5 & TypeScript - SAP
There are sometimes errors in the API documentation. Fixing them can change the type definitions in a way that causes TS compiler errors...
Read more >
Comparing TypeScript's union types, enums and const enums
Enum is an example of a nominal type (enums are not equal if they differ by name). Unions represent structural types (if literal...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found