Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import {
type Type,
UnionType,
} from "../../Type/index.js";
import { matchType, nullableFromUnion } from "../../Type/TypeUtils.js";
import {
directlyReachableSingleNamedType,
matchType,
nullableFromUnion,
} from "../../Type/TypeUtils.js";
import {
JavaScriptRenderer,
type JavaScriptTypeAnnotations,
Expand Down Expand Up @@ -237,15 +241,21 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer {
}

protected emitTypes(): void {
// emit primitive top levels
// Emit top levels whose name isn't used by forEachNamedType below.
this.forEachTopLevel("none", (t, name) => {
if (!t.isPrimitive()) {
if (directlyReachableSingleNamedType(t) !== undefined) {
return;
}

this.ensureBlankLine();
this.emitDescription(this.descriptionForType(t));
this.emitLine("type ", name, " = ", this.sourceFor(t).source, ";");
this.emitLine(
"export type ",
name,
" = ",
this.sourceFor(t).source,
";",
);
});

this.forEachNamedType(
Expand Down
1 change: 1 addition & 0 deletions test/inputs/schema/empty-object.1.fail.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[1, 2, 3]
1 change: 1 addition & 0 deletions test/inputs/schema/empty-object.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
5 changes: 5 additions & 0 deletions test/inputs/schema/empty-object.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-06/schema#",
"type": "object",
"properties": {}
}
22 changes: 20 additions & 2 deletions test/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1199,7 +1199,11 @@ export const JavaScriptPropTypesLanguage: Language = {
"spotify-album.json", // renderer does not support recursion
"76ae1.json", // renderer does not support recursion
],
skipSchema: ["integer-before-number.schema"], // Python-specific union-order regression.
skipSchema: [
// The renderer does not support a bare top-level map.
"empty-object.schema",
"integer-before-number.schema", // Python-specific union-order regression.
],
skipMiscJSON: false,
rendererOptions: { "module-system": "es6" },
quickTestRendererOptions: [{ converters: "top-level" }],
Expand Down Expand Up @@ -1372,7 +1376,11 @@ I havea no idea how to encode these tests correctly.
"php-mixed-union.json",
"nst-test-suite.json",
],
skipSchema: ["integer-before-number.schema"], // Python-specific union-order regression.
skipSchema: [
// The renderer does not support a bare top-level map.
"empty-object.schema",
"integer-before-number.schema", // Python-specific union-order regression.
],
skipMiscJSON: false,
rendererOptions: { "just-types": "true" },
quickTestRendererOptions: [],
Expand Down Expand Up @@ -1907,6 +1915,7 @@ export const HaskellLanguage: Language = {
// (A top-level `[Int]` correctly fails to decode `[1, 2, "three"]`,
// but the driver still exits 0.)
"boolean-subschema.schema",
"empty-object.schema",
"issue2680-top-level-array.schema",
"nested-intersection-union.schema",
"prefix-items.schema",
Expand Down Expand Up @@ -1963,6 +1972,8 @@ export const PHPLanguage: Language = {
],
skipMiscJSON: true,
skipSchema: [
// The renderer does not support a bare top-level map.
"empty-object.schema",
"integer-before-number.schema", // Python-specific union-order regression.
// PHP class names are case-insensitive, but the namer dedups
// case-sensitively, so this declares classes that collide (same
Expand Down Expand Up @@ -2070,6 +2081,8 @@ export const TypeScriptZodLanguage: Language = {
],
skipMiscJSON: false,
skipSchema: [
// The renderer does not support a bare top-level map.
"empty-object.schema",
"integer-before-number.schema", // Python-specific union-order regression.
"any.schema",
...skipsUntypedUnions,
Expand Down Expand Up @@ -2195,6 +2208,8 @@ export const TypeScriptEffectSchemaLanguage: Language = {
],
skipMiscJSON: false,
skipSchema: [
// The renderer does not support a bare top-level map.
"empty-object.schema",
"integer-before-number.schema", // Python-specific union-order regression.
"any.schema",
...skipsUntypedUnions,
Expand Down Expand Up @@ -2265,7 +2280,10 @@ export const ElixirLanguage: Language = {
],
skipMiscJSON: false,
skipSchema: [
// The renderer does not support a bare top-level map.
"empty-object.schema",
"integer-before-number.schema", // Python-specific union-order regression.

// The error occurs because a guard clause that references TopLevel is compiled before TopLevel itself. To fix this, put
// TopLevel before Bar, but this doesn't address the actual problem if for example a pattern match to Bar was in TopLevel.
"mutually-recursive.schema",
Expand Down
79 changes: 79 additions & 0 deletions test/unit/typescript-flow-top-level-alias.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Top-level array/map schema fixtures exercise their generated converters, but
// those converters inline the collection type, so they still compile when the
// public top-level alias is missing. Assert the declaration itself here to
// prevent that regression.

import {
InputData,
JSONSchemaInput,
type LanguageName,
quicktype,
} from "quicktype-core";
import { describe, expect, test } from "vitest";

async function renderSchema(
lang: LanguageName,
name: string,
schema: object,
): Promise<string> {
const schemaInput = new JSONSchemaInput(undefined);
await schemaInput.addSource({ name, schema: JSON.stringify(schema) });
const inputData = new InputData();
inputData.addInput(schemaInput);

const result = await quicktype({
inputData,
lang,
rendererOptions: { "just-types": true },
});
return result.lines.join("\n");
}

describe("TypeScript/Flow unnamed top-level aliases", () => {
test.each([
["typescript", "unknown"],
["flow", "mixed"],
] as const)("%s emits a top-level map alias", async (lang, anyType) => {
const output = await renderSchema(lang, "Values", {
type: "object",
additionalProperties: {},
});

expect(output).toContain(
`export type Values = { [key: string]: ${anyType} };`,
);
});

test.each([
"typescript",
"flow",
] as const)("%s emits a top-level array alias", async (lang) => {
const output = await renderSchema(lang, "Values", {
type: "array",
items: { type: "number" },
});

expect(output).toContain("export type Values = number[];");
});

test.each([
"typescript",
"flow",
] as const)("%s does not alias a map whose value type claims the top-level name", async (lang) => {
const output = await renderSchema(lang, "TopLevel", {
type: "object",
additionalProperties: {
type: "object",
properties: {
one: { type: "integer" },
two: { type: "boolean" },
},
required: ["one", "two"],
},
});
const declarations =
output.match(/export (?:type|interface) TopLevel\b/g) ?? [];

expect(declarations).toHaveLength(1);
});
});
Loading