diff --git a/src/ExposeNodeParser.ts b/src/ExposeNodeParser.ts index 171a67d5f..670baf22f 100644 --- a/src/ExposeNodeParser.ts +++ b/src/ExposeNodeParser.ts @@ -6,6 +6,10 @@ import { DefinitionType } from "./Type/DefinitionType.js"; import type { ReferenceType } from "./Type/ReferenceType.js"; import { hasJsDocTag } from "./Utils/hasJsDocTag.js"; import { symbolAtNode } from "./Utils/symbolAtNode.js"; +import { AliasType } from "./Type/AliasType.js"; +import { derefAliasedType, isDeepLiteralUnion } from "./Utils/derefType.js"; +import { ObjectType } from "./Type/ObjectType.js"; +import { IntersectionType } from "./Type/IntersectionType.js"; export class ExposeNodeParser implements SubNodeParser { public constructor( @@ -22,7 +26,7 @@ export class ExposeNodeParser implements SubNodeParser { public createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType { const baseType = this.subNodeParser.createType(node, context, reference); - if (!this.isExportNode(node)) { + if (!this.isExportNode(node) || this.isFromLib(node) || this.shouldInline(node, baseType, context)) { return baseType; } @@ -49,4 +53,49 @@ export class ExposeNodeParser implements SubNodeParser { return argumentIds.length ? `${fullName}<${argumentIds.join(",")}>` : fullName; } + + private isFromLib(node: ts.Node): boolean { + const sourceFile = node.getSourceFile(); + if (!sourceFile) { + return false; + } + return /[\\/]typescript[\\/]lib[\\/]/i.test(sourceFile.fileName); + } + + private shouldInline(node: ts.Node, type: BaseType, context: Context): boolean { + if (!ts.isTypeAliasDeclaration(node)) { + return false; + } + if (!(type instanceof AliasType)) { + return false; + } + if (!node.typeParameters?.length) { + return false; + } + + const localSymbol: ts.Symbol = (node as any).localSymbol; + const isExported = localSymbol ? "exportSymbol" in localSymbol : false; + + const actual = derefAliasedType(type.getType()); + const hasStructuralArg = context + .getArguments() + .some((arg) => /(structure|object|alias|def-alias)-/.test(arg?.getName() ?? "")); + + if (isExported && !hasStructuralArg) { + return false; + } + + if (isDeepLiteralUnion(actual)) { + return true; + } + + if (!isExported && (actual instanceof ObjectType || actual instanceof IntersectionType)) { + return true; + } + + if (hasStructuralArg) { + return true; + } + return false; + } } diff --git a/test/valid-data-other.test.ts b/test/valid-data-other.test.ts index 81bfab963..4d0b3415d 100644 --- a/test/valid-data-other.test.ts +++ b/test/valid-data-other.test.ts @@ -57,6 +57,9 @@ describe("valid-data-other", () => { it("generic-nested", assertValidSchema("generic-nested", "MyObject")); it("generic-prefixed-number", assertValidSchema("generic-prefixed-number", "MyObject")); it("generic-void", assertValidSchema("generic-void", "MyObject")); + it("generic-mapped-complex", assertValidSchema("generic-mapped-complex", "*")); + it("generic-valueof", assertValidSchema("generic-valueof", "*")); + it("generic-mapped-reused", assertValidSchema("generic-mapped-reused", "*", { encodeRefs: false })); it("nullable-null", assertValidSchema("nullable-null", "MyObject")); diff --git a/test/valid-data/generic-mapped-complex/main.ts b/test/valid-data/generic-mapped-complex/main.ts new file mode 100644 index 000000000..10158945f --- /dev/null +++ b/test/valid-data/generic-mapped-complex/main.ts @@ -0,0 +1,13 @@ +import { OverrideProperties } from "./util"; + +export type Base = { + foo: string; + bar: number; +}; + +export type MyType = OverrideProperties< + Base, + { + bar: string; + } +>; diff --git a/test/valid-data/generic-mapped-complex/schema.json b/test/valid-data/generic-mapped-complex/schema.json new file mode 100644 index 000000000..f3513f943 --- /dev/null +++ b/test/valid-data/generic-mapped-complex/schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Base": { + "additionalProperties": false, + "properties": { + "bar": { + "type": "number" + }, + "foo": { + "type": "string" + } + }, + "required": [ + "foo", + "bar" + ], + "type": "object" + }, + "MyType": { + "additionalProperties": false, + "properties": { + "bar": { + "type": "string" + }, + "foo": { + "type": "string" + } + }, + "required": [ + "bar", + "foo" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/generic-mapped-complex/util.ts b/test/valid-data/generic-mapped-complex/util.ts new file mode 100644 index 000000000..cf3309dee --- /dev/null +++ b/test/valid-data/generic-mapped-complex/util.ts @@ -0,0 +1,27 @@ +// types are from https://github.com/sindresorhus/type-fest + +export type Simplify = { [KeyType in keyof T]: T[KeyType] } & {}; + +export type PickIndexSignature = { + [KeyType in keyof ObjectType as {} extends Record ? KeyType : never]: ObjectType[KeyType]; +}; + +export type OmitIndexSignature = { + [KeyType in keyof ObjectType as {} extends Record ? never : KeyType]: ObjectType[KeyType]; +}; + +type SimpleMerge = { + [Key in keyof Destination as Key extends keyof Source ? never : Key]: Destination[Key]; +} & Source; + +export type Merge = Simplify< + SimpleMerge, PickIndexSignature> & + SimpleMerge, OmitIndexSignature> +>; + +export type OverrideProperties< + TOriginal, + TOverride extends Partial> & { + [Key in keyof TOverride]: Key extends keyof TOriginal ? TOverride[Key] : never; + }, +> = Merge; diff --git a/test/valid-data/generic-mapped-reused/main.ts b/test/valid-data/generic-mapped-reused/main.ts new file mode 100644 index 000000000..a23ae1d24 --- /dev/null +++ b/test/valid-data/generic-mapped-reused/main.ts @@ -0,0 +1,15 @@ +export type MyHelper = { + [K in keyof A as K extends keyof B ? never : K]: A[K]; +} & B; + +type Base = { foo: string; bar: number }; +type Patch = { bar: string; baz: boolean }; + +type Resolved = MyHelper; // ← `Resolved` NOT EXPORTED + +export interface Foo { + beta: Resolved; +} +export interface Bar { + gamma: Resolved; +} diff --git a/test/valid-data/generic-mapped-reused/schema.json b/test/valid-data/generic-mapped-reused/schema.json new file mode 100644 index 000000000..074350edf --- /dev/null +++ b/test/valid-data/generic-mapped-reused/schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Bar": { + "additionalProperties": false, + "properties": { + "gamma": { + "$ref": "#/definitions/MyHelper" + } + }, + "required": [ + "gamma" + ], + "type": "object" + }, + "Foo": { + "additionalProperties": false, + "properties": { + "beta": { + "$ref": "#/definitions/MyHelper" + } + }, + "required": [ + "beta" + ], + "type": "object" + }, + "MyHelper": { + "additionalProperties": false, + "properties": { + "bar": { + "type": "string" + }, + "baz": { + "type": "boolean" + }, + "foo": { + "type": "string" + } + }, + "required": [ + "bar", + "baz", + "foo" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/generic-valueof/main.ts b/test/valid-data/generic-valueof/main.ts new file mode 100644 index 000000000..884c3f894 --- /dev/null +++ b/test/valid-data/generic-valueof/main.ts @@ -0,0 +1,8 @@ +export const RuntimeObject = { + FOO: "foo-val", + BAR: "bar-val", +} as const; + +export type ValueOf = T[keyof T]; + +export type MyType = ValueOf; diff --git a/test/valid-data/generic-valueof/schema.json b/test/valid-data/generic-valueof/schema.json new file mode 100644 index 000000000..ef4037017 --- /dev/null +++ b/test/valid-data/generic-valueof/schema.json @@ -0,0 +1,13 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "enum": [ + "foo-val", + "bar-val" + ], + "type": "string" + } + } +}