-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Generated typescript recursive query types are wrong #2631
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
Comments
@freshollie Maybe I'm missing something, not sure. In your use case, you are using:
Which means that With your schema, it seems to work for me in TS Playground: And if I'm changing it to force a value there and make the field as non-nullable, it allow me to access it directly: You shouldn't use If you wish to have a dedicated type for |
Hey, thanks for the reply. Firstly, the schema is correct for my requirements. I am using Secondly, in my playground I am using the generated schema types directly, only to imitate the datatypes which would have come out of import { GetCategoriesQuery } from "./types";
// This is the Category object I want to use with the data
// Notice that it doesn't accept null as a children value
interface Category {
id: string;
name: string;
children?: Category[];
}
const printChildren = (children?: Category[]) => {
if (children !== undefined) {
console.log(children.length);
}
};
// This function accepts only the Category type, so it
// shouldn't allow objects with nullable `children` to be provided
const handleCategories = (categories: Category[]) => {
categories.forEach(category => printChildren(category.children));
};
const result: GetCategoriesQuery = {
categories: [
{
id: "id",
name: "name",
children: null
}
]
};
// No error when nullable children are provided
handleCategories(result.categories); |
Another example which shows the issue with using intersection types, which I think highlights both the issue with Typescript and my issue with this library. type Category = {
id: string;
children?: Category[];
};
type Maybe<T> = T | null;
type FetchedCategory = { id: string } & {
children: Maybe<Array<FetchedCategory>>;
};
const a: FetchedCategory[] = [
{
id: "a",
children: null,
},
];
// No error, because what?
const b: Category[] = a;
// Error, children cannot be null
const c: Category[] = [
{
id: "a",
children: null,
},
]; |
I think a code sandbox reproduction would be helpful. Please also include your typescript config. |
My original comment:
|
@freshollie Sorry! type Category = {
id: string;
- children?: Category[];
+ children: Category[] | null;
}; I am having a hard time understanding your issue. Does this solve your issue? https://codesandbox.io/s/graphql-codegen-issue-template-ldmhy?fontsize=14&module=%2Ftest.ts GraphQL is AFAI not meant for recursion (this might be related: graphql/graphql-spec#91) |
@n1ru4l notice the first assignment Yes, I followed the examples in graphql/graphql-spec#91 to generate my query. Your changes to the test.ts file do not solve the issue. Run |
I will point out what I am trying to show with the example source with typescript intersections: const a: FetchedCategory[] = [
{
id: "a",
children: null,
},
];
// No error, because what? (THIS IS WRONG THERE SHOULD BE AN ERROR HERE)
const b: Category[] = a;
// Error, children cannot be null (THIS IS CORRECT, NULL IS NOT ASSIGNABLE TO CHILDREN)
const c: Category[] = [
{
id: "a",
children: null,
},
]; |
|
Oops, didn't see your logic change. const printChildren = <T extends {}>(children: T[]) => {
if (children) {
console.log(children.length);
}
}; The original logic didn't take into account nulls, because my const printChildren = (children?: Category[]) => {
if (children !== undefined) {
console.log(children.length);
}
}; Yes, you are correct this solves the issue with the code, but is not the point in typescript and codegen to highlight issues with code before they show at runtime? Yes, I could fix my code by changing The point is that I had to find this by mistake by accident, my types should have picked this up when assigning the output of the graphql query to my funciton which uses |
What is the point of redeclaring types you already generated? What exactly could graphql-codegen change to solve your problem? |
Ignore the crudeness of the example I provide. The idea is that obviously I might want to use the GraphQL query data in a function which has an interface expecting a type. For example a React Component. In this case, I should transform the data from the query response to remove the null types as the interface doesn't accept them. However, this was not highlighted. The point in the graphql types is it is supposed to highlight these issues and require the developers to make the correct data transformations in order to conform to the interface. In the examples I have given this is not happening, due to the way codegen generates the types. |
The idea of GraphQL is to generate a schema that is consumed by the client without the need for transformation. Ideally, your Schema should represent the UI structure of your application. Of course, I can understand that this is not always possible. People use fragments for declaring data dependencies for components. E.g. Relay is 100% based on using fragments and apollo v3 will also shift towards the tendency of encouraging the usage of fragments.
Sorry for repeating my question: What exactly could graphql-codegen change to solve your problem? |
The type generated for the query is: export type GetCategoriesQuery = (
{ __typename?: 'Query' }
& { categories: Array<(
{ __typename?: 'Category' }
& Pick<Category, 'id' | 'name'>
& { children: Maybe<Array<(
{ __typename?: 'Category' }
& Pick<Category, 'id' | 'name'>
& { children: Maybe<Array<(
{ __typename?: 'Category' }
& Pick<Category, 'id' | 'name'>
)>> }
)>> }
)> }
); Which should be equal to export type FixedGetCategoriesQuery = {
__typename?: "Query";
categories: Array<{
id: string;
name: string;
__typename?: "Category";
children?: Maybe<
Array<{
id: string;
name: string;
__typename?: "Category";
children?: Maybe<
Array<{
id: string;
name: string;
__typename?: "Category";
}>
>;
}>
>;
}>;
}; When using this fixed query, typescript correctly highlights the type issues: import { FixedGetCategoriesQuery } from "./types";
interface Category {
id: string;
name: string;
children?: Category[];
}
const printChildren = (children?: Category[]) => {
if (children !== undefined) {
console.log(children.length);
}
};
const handleCategories = (categories: Category[]) => {
categories.forEach(category => printChildren(category.children));
};
const result: FixedGetCategoriesQuery = {
categories: [
{
id: "id",
name: "name",
children: null
}
]
};
handleCategories(result.categories); Argument of type '{ id: string; name: string; __typename?: "Category" | undefined; children?: { id: string; name: string; __typename?: "Category" | undefined; children?: { id: string; name: string; __typename?: "Category" | undefined; children?: { ...; }[] | ... 1 more ... | undefined; }[] | null | undefined; }[] | null | undefined; ...' is not assignable to parameter of type 'Category[]'.
Type '{ id: string; name: string; __typename?: "Category" | undefined; children?: { id: string; name: string; __typename?: "Category" | undefined; children?: { id: string; name: string; __typename?: "Category" | undefined; children?: { ...; }[] | ... 1 more ... | undefined; }[] | null | undefined; }[] | null | undefined; }' is not assignable to type 'Category'.
Types of property 'children' are incompatible.
Type '{ id: string; name: string; __typename?: "Category" | undefined; children?: { id: string; name: string; __typename?: "Category" | undefined; children?: { id: string; name: string; __typename?: "Category" | undefined; }[] | null | undefined; }[] | null | undefined; }[] | null | undefined' is not assignable to type 'Category[] | undefined'.
Type 'null' is not assignable to type 'Category[] | undefined'.ts(2345) |
Does this makes sense? I understand Pick is useful for being able to use the base type, but using intersection in typescript does not behave as expected. Codegen should generate query types without intersections in order to have correct types. |
@freshollie Does this option solve your issues? https://graphql-code-generator.com/docs/plugins/typescript-operations#preresolvetypes-boolean-default-value-false |
@n1ru4l yes! This creates the correct types for the query! Do you see the issue though? By default this query is not type checked correctly, so why even generate it? |
@freshollie Shouldn't this be fixed once typescript 3.7 is out? (microsoft/TypeScript#33243) |
I tested with |
@freshollie I think it will take a while until it is done... (https://github.com/microsoft/TypeScript/milestone/98) @dotansimha How do you feel about this? |
Yeah, advice I got was it's not 100% likely this issue gets fixed with 3.7 as the goals of the milestone change. Why is the generation with Pick the default if it doesn't work as intended? |
Because you are the first person that experienced this issue and you reported it today 😉 |
Oh, I didn't mean to be so abrupt. I meant to mean, why does the option exist :P |
@dotansimha @n1ru4l any conclusions on this? To summaries: Other people may encounter this and |
I still think this is an issue with typescript that should be documented. |
@freshollie Thanks, closing for now. The current default is using |
Describe the bug
Because codegen relies heavily on Intersection types, typescript does not correctly check query fields which are optional, and allows nulls to go unchecked in code.
An example query:
To Reproduce
Use the code sandbox:
https://codesandbox.io/s/graphql-codegen-issue-template-563vy
yarn generate
will generate the query types for the nested query onCategory
test.ts
shows how these types provide no protection for null fields in typescript.We generate some query response data which is a valid structure for the
GetCategoriesQuery
and then use that data in a function which uses theCategory
type as if it's children are not null (should throw an error)yarn build
will show how no type errors are generated from the types.yarn test
will show how a runtime error exists when the .length property is queried on thechildren
field.Expected result
GetCategoriesQuery
should not be compatible withCategory
aschildren
is not expected to be anull
type inCategory
.Environment:
@graphql-codegen/[email protected]
:Additional context
Typescript has issues with intersections being tracked microsoft/TypeScript#33243. Using them for generating these types seems to be the issue here, as manually generating the type without intersections correctly picks up the errors.
The text was updated successfully, but these errors were encountered: