Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make classes and interfaces inherit GraphQL fields and interfaces from classes they extend and interfaces they implement #145

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

captbaritone
Copy link
Owner

@captbaritone captbaritone commented Jul 9, 2024

/** @gqlType */
class Parent {
  /** @gqlField */
  parentField: string;
}

class Intermediate extends Parent {}

/** @gqlType */
export class Child extends Intermediate {
  /** @gqlField */
  childField: string;
}

Would now extract:

type Child {
  childField: String
  parentField: String
}

type Parent {
  parentField: String
}

This change also allows you to define GraphQL interfaces using TypeScript types.

TODO

  • Documentation
  • Integration tests
  • Validate/test method resolution order
  • Don't use private methods/properties of the typecontext
  • Add caching to avoid duplicate recursion in heritage propagation
  • Changelog (this is a breaking change!)

Copy link

netlify bot commented Jul 9, 2024

Deploy Preview for grats failed.

Name Link
🔨 Latest commit 5c7c0ae
🔍 Latest deploy log https://app.netlify.com/sites/grats/deploys/66c57abbb652600008d4ab72

@captbaritone
Copy link
Owner Author

This feature has made me realize I want Grats to be more about trying to derive GraphQL matching the semantics of idiomatic TypeScript code where possible.

So, our goal here should be to match the semantics of TypeScript. One way to view a /** @gqlField */ annotation is as making that field more specific. In typing parlance, an annotated field is a subtype of an unannotated field. What would that mean:

  • Interfaces do not change the implementors in any way. Neither the types not the runtime behavior are modified. They simply enforce constraints on the implementor’s types. If we see annotated fields as a subtype of an unannotated field, that would mean an annotation on an interface’s field should force all implementors to also annotate the field.
    • What about descriptions and @deprecated? How do those fit in those framings?
  • By contrast extending (or inheriting from) a parent class does change the class. It adds additional fields which are not present on the classes definition. However, if the class does decide to define its own definition of the field, it must define that field as a subtype of the parent classes field. This means classes would automatically inherit fields from their parent classes, but if they chose to redefine the class, they would be required to explicitly add the annotation.
  • Put together:
    • A class can implement an interface by inheriting some of the interface’s fields from a parent class. So, on a class, not all exposed fields will always be explicitly present. But, any locally defined fields will explicitly communicate if they are exposed via their annotation (or lack thereof).
    • We can support defining an interface using an abstract class.

Nice properties:

  • Any locally defined fields will explicitly communicate if they are exposed via the presence or absence of an annotation
  • Interfaces remain entirely contractual. They do not change the behavior of the types which implement them.
  • We only need to support single in inheritance now

Challenges

  • If a field redefined on the child class but not annotated, that should be an error. Does this mean we need to collect unannotated fields? (at least their names and locations?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant