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

Enums break .d.ts typings #179

Closed
felixfbecker opened this issue Apr 18, 2018 · 21 comments
Closed

Enums break .d.ts typings #179

felixfbecker opened this issue Apr 18, 2018 · 21 comments

Comments

@felixfbecker
Copy link

Unfortunately the use of real enums means the typings cannot be generated anymore referencing the enums will fail at runtime with Uncaught ReferenceError: GQL is not defined. I think using const enums might work, but I think the better solution here is to generate a proper .ts module. That however gets hairy to combine with external modules, as the current file doesn't export anything and it can't be imported.

I would propose to add a new flag --module that makes gql2ts output an external module (exporting the types) instead of a declare namespace.

@brettjurgens
Copy link
Contributor

I've run into this as well. I've considered going back to the unioned strings, but I'm still unsure.

i've also thought about removing the namespace option altogether.

in the meantime, the solution i've used is to provide overrides for the formatters.

const {
  DEFAULT_INTERFACE_BUILDER,
  DEFAULT_TYPE_BUILDER,
  DEFAULT_ENUM_TYPE_BUILDER,
  DEFAULT_EXPORT_FUNCTION
} = require('@gql2ts/language-typescript');

const addConst = input => `const ${input}`;

const overrides = {
  generateNamespace: (_, interfaces) => `// tslint:disable
  // graphql typescript definitions

  ${interfaces}

  // tslint:enable
  `,
  interfaceBuilder: (name, body) => DEFAULT_EXPORT_FUNCTION(DEFAULT_INTERFACE_BUILDER(name, body)),
  typeBuilder: (name, body) => DEFAULT_EXPORT_FUNCTION(DEFAULT_TYPE_BUILDER(name, body)),
  enumTypeBuilder: (name, values) => DEFAULT_EXPORT_FUNCTION(addConst(DEFAULT_ENUM_TYPE_BUILDER(name, values))),
};

this is obviously not great, but will hopefully help in the meantime. I think const enums are the way to go (although I've run into another issue where two enums that have the same definition are not equal 😞)

this is something that should be looked into in v2 #163

@brettjurgens brettjurgens mentioned this issue Apr 18, 2018
17 tasks
@harm-less
Copy link

I can confirm the const enum works like a charm:

import {extend} from 'lodash';
import {generateNamespace, ISchemaToInterfaceOptions} from '@gql2ts/from-schema';
import * as fs from 'fs';
import {ITypeMap, IFromQueryOptions} from '@gql2ts/types';
import {DEFAULT_TYPE_MAP, DEFAULT_EXPORT_FUNCTION} from '@gql2ts/language-typescript';

fs.readFile('./src/api/graphql/schema.graphql', {encoding: 'utf8'}, (err, schema) => {
	if (err) {
		throw err;
	}

	const options: Partial<ISchemaToInterfaceOptions> = {
		typeMap: extend(DEFAULT_TYPE_MAP, {
			Date: 'Date',
			UUID: 'string'
		}) as ITypeMap
	};

	// @todo: This override is required for a erroneous enum:
	// https://github.com/avantcredit/gql2ts/issues/179
	const overrides: Partial<IFromQueryOptions> = {
		enumTypeBuilder: (name, values) =>
			DEFAULT_EXPORT_FUNCTION(`const enum ${name} ${values}`).replace(';', '')
	};
	const typescriptDefinitions = generateNamespace('GQL', schema, options, overrides);

	fs.writeFile('./src/api/graphql/schema.d.ts', typescriptDefinitions, () => {});
});

Note that I'm also replacing the ; character, but I think that was also mentioned in a different issue.

@brettjurgens
Copy link
Contributor

@harm-less what's the issue with semicolons? is that just a preference or is something breaking?

@raysuelzer
Copy link
Contributor

raysuelzer commented Jul 9, 2018

@brettjurgens it is not valid typescript. It creates an error.

Edit: : An implementation cannot be declared in ambient contexts.

@raysuelzer
Copy link
Contributor

@brettjurgens if you want i can put in a PR for this, but maybe not until next week.

@brettjurgens
Copy link
Contributor

@raysuelzer that'd be great!

I'm still unsure if we should even be doing native enums, because they seem to be causing a few problems. We've reverted to using string unions instead

@felixfbecker
Copy link
Author

I think native enums are better for IntelliSense (discovering the members).
Note that const enums have some problems though, they require type info to compile to transpileOnly or the beta TS Babel transform will not work if you use them.

@brettjurgens
Copy link
Contributor

@felixfbecker intellisense works well with string unions as well. Const enums are great, but like you said don't work in certain situations.

I think we can keep enums in the from-schema package and use string unions in the from-query. The issue I've been encountering is that two equivalent enums are not equal as far as the type system is concerned. This is mostly an issue with input values (i.e. args or mutations)

Typing this on my phone, so hopefully it's coherent 😂

@felixfbecker
Copy link
Author

intellisense works well with string unions as well

Not when the type for the position you are assigning it to cannot be inferred, e.g. cause you are assigning to a variable/property without a type declaration, to a function that takes any (e.g. fetch() with JSON.stringify()), or when the surrounding object is a union type.

The issue I've been encountering is that two equivalent enums are not equal as far as the type system is concerned

That's a problem indeed, and surprises me...

@brettjurgens
Copy link
Contributor

brettjurgens commented Jul 11, 2018

@felixfbecker

Not when the type for the position you are assigning it to cannot be inferred, e.g. cause you are assigning to a variable/property without a type declaration, to a function that takes any (e.g. fetch() with JSON.stringify()), or when the surrounding object is a union type.

Ah, you're absolutely correct - good point.

I think there's some middle ground that we can hit here, but I'm not entirely sure. I've also thought about using string unions for the values of the fields, but also generating the enum.
e.g.

interface IProduct {
  productType: 'typeA' | 'typeB' | 'typeC';
}

const enum ProductType {
  typeA = 'typeA',
  typeB = 'typeB',
  typeC = 'typeC'

but I can't decide if I like it or not

@felixfbecker
Copy link
Author

Why not have it configurable?

@brettjurgens
Copy link
Contributor

@felixfbecker it should be, just trying to figure out what the most sane default behavior is. I don't want people generating code that doesn't work in their usecase by default

@raysuelzer
Copy link
Contributor

I already convertee everything to enums, but did find string literals to be less of an issue.

@raysuelzer
Copy link
Contributor

#202 @brettjurgens

@brettjurgens
Copy link
Contributor

this should be fixed in 1.8.1, if not lets reopen

@voxmatt
Copy link

voxmatt commented Jul 24, 2018

Are you guys generating the typings into a non-.d.ts file? Maybe I'm missing something, but any declarations file will get compiled away, even if they are const enums. I feel like I'm misunderstanding @felixfbecker's issue and @raysuelzer's solution.

@felixfbecker
Copy link
Author

Yes, I am compiling to a .ts file

@raysuelzer
Copy link
Contributor

@voxmatt , the issue I had even happened when using .d.ts file. It did not happen until after I compiled my angular application in production mode.... Might be an issue with their tree shaking, but it definitely caused errors both ways on my end.

@voxmatt
Copy link

voxmatt commented Jul 24, 2018

@felixfbecker did you find a solution for namespace vs. module?

@felixfbecker
Copy link
Author

felixfbecker commented Jul 24, 2018

I am using gql2ts programmatically in a Gulpfile to be able to customize it. This is the task:

export async function graphQLTypes(): Promise<void> {
    const schemaStr = await readFile(GRAPHQL_SCHEMA_PATH, 'utf8')
    const schema = buildSchema(schemaStr)
    const result = (await graphql(schema, introspectionQuery)) as { data: IntrospectionQuery }

    const formatOptions = (await resolveConfig(__dirname, { config: __dirname + '/../prettier.config.js' }))!
    const typings =
        'export type ID = string\n\n' +
        generateNamespace(
            '',
            result,
            {
                typeMap: {
                    ...DEFAULT_TYPE_MAP,
                    ID: 'ID',
                },
            },
            {
                generateNamespace: (name: string, interfaces: string) => interfaces,
                interfaceBuilder: (name: string, body: string) =>
                    'export ' + DEFAULT_OPTIONS.interfaceBuilder(name, body),
                enumTypeBuilder: (name: string, values: string) =>
                    'export ' + DEFAULT_OPTIONS.enumTypeBuilder(name, values),
                typeBuilder: (name: string, body: string) => 'export ' + DEFAULT_OPTIONS.typeBuilder(name, body),
                wrapList: (type: string) => `${type}[]`,
                postProcessor: (code: string) => format(code, { ...formatOptions, parser: 'typescript' }),
            }
        )
    await writeFile(__dirname + '/src/backend/graphqlschema.ts', typings)
}

Works really well! Thanks for all the options @brettjurgens

@jLouzado
Copy link

I think we can keep enums in the from-schema package and use string unions in the from-query.

@brettjurgens I think this would be really useful; enums have a lot of known issues which are unlikely to change anytime soon: microsoft/TypeScript#33283

For example, two different queries might both have a key that's exactly the same shape. All the other keys will be inferred (correctly) as being equivalent, except for the enums.

If we could generate string unions instead of enums (or atleast make that a configurable option) that would be amazing.

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

No branches or pull requests

6 participants