Skip to content

Commit

Permalink
[Typescript] enum as const: add support const assertions from T… (#3569)
Browse files Browse the repository at this point in the history
* Typescript plugin: enumAsConst feature

* add type gen
  • Loading branch information
DragorWW committed Mar 8, 2020
1 parent 393ec96 commit 700f32f
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 5 deletions.
17 changes: 16 additions & 1 deletion docs/generated-config/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ path/to/file.ts:
enumsAsTypes: true
```

### enumsAsConst (`boolean`, default value: `false`)

Generates enum as TypeScript `const assertions` instead of `enum`. This can even be used to enable enum-like patterns in plain JavaScript code if you choose not to use TypeScript’s enum construct.

#### Usage Example

```yml
generates:
path/to/file.ts:
plugins:
- typescript
config:
enumsAsConst: true
```

### immutableTypes (`boolean`, default value: `false`)

Generates immutable types by adding `readonly` to properties and uses `ReadonlyArray`.
Expand Down Expand Up @@ -106,4 +121,4 @@ path/to/file.ts:
- typescript
config:
noExport: true
```
```
17 changes: 17 additions & 0 deletions packages/plugins/typescript/typescript/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@ export interface TypeScriptPluginConfig extends RawTypesConfig {
* ```
*/
enumsAsTypes?: boolean;
/**
* @name enumsAsConst
* @type boolean
* @description Generates enum as TypeScript `const assertions` instead of `enum`. This can even be used to enable enum-like patterns in plain JavaScript code if you choose not to use TypeScript’s enum construct.
* @default false
*
* @example
* ```yml
* generates:
* path/to/file.ts:
* plugins:
* - typescript
* config:
* enumsAsConst: true
* ```
*/
enumsAsConst?: boolean;
/**
* @name fieldWrapperValue
* @type string
Expand Down
41 changes: 37 additions & 4 deletions packages/plugins/typescript/typescript/src/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface TypeScriptPluginParsedConfig extends ParsedTypesConfig {
avoidOptionals: AvoidOptionalsConfig;
constEnums: boolean;
enumsAsTypes: boolean;
enumsAsConst: boolean;
fieldWrapperValue: string;
immutableTypes: boolean;
maybeValue: string;
Expand All @@ -26,6 +27,7 @@ export class TsVisitor<TRawConfig extends TypeScriptPluginConfig = TypeScriptPlu
fieldWrapperValue: getConfigValue(pluginConfig.fieldWrapperValue, 'T'),
constEnums: getConfigValue(pluginConfig.constEnums, false),
enumsAsTypes: getConfigValue(pluginConfig.enumsAsTypes, false),
enumsAsConst: getConfigValue(pluginConfig.enumsAsConst, false),
immutableTypes: getConfigValue(pluginConfig.immutableTypes, false),
wrapFieldDefinitions: getConfigValue(pluginConfig.wrapFieldDefinitions, false),
...(additionalConfig || {}),
Expand Down Expand Up @@ -133,14 +135,45 @@ export class TsVisitor<TRawConfig extends TypeScriptPluginConfig = TypeScriptPlu
})
.join(' |\n')
).string;
} else {
return new DeclarationBlock(this._declarationBlockConfig)
}

if (this.config.enumsAsConst) {
const typeName = `export type ${enumTypeName} = typeof ${enumTypeName}[keyof typeof ${enumTypeName}];`;
const enumAsConst = new DeclarationBlock({
...this._declarationBlockConfig,
blockTransformer: block => {
return block + ' as const';
},
})
.export()
.asKind(this.config.constEnums ? 'const enum' : 'enum')
.asKind('const')
.withName(enumTypeName)
.withComment((node.description as any) as string)
.withBlock(this.buildEnumValuesBlock(enumName, node.values)).string;
.withBlock(
node.values
.map(enumOption => {
const optionName = this.convertName(enumOption, { useTypesPrefix: false, transformUnderscore: true });
const comment = transformComment((enumOption.description as any) as string, 1);
let enumValue: string | number = enumOption.name as any;

if (this.config.enumValues[enumName] && this.config.enumValues[enumName].mappedValues && typeof this.config.enumValues[enumName].mappedValues[enumValue] !== 'undefined') {
enumValue = this.config.enumValues[enumName].mappedValues[enumValue];
}

return comment + indent(`${optionName}: ${wrapWithSingleQuotes(enumValue)}`);
})
.join(',\n')
).string;

return [enumAsConst, typeName].join('\n');
}

return new DeclarationBlock(this._declarationBlockConfig)
.export()
.asKind(this.config.constEnums ? 'const enum' : 'enum')
.withName(enumTypeName)
.withComment((node.description as any) as string)
.withBlock(this.buildEnumValuesBlock(enumName, node.values)).string;
}

protected getPunctuation(declarationKind: DeclarationKind): string {
Expand Down
21 changes: 21 additions & 0 deletions packages/plugins/typescript/typescript/tests/typescript.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,27 @@ describe('TypeScript', () => {
}`);
});

it('Should work with enum as const', async () => {
const schema = buildSchema(/* GraphQL */ `
enum MyEnum {
A_B_C
X_Y_Z
_TEST
My_Value
}
`);
const result = (await plugin(schema, [], { enumsAsConst: true }, { outputFile: '' })) as Types.ComplexPluginOutput;

expect(result.content).toBeSimilarStringTo(`
export const MyEnum = {
ABC: 'A_B_C',
XYZ: 'X_Y_Z',
Test: '_TEST',
MyValue: 'My_Value'
} as const;
export type MyEnum = typeof MyEnum[keyof typeof MyEnum];`);
});

it('Should work with enum and enum values (enumsAsTypes)', async () => {
const schema = buildSchema(/* GraphQL */ `
"custom enum"
Expand Down

0 comments on commit 700f32f

Please sign in to comment.