diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts index 9858284acb..ba419f178e 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts @@ -13,6 +13,10 @@ export class FlowRenderer extends TypeScriptFlowBaseRenderer { return this._tsFlowOptions.preferUnknown ? "mixed" : "any"; } + protected get undefinedType(): string { + return "void"; + } + protected forbiddenNamesForGlobalNamespace(): string[] { return [ "Class", diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts index 179e118c20..c215de4483 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts @@ -96,6 +96,13 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { return singleWord([parenIfNeeded(itemType), "[]"]); } + /** Whether a union is spelled out as `a | b` instead of by name. */ + private inlinesUnion(u: UnionType): boolean { + return ( + !this._tsFlowOptions.declareUnions || nullableFromUnion(u) !== null + ); + } + protected sourceFor(t: Type): MultiWord { const emptyObjectType = this.emptyObjectTypeFor(t); if (emptyObjectType !== undefined) { @@ -134,10 +141,7 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { ]), (_enumType) => panic("We handled this above"), (unionType) => { - if ( - !this._tsFlowOptions.declareUnions || - nullableFromUnion(unionType) !== null - ) { + if (this.inlinesUnion(unionType)) { const children = Array.from(unionType.getChildren()).map( (c) => parenIfNeeded(this.sourceFor(c)), ); @@ -162,6 +166,11 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { * `prefer-unknown` option, plain `any` without it. */ protected abstract anyType(): string; + /** The type spelling for a missing optional property. */ + protected get undefinedType(): string { + return "undefined"; + } + protected abstract emitEnum(e: EnumType, enumName: Name): void; protected abstract emitClassBlock(c: ClassType, className: Name): void; @@ -188,10 +197,35 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { const additionalProperties = c.getAdditionalProperties(); if (additionalProperties) { - const indexTypes = [ - additionalProperties, - ...Array.from(c.getProperties().values(), (p) => p.type), - ].map((t) => this.sourceFor(t).source); + const properties = Array.from(c.getProperties().values()); + let indexTypes: Sourcelike[]; + if (additionalProperties.kind === "any") { + indexTypes = [this.anyType()]; + } else { + // Inlined unions render as their members, so collect + // those members to avoid repeating them. + const types = new Set(); + const addType = (t: Type): void => { + if (t instanceof UnionType && this.inlinesUnion(t)) { + for (const child of t.getChildren()) { + addType(child); + } + } else { + types.add(t); + } + }; + + addType(additionalProperties); + for (const p of properties) { + addType(p.type); + } + + indexTypes = Array.from(types, (t) => this.sourceFor(t).source); + if (properties.some((p) => p.isOptional)) { + indexTypes.push(this.undefinedType); + } + } + this.emitTable([ [ "[property: string]", diff --git a/test/fixtures/typescript/tsconfig.json b/test/fixtures/typescript/tsconfig.json index 6bcd3937d1..d5777e9fbb 100644 --- a/test/fixtures/typescript/tsconfig.json +++ b/test/fixtures/typescript/tsconfig.json @@ -3,7 +3,7 @@ "target": "es6", "noEmit": true, "noImplicitAny": true, - "strictNullChecks": false + "strictNullChecks": true }, "files": ["main.ts"] }