diff --git a/src/NodeParser/MappedTypeNodeParser.ts b/src/NodeParser/MappedTypeNodeParser.ts index 62561819..44194cc1 100644 --- a/src/NodeParser/MappedTypeNodeParser.ts +++ b/src/NodeParser/MappedTypeNodeParser.ts @@ -18,6 +18,7 @@ import { derefAnnotatedType, derefType } from "../Utils/derefType.js"; import { getKey } from "../Utils/nodeKey.js"; import { preserveAnnotation } from "../Utils/preserveAnnotation.js"; import { removeUndefined } from "../Utils/removeUndefined.js"; +import { uniqueTypeArray } from "../Utils/uniqueTypeArray.js"; export class MappedTypeNodeParser implements SubNodeParser { public constructor( @@ -94,16 +95,11 @@ export class MappedTypeNodeParser implements SubNodeParser { if (!node.nameType) { return rawKey; } - const key = derefType( - this.childNodeParser.createType(node.nameType, this.createSubContext(node, rawKey, context)), - ); - - return key; + return derefType(this.childNodeParser.createType(node.nameType, this.createSubContext(node, rawKey, context))); } protected getProperties(node: ts.MappedTypeNode, keyListType: UnionType, context: Context): ObjectProperty[] { - return keyListType - .getTypes() + return uniqueTypeArray(keyListType.getFlattenedTypes(derefType)) .filter((type): type is LiteralType => type instanceof LiteralType) .map((type) => [type, this.mapKey(node, type, context)]) .filter((value): value is [LiteralType, LiteralType] => value[1] instanceof LiteralType) diff --git a/src/Type/UnionType.ts b/src/Type/UnionType.ts index 2d36da25..b1700eb3 100644 --- a/src/Type/UnionType.ts +++ b/src/Type/UnionType.ts @@ -1,7 +1,7 @@ import { BaseType } from "./BaseType.js"; import { uniqueTypeArray } from "../Utils/uniqueTypeArray.js"; import { NeverType } from "./NeverType.js"; -import { derefType } from "../Utils/derefType.js"; +import { derefAliasedType, derefType, isHiddenType } from "../Utils/derefType.js"; export class UnionType extends BaseType { private readonly types: BaseType[]; @@ -56,4 +56,19 @@ export class UnionType extends BaseType { } } } + + /** + * Get the types in this union as a flat list. + */ + public getFlattenedTypes(deref: (type: BaseType) => BaseType = derefAliasedType): BaseType[] { + return this.getTypes() + .filter((t) => !isHiddenType(t)) + .map(deref) + .flatMap((t) => { + if (t instanceof UnionType) { + return t.getFlattenedTypes(deref); + } + return t; + }); + } } diff --git a/src/TypeFormatter/LiteralUnionTypeFormatter.ts b/src/TypeFormatter/LiteralUnionTypeFormatter.ts index 68572bfc..937d2a0e 100644 --- a/src/TypeFormatter/LiteralUnionTypeFormatter.ts +++ b/src/TypeFormatter/LiteralUnionTypeFormatter.ts @@ -6,7 +6,6 @@ import { LiteralType, LiteralValue } from "../Type/LiteralType.js"; import { NullType } from "../Type/NullType.js"; import { StringType } from "../Type/StringType.js"; import { UnionType } from "../Type/UnionType.js"; -import { derefAliasedType, isHiddenType } from "../Utils/derefType.js"; import { typeName } from "../Utils/typeName.js"; import { uniqueArray } from "../Utils/uniqueArray.js"; @@ -20,10 +19,10 @@ export class LiteralUnionTypeFormatter implements SubTypeFormatter { let allStrings = true; let hasNull = false; - const flattenedTypes = flattenTypes(type); + const literals = type.getFlattenedTypes(); // filter out String types since we need to be more careful about them - const types = flattenedTypes.filter((t) => { + const types = literals.filter((t) => { if (t instanceof StringType) { hasString = true; preserveLiterals = preserveLiterals || t.getPreserveLiterals(); @@ -70,23 +69,10 @@ export class LiteralUnionTypeFormatter implements SubTypeFormatter { } } -function flattenTypes(type: UnionType): (StringType | LiteralType | NullType)[] { - return type - .getTypes() - .filter((t) => !isHiddenType(t)) - .map(derefAliasedType) - .flatMap((t) => { - if (t instanceof UnionType) { - return flattenTypes(t); - } - return t as StringType | LiteralType | NullType; - }); -} - export function isLiteralUnion(type: UnionType): boolean { - return flattenTypes(type).every( - (item) => item instanceof LiteralType || item instanceof NullType || item instanceof StringType, - ); + return type + .getFlattenedTypes() + .every((item) => item instanceof LiteralType || item instanceof NullType || item instanceof StringType); } function getLiteralValue(value: LiteralType | NullType): LiteralValue | null { diff --git a/test/valid-data-type.test.ts b/test/valid-data-type.test.ts index d91be0ac..1f219244 100644 --- a/test/valid-data-type.test.ts +++ b/test/valid-data-type.test.ts @@ -102,6 +102,7 @@ describe("valid-data-type", () => { it("type-mapped-additional-props", assertValidSchema("type-mapped-additional-props", "MyObject")); it("type-mapped-array", assertValidSchema("type-mapped-array", "MyObject")); it("type-mapped-union-intersection", assertValidSchema("type-mapped-union-intersection", "MyObject")); + it("type-mapped-union-union", assertValidSchema("type-mapped-union-union", "MyType")); it("type-mapped-enum", assertValidSchema("type-mapped-enum", "MyObject")); it("type-mapped-enum-optional", assertValidSchema("type-mapped-enum-optional", "MyObject")); it("type-mapped-enum-null", assertValidSchema("type-mapped-enum-null", "MyObject")); diff --git a/test/valid-data/type-mapped-union-union/main.ts b/test/valid-data/type-mapped-union-union/main.ts new file mode 100644 index 00000000..6ecd7bfa --- /dev/null +++ b/test/valid-data/type-mapped-union-union/main.ts @@ -0,0 +1,6 @@ +type MyType1 = "s1"; +type MyType2 = MyType1 | "s2" | "s3"; +type MyType3 = MyType2 | "s4" | "s5"; +type MyType10 = MyType3 | MyType2 | "s6"; + +export type MyType = Record; diff --git a/test/valid-data/type-mapped-union-union/schema.json b/test/valid-data/type-mapped-union-union/schema.json new file mode 100644 index 00000000..8deb3bb8 --- /dev/null +++ b/test/valid-data/type-mapped-union-union/schema.json @@ -0,0 +1,40 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "additionalProperties": { + "type": "string" + }, + "properties": { + "s1": { + "type": "string" + }, + "s2": { + "type": "string" + }, + "s3": { + "type": "string" + }, + "s4": { + "type": "string" + }, + "s5": { + "type": "string" + }, + "s6": { + "type": "string" + } + }, + "required": [ + "s1", + "s2", + "s3", + "s4", + "s5", + "s6" + ], + "type": "object" + } + } +}