diff --git a/src/tests/fixtures/interfaces/TagAttachedToWrongNode.ts.expected b/src/tests/fixtures/interfaces/TagAttachedToWrongNode.ts.expected index dcb94a3c..9cb751d6 100644 --- a/src/tests/fixtures/interfaces/TagAttachedToWrongNode.ts.expected +++ b/src/tests/fixtures/interfaces/TagAttachedToWrongNode.ts.expected @@ -7,7 +7,7 @@ function Foo() {} ----------------- OUTPUT ----------------- -src/tests/fixtures/interfaces/TagAttachedToWrongNode.ts:1:5 - error: `@gqlInterface` can only be used on interface declarations. e.g. `interface MyInterface {}` +src/tests/fixtures/interfaces/TagAttachedToWrongNode.ts:1:5 - error: `@gqlInterface` can only be used on interface or abstract class declarations. e.g. `interface MyInterface {}` or `abstract class MyInterface {}` 1 /** @gqlInterface Person */ ~~~~~~~~~~~~~~~~~~~~~ diff --git a/website/docs/04-docblock-tags/01-types.mdx b/website/docs/04-docblock-tags/01-types.mdx index aed688d9..914336cd 100644 --- a/website/docs/04-docblock-tags/01-types.mdx +++ b/website/docs/04-docblock-tags/01-types.mdx @@ -14,7 +14,7 @@ GraphQL types can be defined by placing a `@gqlType` docblock directly before a: - Interface declaration - Type alias of a literal type or `unknown` -If model your GraphQL resolvers using classes, simply add a `@gqlType` docblock +To model your GraphQL resolvers using classes, simply add a `@gqlType` docblock before the class containing that type's resolvers. @@ -26,6 +26,10 @@ an interface. +:::info +Types declared using TypeScript classes or interfaces will inherit GraphQL fields and interfaces from all classes/interfaces they extend/implement. +::: + Finally, if your types are represented in your code by named types, simply add a `@gqlType` docblock before the type alias which describes that type. @@ -73,7 +77,7 @@ Will generate the following GraphQL schema: -#### Type Alias +### Type Alias Types declared using a type alias _may not_ implement a GraphQL interface. Instead, we recommend using a TypeScript interface to model your GraphQL type. @@ -84,5 +88,5 @@ See [Interfaces](./05-interfaces.mdx) for more information about defining interf ::: :::note -Grats must be able to determine the typename of any type which implements an interface. To achieve this Grats will validate that all implementors of an interface either define a `__typename: "MyType" as const` property or are exported classes. Grats can use either to determin the typename at runtime. +Grats must be able to determine the typename of any type which implements an interface. To achieve this Grats will validate that all implementors of an interface either define a `__typename: "MyType" as const` property or are exported classes. Grats can use either to determine the typename at runtime. ::: diff --git a/website/docs/04-docblock-tags/05-interfaces.mdx b/website/docs/04-docblock-tags/05-interfaces.mdx index e1f8efe1..ea56af23 100644 --- a/website/docs/04-docblock-tags/05-interfaces.mdx +++ b/website/docs/04-docblock-tags/05-interfaces.mdx @@ -1,5 +1,6 @@ import GratsCode from "@site/src/components/GratsCode"; import InterfaceDeclaration from "!!raw-loader!./snippets/04-interface-declaration.out"; +import InterfaceClassDeclaration from "!!raw-loader!./snippets/04-interface-class-declaration.out"; import NodeInterface from "!!raw-loader!./snippets/04-merged-interface-renaming.out"; import InterfaceImplementingInterface from "!!raw-loader!./snippets/04-interface-implement-interface.out"; import InterfaceFieldCommonImpl from "!!raw-loader!./snippets/04-interface-field-common-impl.out"; @@ -9,12 +10,21 @@ import InterfaceFieldCommonImpl from "!!raw-loader!./snippets/04-interface-field GraphQL interfaces can be defined by placing a `@gqlInterface` docblock directly before an: - Interface declaration +- Class declaration - +To model your GraphQL interface using classes, simply add a `@gqlInterface` docblock +before the class. Note that with this approach, implementors of the interface will also inherit the field methods. + + -## Shared Field Implementation +If your types are represented in your code by TypeScript interfaces, simply add a +`@gqlType` docblock before the interface representing that type. Note that by +using `@gqlType` on an interface, Grats will treat it as a GraphQL type and not +an interface. -If you wish to define field which has a single implementation that is shared by all implementors, you can use the [function style of `@gqlField`](./02-fields.mdx#functional-style-fields) to define the field. This will automatically add the field to all implementors of the interface. + + +If you wish to provide shared implementations for fields in an interface defined in this way, you can use the [functional style fields](./02-fields.mdx#functional-style-fields). @@ -32,10 +42,8 @@ Which will generate the following GraphQL schema: ---- - -:::note -Each implementor of an interface must declare define all the fields required by the interface with `/** @gqlField */`. This means that if you have an interface that implements another interface, you must define all the fields required by both interfaces. +:::info +Interfaces will inherit GraphQL fields and interfaces from all interfaces they extend. ::: ## Merged Interfaces diff --git a/website/docs/04-docblock-tags/06-unions.mdx b/website/docs/04-docblock-tags/06-unions.mdx index eee510e9..93db72eb 100644 --- a/website/docs/04-docblock-tags/06-unions.mdx +++ b/website/docs/04-docblock-tags/06-unions.mdx @@ -36,5 +36,5 @@ All the types referenced in the TypeScript union but be explicitly annotated wit ::: :::note -Grats must be able to determine the typename of any type which members of a union. To achieve this Grats will validate that all member types either define a `__typename: "MyType" as const` property or are exported classes. Grats can use either to determin the typename at runtime. +Grats must be able to determine the typename of any type which members of a union. To achieve this Grats will validate that all member types either define a `__typename: "MyType" as const` property or are exported classes. Grats can use either to determine the typename at runtime. ::: diff --git a/website/docs/04-docblock-tags/snippets/04-interface-class-declaration.grats.ts b/website/docs/04-docblock-tags/snippets/04-interface-class-declaration.grats.ts new file mode 100644 index 00000000..1a3b6d91 --- /dev/null +++ b/website/docs/04-docblock-tags/snippets/04-interface-class-declaration.grats.ts @@ -0,0 +1,7 @@ +/** @gqlInterface */ +class MyInterfaceClass { + /** @gqlField */ + someField(): string { + return "someField"; + } +} diff --git a/website/docs/04-docblock-tags/snippets/04-interface-class-declaration.out b/website/docs/04-docblock-tags/snippets/04-interface-class-declaration.out new file mode 100644 index 00000000..ba6b9a99 --- /dev/null +++ b/website/docs/04-docblock-tags/snippets/04-interface-class-declaration.out @@ -0,0 +1,30 @@ +/** @gqlInterface */ +class MyInterfaceClass { + /** @gqlField */ + someField(): string { + return "someField"; + } +} + +=== SNIP === +interface MyInterfaceClass { + someField: String +} +=== SNIP === +import { GraphQLSchema, GraphQLInterfaceType, GraphQLString } from "graphql"; +export function getSchema(): GraphQLSchema { + const MyInterfaceClassType: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: "MyInterfaceClass", + fields() { + return { + someField: { + name: "someField", + type: GraphQLString + } + }; + } + }); + return new GraphQLSchema({ + types: [MyInterfaceClassType] + }); +} diff --git a/website/docs/04-docblock-tags/snippets/04-interface-declaration.grats.ts b/website/docs/04-docblock-tags/snippets/04-interface-declaration.grats.ts index 00fa0b15..20b0fb56 100644 --- a/website/docs/04-docblock-tags/snippets/04-interface-declaration.grats.ts +++ b/website/docs/04-docblock-tags/snippets/04-interface-declaration.grats.ts @@ -2,7 +2,7 @@ * A description of my interface. * @gqlInterface MyInterfaceName */ -interface MyClass { +interface MyInterface { /** @gqlField */ someField: string; } diff --git a/website/docs/04-docblock-tags/snippets/04-interface-declaration.out b/website/docs/04-docblock-tags/snippets/04-interface-declaration.out index 97db0c08..2a3c4adf 100644 --- a/website/docs/04-docblock-tags/snippets/04-interface-declaration.out +++ b/website/docs/04-docblock-tags/snippets/04-interface-declaration.out @@ -2,7 +2,7 @@ * A description of my interface. * @gqlInterface MyInterfaceName */ -interface MyClass { +interface MyInterface { /** @gqlField */ someField: string; } diff --git a/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.grats.ts b/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.grats.ts index 6add05a9..5da5ceb5 100644 --- a/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.grats.ts +++ b/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.grats.ts @@ -8,7 +8,6 @@ interface Person { // highlight-start interface User extends Person { // highlight-end - /** @gqlField */ name: string; /** @gqlField */ diff --git a/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.out b/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.out index 1991858e..55e6fe69 100644 --- a/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.out +++ b/website/docs/04-docblock-tags/snippets/04-interface-implement-interface.out @@ -8,7 +8,6 @@ interface Person { // highlight-start interface User extends Person { // highlight-end - /** @gqlField */ name: string; /** @gqlField */ diff --git a/website/docs/05-guides/09-inheritance.mdx b/website/docs/05-guides/09-inheritance.mdx new file mode 100644 index 00000000..f20df078 --- /dev/null +++ b/website/docs/05-guides/09-inheritance.mdx @@ -0,0 +1,35 @@ +# Inheritance + +Because Grats marries the worlds of TypeScript and GraphQL ends up implementing a version of inheritance that is something of a hybrid between the two. This guide explains the principles of inheritance in Grats and shows practical examples of how it works in practice. + +Lets start with a reminder of how inheritnace works in TypeScript and GraphQL respectively and then we'll see how Grats combines the two. + +## TypeScript Inheritance + +TypeScript implemenets single inheritance, meaning that a class can only inherit from at most one other class. This is done using the `extends` keyword. When a class extends another class, it inherits all the fields and methods of the parent class. Note that this works recursively. + +Classes in TypeScript may also implement interfaces. While a class may implement multiple interfaces, the interfaces are simply type-checking contracts and do not add any fields or methods to the class. + +## GraphQL Inheritance + +GraphQL does not have any notion of inheritance. While GraphQL types and interfacs can implement interfaces, implementing an interface does not automatically add any fields to the type. It's simply a type-checking contract, and each type or interface must explicitly define the fields required by any interfaces it implements. + +Similarly, if a type implements an iterface with itself implements a second interface, the type must explicitly declare that it also implements that second interface, or else it will trigger a validation error. + +## Grats Inheritance + +Grats tries to match the semantics of TypeScript as much as possible while still generating a valid GraphQL schema. + +If a `@gqlType` class extends another class, Grats will automatically infer that any fields marked as `@gqlField` in the parent class should also be exposed in the child class. + +Similarly, if a `@gqlType` class implements interfaces, Grats will automatically infer that any fields marked as `@gqlField` in any of those interface should also be exposed on the class. + +Both of these are done recursively, so if a parent class implements an interface which extends another interface, the child class will inherit all the fields from both interfaces. + +### Overrides/Precedence + +In terms of runtime behavior, the code being executed is TypeScript and thus will match TypeScript's semantics. Methods on a child class will override methods on a parent class. + +If a field or method is marked as a `@gqlField` on both a parent class and a child class, the child class will take precedence. This can be useful if you want the child class to have a different GraphQL declaration from its parent. For example, you might want to add a different description, or one might be deprecated while the other is not, or the child might have a return type which is a subtype of the parent's return type. + +### Function style fields diff --git a/website/src/components/PlaygroundFeatures/defaultState.ts b/website/src/components/PlaygroundFeatures/defaultState.ts index c0fde9c0..de98512e 100644 --- a/website/src/components/PlaygroundFeatures/defaultState.ts +++ b/website/src/components/PlaygroundFeatures/defaultState.ts @@ -37,6 +37,7 @@ export const DEFAULT_STATE: State = { config: { nullableByDefault: true, reportTypeScriptTypeErrors: true, + importModuleSpecifierEnding: "", }, view: { outputOption: "sdl", diff --git a/website/src/components/PlaygroundFeatures/store.ts b/website/src/components/PlaygroundFeatures/store.ts index bebf36bf..16aa757e 100644 --- a/website/src/components/PlaygroundFeatures/store.ts +++ b/website/src/components/PlaygroundFeatures/store.ts @@ -10,6 +10,7 @@ export type State = { config: { nullableByDefault: boolean; reportTypeScriptTypeErrors: boolean; + importModuleSpecifierEnding: string; }; view: { showGratsDirectives: boolean;