From d9b9a15aa5849a2e298c009f60ada0d3539b6f47 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Tue, 4 Jun 2019 14:29:25 +0200 Subject: [PATCH] Translate intersection into union if needed --- src/NodeParser/IntersectionNodeParser.ts | 36 +++++++- test/valid-data.test.ts | 2 +- .../type-intersection-union/main.ts | 21 +++++ .../type-intersection-union/schema.json | 90 +++++++++++++++++++ 4 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 test/valid-data/type-intersection-union/main.ts create mode 100644 test/valid-data/type-intersection-union/schema.json diff --git a/src/NodeParser/IntersectionNodeParser.ts b/src/NodeParser/IntersectionNodeParser.ts index b2fe140e4..ca62cb0fd 100644 --- a/src/NodeParser/IntersectionNodeParser.ts +++ b/src/NodeParser/IntersectionNodeParser.ts @@ -3,6 +3,8 @@ import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { BaseType } from "../Type/BaseType"; import { IntersectionType } from "../Type/IntersectionType"; +import { UnionType } from "../Type/UnionType"; +import { derefType } from "../Utils/derefType"; import { referenceHidden } from "../Utils/isHidden"; export class IntersectionNodeParser implements SubNodeParser { @@ -15,12 +17,42 @@ export class IntersectionNodeParser implements SubNodeParser { public supportsNode(node: ts.IntersectionTypeNode): boolean { return node.kind === ts.SyntaxKind.IntersectionType; } + public createType(node: ts.IntersectionTypeNode, context: Context): BaseType { const hidden = referenceHidden(this.typeChecker); - return new IntersectionType( + return this.translate(new IntersectionType( node.types .filter((subnode) => !hidden(subnode)) .map((subnode) => this.childNodeParser.createType(subnode, context)), - ); + )); + } + + /** + * Translates the given intersection type into a union type if necessary so `A & (B | C)` becomes + * `(A & B) | (A & C)`. If no translation is needed then the original intersection type is returned. + * + * @param intersection - The intersection type to translate. + * @return Either the union type into which the intersection type was translated or the original intersection type + * if no translation is needed. + */ + private translate(intersection: IntersectionType): IntersectionType | UnionType { + const unions = intersection.getTypes().map(type => { + const derefed = derefType(type); + return derefed instanceof UnionType ? derefed.getTypes() : [ type ]; + }); + const result: IntersectionType[] = []; + function process(i: number, types: BaseType[] = []) { + for (const type of unions[i]) { + const currentTypes = types.slice(); + currentTypes.push(type); + if (i < unions.length - 1) { + process(i + 1, currentTypes); + } else { + result.push(new IntersectionType(currentTypes)); + } + } + } + process(0); + return result.length > 1 ? new UnionType(result) : intersection; } } diff --git a/test/valid-data.test.ts b/test/valid-data.test.ts index f5f2a85a8..96efbd675 100644 --- a/test/valid-data.test.ts +++ b/test/valid-data.test.ts @@ -102,7 +102,7 @@ describe("valid-data", () => { it("type-union", assertSchema("type-union", "TypeUnion")); it("type-union-tagged", assertSchema("type-union-tagged", "Shape")); it("type-intersection", assertSchema("type-intersection", "MyObject")); - it("type-intersection-union", assertSchema("type-intersection", "MyObject")); + it("type-intersection-union", assertSchema("type-intersection-union", "MyObject")); it("type-intersection-additional-props", assertSchema("type-intersection-additional-props", "MyObject")); it("type-typeof", assertSchema("type-typeof", "MyType")); diff --git a/test/valid-data/type-intersection-union/main.ts b/test/valid-data/type-intersection-union/main.ts new file mode 100644 index 000000000..26e6f0038 --- /dev/null +++ b/test/valid-data/type-intersection-union/main.ts @@ -0,0 +1,21 @@ +export interface A { + a: number; +} + +export interface B { + b: number; +} + +export interface C { + c: number; +} + +export interface D { + d: number; +} + +export interface E { + e: number; +} + +export type MyObject = (B | C) & A & (D | E); diff --git a/test/valid-data/type-intersection-union/schema.json b/test/valid-data/type-intersection-union/schema.json new file mode 100644 index 000000000..9cc6fef51 --- /dev/null +++ b/test/valid-data/type-intersection-union/schema.json @@ -0,0 +1,90 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-06/schema#", + "definitions": { + "MyObject": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "a": { + "type": "number" + }, + "b": { + "type": "number" + }, + "d": { + "type": "number" + } + }, + "required": [ + "a", + "b", + "d" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "a": { + "type": "number" + }, + "b": { + "type": "number" + }, + "e": { + "type": "number" + } + }, + "required": [ + "a", + "b", + "e" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + }, + "d": { + "type": "number" + } + }, + "required": [ + "a", + "c", + "d" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + }, + "e": { + "type": "number" + } + }, + "required": [ + "a", + "c", + "e" + ], + "type": "object" + } + ] + } + } +}