-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Change type of extensions from anonymous Record to named interfaces #2465
Conversation
@benjie Thanks for PR 👍 |
@benjie Can you please describe your use case in more detail so I can think about alternative solutions? |
That's what declaration merging solves; everyone's type completion would be relevant to their project solely based on the dependencies they pull in; e.g. In MyType.ts: import { GraphQLObjectType } from "graphql";
declare module "graphql" {
interface GraphQLObjectTypeExtensions {
/*
* This attribute declaration gets merged in with all the other
* attribute declarations from every other module _that you have
* imported_ that declares this interface.
*/
myNumber?: number;
}
}
export default new GraphQLObjectType({
name: "MyType",
fields: {...},
extensions: {
myNumber: 42,
},
}); In SomeOtherFile.ts: import { GraphQLSchema } from "graphql";
import MyType from "./MyType";
import DifferentType from "some-other-module";
console.log(MyType.extensions?.myNumber); // 42
console.log(DifferentType.extensions?.myNumber); // undefined ^ The above would not throw typing errors, and would know what The main issue this would open is that once an attribute is defined on extensions, all attributes with that name on that type of extension (e.g. GraphQLObjectTypeExtensions) would have to have the same type. This is probably a good thing; I'd suggest using namespacing to help avoid conflicts from different modules. If you want to introspect over the properties of
I'd need to understand more what you have in mind here to comment; I assume it relates to graphql/graphql-spec#300 ? I don't currently see any issues here that using |
@benjie I mean that when you generate graphql-js/src/type/definition.js Line 574 in 3f859d4
|
You can still add the loss-of-type-safety necessary for that with declaration merging; e.g.: interface Extensions {
}
export class GraphQLObjectType {
extensions: Extensions
constructor() {
this.extensions = {}
}
}
interface Extensions {
myVar?: string
}
interface Extensions {
[key: string]: any;
}
const o = new GraphQLObjectType();
o.extensions?.myVar;
o.extensions?.foo;
The main motivation here is to avoid loss of type safety - currently as soon as you read an attribute from |
@IvanGoncharov Would you be open to me changing these to: export interface GraphQLObjectTypeExtensions {
[key: string]: unknown;
} This way you still get basically the same as you have currently (except a named interface rather than anonymous Record), but TypeScript can tab-complete and understand the declaration-merged declared attributes if people opt-into that. |
The simplest use case would be query complexity: When we put some metadata to type's So if you install const Post = new GraphQLObjectType({
name: 'Post',
fields: () => ({
title: { type: GraphQLString },
text: {
type: GraphQLString,
extensions: {
complexity: 5
},
},
}),
}); Or provide intellisense (type-inference) for the callback calculator parameters: const Query = new GraphQLObjectType({
name: 'Query',
fields: () => ({
posts: {
type: new GraphQLList(Post),
args: {
count: {
type: GraphQLInt,
defaultValue: 10
}
},
extensions: {
complexity: ({args, childComplexity}) => childComplexity * args.count,
},
},
}),
}); |
I've added this change. I think this is now a fairly non-controversion change:
|
To make this even more uncontroversial I've converted the |
@benjie You also need to apply the same changes to That said I still think extending types globally is not a good idea but I don't want to block this PR since I don't object changes themself I just object how they would be used. |
@benjie Just to capture note from our chat:
|
I've extended to GraphQLSchema and GraphQLDirective; exposed through /**
* It's strongly recommended that you prefix each added attribute's name with an
* identifier unique to your project to avoid conflicts with other projects.
*/ This allows for using a namespaced wrapper object as you suggest, or just having a mess of attributes that shouldn't conflict (which may be more convenient for other projects). There's a small issue with the Flow typings: previously you were using |
Would you folks be open to making the Extensions types generic over the same things the containing type is generic over so that the properties of our strongly typed extensions can be generic over those as well? If you're attaching a function as an extension to a Prior to extensions being a thing at all, one library I've worked with was using TypeScript declaration merging to extend the definition |
… while we wait for GraphQL to figure out how to strongly type extensions See graphql/graphql-js#2465
@airhorns Sounds reasonable the only downside I see is that unused template arguments will look strange so it we should extend their description saying they are open types and it's possible to extend them so we provide template arguments for this reason. @benjie Can you please add simple test extending at least a couple of them in our TS integration tests: |
@benjie My initial concern was that some developers will generate extensions from some 3rd-party sources or even user-provided data in case platforms like CMS. Since the end goal is to allow exposing Thinking about it just remove those types from Flow since we migrating TS in next release anyway. |
@benjie Can you please explain user cases that you have in mind where it's more convenient? Because I actually prefer to standardize on one approach to not confuse library authors and their users. |
@benjie Looking at @airhorns example it sims that join monster is currently using |
@IvanGoncharov would you like to provide alternative wording for the comment? |
someArgumentExtension?: SomeExtension; | ||
} | ||
} | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how best to selectively enable this for TypeScript 3.2 and above only.
Further, the types themselves are not concretely tested, pointing to an example in the GraphQL source where this takes place would be helpful.
@IvanGoncharov This is ready for re-review. All comments were addressed, however I was not able to figure out what testing could be added for the types that supports back to TypeScript 2.6. If you uncomment the block I commented, it works on TS>=3.2, but fails on 3.1:
Since this is user code, this should not be a concern (we just shouldn't add this block of code in TypeScript 3.1 or below). I considered changing |
integrationTests/ts/index.ts
Outdated
@@ -1,30 +0,0 @@ | |||
import { GraphQLString, GraphQLSchema, GraphQLObjectType } from 'graphql/type'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This entire file was renamed to integrationTests.ts.template
}; | ||
|
||
// The following code block requires a newer version of TypeScript | ||
/*! >=3.2 !*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This "magic comment" syntax is documented in test.js
integrationTests/ts/test.js
Outdated
for (const template of templates) { | ||
template.writeFileSync(version); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@IvanGoncharov I seek your guidance on this approach to allowing a block of code to be omitted when testing older TypeScript versions. It works; but it does mean the tests write to the filesystem, which makes me uncomfortable. It's a fairly simple implementation though, basically powered by a single regexp and the semver
module.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@benjie Thanks for trying this solution.
How about just have index-3.2.ts
and index.ts
?
Cycle starts with index.ts
since we start from newest versions first but if
fs.fileExistSync(`index-${version.replace('typescript-', '')}.ts`)
exist we will switch to this file for this and latter versions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was my first thought; but I realised that keeping these files in sync would be challenging, and it increases the amount of code in the tests without offering much in the way of value. It also doesn't allow for more complex ranges like >=3.2 <5
which may be required in future. "Turning off" a block of code seemed like the simplest solution from the point of view of writing the tests, at a slight cost of the test infrastructure being more complex.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose the more complex ranges are enabled by having a 4.9
file and another 3.1
file; but this still seems more complex/cumbersome than just removing a block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@benjie couple of points:
- don't like having
glob
andsemver
as dependencies it means we need to update them constantly to prevent GitHub marking this repo as affected by yet another security vulnerability. - Version after next will drop all TS up to 3.7 (to support recursive types) so this is the temporary workaround to keep maintaining
15.x.x
for some time and possibly backport critical fixes. - I don't expect this file to be changed frequently since we will migrate our codebase to TS we will need it just as a sanity check and for very rare occasions where we can't test stuff in our codebase like this one.
So I will likely remove this mechanism in 16.0.0
anyway so no need to make it too complex or future proof just make it to require as low maintenance as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@benjie Let's not block this PR and keep it KISS just add test as a comment with FIXME
and I will uncomment it in 16.0.0
.
How does it sounds?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- You already have a lot of dependencies on both of these modules according to
package-lock.json
, but I totally get where you're coming from - Great 👍
- Okay.
I've reverted the commit that adds this testing support.
… versions" This reverts commit 04c837a.
Bless both of your hearts for making this happen and sweating the details! Thanks!! |
After graphql/graphql-js#2465 , we can now use TypeScript declaration merging to augment the graphql-types nice and cleanly. Woop woop!
After graphql/graphql-js#2465 , we can now use TypeScript declaration merging to augment the graphql-types nice and cleanly. Woop woop!
After graphql/graphql-js#2465 , we can now use TypeScript declaration merging to augment the graphql-types nice and cleanly. Woop woop!
* Switch to using GraphQL 15's extensions for join-monster config in a schema GraphQL 15 doesn't let schema authors attach arbitrary properties to schema objects anymore, so join-monster's config style has to change. There's an `extensions` property that works great for this, let's use that! * Update docs to reflect new extensions configuration setup * Bump join-monster version to indicate breaking change * Update TypeScript types to export strongly typed extension interfaces After graphql/graphql-js#2465 , we can now use TypeScript declaration merging to augment the graphql-types nice and cleanly. Woop woop! * Add a changelog entry explaining how to migrate to the new extensions format * Fix a couple broken TypeScript types for thunking and add TypeScript tests
Following on from #2104 (comment) I thought it best to demonstrate with a PR.
I'm concerned about losing type safety of the
extensions
field due to theany
's here. I consulted @orta on the best way to type this in TypeScript, and he confirmed that using an empty interface and declaration merging is the best practice here. He provided the following example:https://www.typescriptlang.org/play/?ssl=12&ssc=1&pln=16&pc=1#code/JYOwLgpgTgZghgYwgAgKIA9IgM7APY7IDeAsAFDkC+55E6ADnlGMggDZzbbIDiUc9ABYBFADIB5AEYArCAjAAVAJ70UpMsmR0suAtgBcaTBBz4cNDaz1goAV3lMAFAEpiycps1hBwbADptE11CAF5iaksIiPJQSFhEFAwdM251TQBbJQA1OCgAfkNsG1AAcypyIA
He noted that the fields added to the interface by merged declarations should contain
?
to avoid issues at the internal callsite, but I don't think that's an issue for us since all extension attributes would be optional; accessed via something like:myGraphQLObjectType?.extensions?.myAttribute
.This would be a breaking change to the types (any
->unknown
), so I think it's important to include it before GraphQL.js v15 is released.