Skip to content

Commit

Permalink
fix: union types of union types in Records (#1933)
Browse files Browse the repository at this point in the history
Revised version of #1926 by dpvball
  • Loading branch information
domoritz committed Apr 21, 2024
1 parent 9dd5462 commit 0d9c3fb
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 27 deletions.
10 changes: 3 additions & 7 deletions src/NodeParser/MappedTypeNodeParser.ts
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 16 additions & 1 deletion 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[];
Expand Down Expand Up @@ -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;
});
}
}
24 changes: 5 additions & 19 deletions src/TypeFormatter/LiteralUnionTypeFormatter.ts
Expand Up @@ -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";

Expand All @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions test/valid-data-type.test.ts
Expand Up @@ -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"));
Expand Down
6 changes: 6 additions & 0 deletions 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<MyType10, string>;
40 changes: 40 additions & 0 deletions 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"
}
}
}

0 comments on commit 0d9c3fb

Please sign in to comment.