diff --git a/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts b/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts index 0adcaefd30..c14a44efdb 100644 --- a/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts +++ b/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts @@ -239,6 +239,16 @@ export class JavaScriptRenderer extends ConvenienceRenderer { }; } + /** The expression a deserializer returns when runtime typechecks + * are disabled. Subclasses can wrap it in a cast to the target + * type if `parsedJson`'s type isn't assignable to it. */ + protected uncheckedParsedJson( + _t: Type, + parsedJson: Sourcelike, + ): Sourcelike { + return parsedJson; + } + protected emitConvertModuleBody(): void { const converter = (t: Type, name: Name): void => { const typeMap = this.typeMapTypeFor(t); @@ -251,7 +261,11 @@ export class JavaScriptRenderer extends ConvenienceRenderer { ? "JSON.parse(json)" : "json"; if (!this._jsOptions.runtimeTypecheck) { - this.emitLine("return ", parsedJson, ";"); + this.emitLine( + "return ", + this.uncheckedParsedJson(t, parsedJson), + ";", + ); } else { this.emitLine( "return cast(", diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts index a0a3bfaee2..9e48bfb147 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts @@ -1,17 +1,42 @@ import type { Name } from "../../Naming.js"; +import type { Sourcelike } from "../../Source.js"; import { utf16StringEscape } from "../../support/Strings.js"; import { defined } from "../../support/Support.js"; -import type { ClassType, EnumType } from "../../Type/index.js"; +import type { ClassType, EnumType, Type } from "../../Type/index.js"; import type { JavaScriptTypeAnnotations } from "../JavaScript/index.js"; import { TypeScriptFlowBaseRenderer } from "./TypeScriptFlowBaseRenderer.js"; import { tsFlowTypeAnnotations } from "./utils.js"; export class FlowRenderer extends TypeScriptFlowBaseRenderer { + protected anyType(): string { + return this._tsFlowOptions.preferUnknown ? "mixed" : "any"; + } + protected forbiddenNamesForGlobalNamespace(): string[] { return ["Class", "Date", "Object", "String", "Array", "JSON", "Error"]; } + protected uncheckedParsedJson(t: Type, parsedJson: Sourcelike): Sourcelike { + // With `raw-type any` and `prefer-unknown` the deserializer's + // parameter is `mixed`, which can't be returned as the target + // type without a cast. + if ( + this._tsFlowOptions.rawType !== "json" && + this._tsFlowOptions.preferUnknown + ) { + return [ + "((", + parsedJson, + ": any): ", + this.sourceFor(t).source, + ")", + ]; + } + + return parsedJson; + } + protected get typeAnnotations(): JavaScriptTypeAnnotations { return { never: "", ...tsFlowTypeAnnotations }; } diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts index d20f56c19c..d833317d86 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts @@ -64,7 +64,7 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { return matchType( t, - (_anyType) => singleWord("any"), + (_anyType) => singleWord(this.anyType()), (_nullType) => singleWord("null"), (_boolType) => singleWord("boolean"), (_integerType) => singleWord("number"), @@ -113,6 +113,12 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { ); } + /** The type emitted for `any`-typed values in type declarations + * and converter signatures: the language's type-safe top type + * (`unknown` for TypeScript, `mixed` for Flow) with the + * `prefer-unknown` option, plain `any` without it. */ + protected abstract anyType(): string; + protected abstract emitEnum(e: EnumType, enumName: Name): void; protected abstract emitClassBlock(c: ClassType, className: Name): void; @@ -198,7 +204,7 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { protected deserializerFunctionLine(t: Type, name: Name): Sourcelike { const jsonType = - this._tsFlowOptions.rawType === "json" ? "string" : "any"; + this._tsFlowOptions.rawType === "json" ? "string" : this.anyType(); return [ "function to", name, @@ -212,7 +218,7 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { protected serializerFunctionLine(t: Type, name: Name): Sourcelike { const camelCaseName = modifySource(camelCase, name); const returnType = - this._tsFlowOptions.rawType === "json" ? "string" : "any"; + this._tsFlowOptions.rawType === "json" ? "string" : this.anyType(); return [ "function ", camelCaseName, @@ -227,6 +233,10 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { return undefined; } + // The runtime typecheck helpers are deliberately dynamic, so they + // stay on `any` even with `prefer-unknown`. Only the public API + // surface (type declarations and converter signatures) uses + // `anyType()`. protected get castFunctionLines(): [string, string] { return [ "function cast(val: any, typ: any): T", diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts index 376036727d..1d0d488424 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts @@ -9,13 +9,31 @@ import { TypeScriptFlowBaseRenderer } from "./TypeScriptFlowBaseRenderer.js"; import { tsFlowTypeAnnotations } from "./utils.js"; export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { + protected anyType(): string { + return this._tsFlowOptions.preferUnknown ? "unknown" : "any"; + } + protected forbiddenNamesForGlobalNamespace(): string[] { return ["Array", "Date"]; } + protected uncheckedParsedJson(t: Type, parsedJson: Sourcelike): Sourcelike { + // With `raw-type any` and `prefer-unknown` the deserializer's + // parameter is `unknown`, which can't be returned as the target + // type without a cast. + if ( + this._tsFlowOptions.rawType !== "json" && + this._tsFlowOptions.preferUnknown + ) { + return [parsedJson, " as ", this.sourceFor(t).source]; + } + + return parsedJson; + } + protected deserializerFunctionLine(t: Type, name: Name): Sourcelike { const jsonType = - this._tsFlowOptions.rawType === "json" ? "string" : "any"; + this._tsFlowOptions.rawType === "json" ? "string" : this.anyType(); return [ "public static to", name, @@ -29,7 +47,7 @@ export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { protected serializerFunctionLine(t: Type, name: Name): Sourcelike { const camelCaseName = modifySource(camelCase, name); const returnType = - this._tsFlowOptions.rawType === "json" ? "string" : "any"; + this._tsFlowOptions.rawType === "json" ? "string" : this.anyType(); return [ "public static ", camelCaseName, diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/language.ts b/packages/quicktype-core/src/language/TypeScriptFlow/language.ts index d7f0f208ba..a894ee2833 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/language.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/language.ts @@ -45,6 +45,11 @@ export const tsFlowOptions = { false, ), readonly: new BooleanOption("readonly", "Use readonly type members", false), + preferUnknown: new BooleanOption( + "prefer-unknown", + "Use unknown (TypeScript) or mixed (Flow) instead of any", + true, + ), }; export const typeScriptLanguageConfig = { diff --git a/test/languages.ts b/test/languages.ts index 94f9828e3a..07f9a417a1 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -864,6 +864,7 @@ export const TypeScriptLanguage: Language = { { "acronym-style": "pascal" }, { converters: "all-objects" }, { readonly: "true" }, + { "prefer-unknown": "false" }, ], sourceFiles: ["src/language/TypeScript/index.ts"], }; @@ -943,6 +944,7 @@ export const FlowLanguage: Language = { { "runtime-typecheck": "false" }, { "runtime-typecheck-ignore-unknown-properties": "true" }, { "nice-property-names": "true" }, + { "prefer-unknown": "false" }, ], sourceFiles: ["src/language/Flow/index.ts"], };