Skip to content

Commit 8c44ddf

Browse files
authored
fix: fully unwrap union aliases in mapped keys to avoid generating incorrect additionalProperties (#2232)
1 parent 32bf3f5 commit 8c44ddf

File tree

8 files changed

+144
-13
lines changed

8 files changed

+144
-13
lines changed

src/NodeParser/MappedTypeNodeParser.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ObjectProperty, ObjectType } from "../Type/ObjectType.js";
1616
import { StringType } from "../Type/StringType.js";
1717
import { SymbolType } from "../Type/SymbolType.js";
1818
import { UnionType } from "../Type/UnionType.js";
19-
import { derefAnnotatedType, derefType } from "../Utils/derefType.js";
19+
import { derefAnnotatedType, derefType, isDeepLiteralUnion } from "../Utils/derefType.js";
2020
import { getKey } from "../Utils/nodeKey.js";
2121
import { preserveAnnotation } from "../Utils/preserveAnnotation.js";
2222
import { removeUndefined } from "../Utils/removeUndefined.js";
@@ -158,6 +158,10 @@ export class MappedTypeNodeParser implements SubNodeParser {
158158
keyListType: UnionType,
159159
context: Context,
160160
): BaseType | boolean {
161+
if (isDeepLiteralUnion(keyListType)) {
162+
return this.additionalProperties;
163+
}
164+
161165
const key = keyListType.getTypes().filter((type) => !(derefType(type) instanceof LiteralType))[0];
162166

163167
if (key) {

src/Utils/derefType.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { AnnotatedType } from "../Type/AnnotatedType.js";
33
import type { BaseType } from "../Type/BaseType.js";
44
import { DefinitionType } from "../Type/DefinitionType.js";
55
import { HiddenType } from "../Type/HiddenType.js";
6+
import { LiteralType } from "../Type/LiteralType.js";
67
import { NeverType } from "../Type/NeverType.js";
78
import { ReferenceType } from "../Type/ReferenceType.js";
9+
import { UnionType } from "../Type/UnionType.js";
810

911
/**
1012
* Dereference the type as far as possible.
@@ -38,6 +40,23 @@ export function isHiddenType(type: BaseType): boolean {
3840
return false;
3941
}
4042

43+
/**
44+
* Recursively checks whether the given type is a union composed entirely of literal types.
45+
*/
46+
export function isDeepLiteralUnion(type: BaseType): boolean {
47+
const resolved = derefType(type);
48+
49+
if (resolved instanceof LiteralType) {
50+
return true;
51+
}
52+
53+
if (resolved instanceof UnionType) {
54+
return resolved.getTypes().every((t) => isDeepLiteralUnion(t));
55+
}
56+
57+
return false;
58+
}
59+
4160
export function derefAliasedType(type: BaseType): BaseType {
4261
if (type instanceof AliasType) {
4362
return derefAliasedType(type.getType());

test/valid-data-type.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ describe("valid-data-type", () => {
8989
it("type-keyof-object", assertValidSchema("type-keyof-object", "MyType"));
9090
it("type-keyof-object-function", assertValidSchema("type-keyof-object-function", "MyType"));
9191
it("type-mapped-pick-union-alias", assertValidSchema("type-mapped-pick-union-alias", "PickAliasedLiteralUnion"));
92+
it("type-mapped-exported-aliases", assertValidSchema("type-mapped-exported-aliases", "*"));
9293
it("type-mapped-simple", assertValidSchema("type-mapped-simple", "MyObject"));
9394
it("type-mapped-index", assertValidSchema("type-mapped-index", "MyObject"));
9495
it("type-mapped-index-as", assertValidSchema("type-mapped-index-as", "MyObject"));
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
interface SomeInterface {
2+
a: number;
3+
b: string;
4+
c: boolean;
5+
d: string[];
6+
e: null;
7+
}
8+
9+
type A = "a";
10+
type B = "b";
11+
type C = "c";
12+
type D = "d";
13+
14+
// Export the aliases individually to verify they're handled correctly
15+
export type AB = A | B;
16+
export type ABC = AB | C;
17+
export type ABCD = ABC | D;
18+
19+
export type ABCDE = Pick<SomeInterface, ABCD | "e">;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"definitions": {
4+
"AB": {
5+
"enum": [
6+
"a",
7+
"b"
8+
],
9+
"type": "string"
10+
},
11+
"ABC": {
12+
"anyOf": [
13+
{
14+
"$ref": "#/definitions/AB"
15+
},
16+
{
17+
"const": "c",
18+
"type": "string"
19+
}
20+
]
21+
},
22+
"ABCD": {
23+
"anyOf": [
24+
{
25+
"$ref": "#/definitions/ABC"
26+
},
27+
{
28+
"const": "d",
29+
"type": "string"
30+
}
31+
]
32+
},
33+
"ABCDE": {
34+
"additionalProperties": false,
35+
"properties": {
36+
"a": {
37+
"type": "number"
38+
},
39+
"b": {
40+
"type": "string"
41+
},
42+
"c": {
43+
"type": "boolean"
44+
},
45+
"d": {
46+
"items": {
47+
"type": "string"
48+
},
49+
"type": "array"
50+
},
51+
"e": {
52+
"type": "null"
53+
}
54+
},
55+
"required": [
56+
"a",
57+
"b",
58+
"c",
59+
"d",
60+
"e"
61+
],
62+
"type": "object"
63+
}
64+
}
65+
}
Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
interface SomeInterface {
2-
foo: string;
3-
bar: number;
2+
a: number;
3+
b: string;
4+
c: boolean;
5+
d: string[];
6+
e: null;
47
}
58

6-
type KeyFoo = "foo";
7-
type KeyBar = "bar";
9+
type A = "a";
10+
type B = "b";
11+
type C = "c";
12+
type D = "d";
13+
type E = "e";
814

9-
export type PickAliasedLiteralUnion = Pick<SomeInterface, KeyFoo | KeyBar>;
15+
type AB = A | B;
16+
type ABC = AB | C;
17+
type ABCD = ABC | D;
18+
19+
export type PickAliasedLiteralUnion = Pick<SomeInterface, ABCD | E>;

test/valid-data/type-mapped-pick-union-alias/schema.json

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,31 @@
55
"PickAliasedLiteralUnion": {
66
"additionalProperties": false,
77
"properties": {
8-
"bar": {
8+
"a": {
99
"type": "number"
1010
},
11-
"foo": {
11+
"b": {
1212
"type": "string"
13+
},
14+
"c": {
15+
"type": "boolean"
16+
},
17+
"d": {
18+
"items": {
19+
"type": "string"
20+
},
21+
"type": "array"
22+
},
23+
"e": {
24+
"type": "null"
1325
}
1426
},
1527
"required": [
16-
"foo",
17-
"bar"
28+
"a",
29+
"b",
30+
"c",
31+
"d",
32+
"e"
1833
],
1934
"type": "object"
2035
}

test/valid-data/type-mapped-union-union/schema.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
"$schema": "http://json-schema.org/draft-07/schema#",
44
"definitions": {
55
"MyType": {
6-
"additionalProperties": {
7-
"type": "string"
8-
},
6+
"additionalProperties": false,
97
"properties": {
108
"s1": {
119
"type": "string"

0 commit comments

Comments
 (0)