From 8547b1252879676be59ea75f1db2e25967fadaac Mon Sep 17 00:00:00 2001 From: Daan Boer <39552979+daanboer@users.noreply.github.com> Date: Sun, 31 Jul 2022 15:27:39 +0200 Subject: [PATCH] feat: support inferred types in conditionals (#1265) --- factory/parser.ts | 3 + src/NodeParser/ConditionalTypeNodeParser.ts | 48 +++++++---- src/NodeParser/InferTypeNodeParser.ts | 17 ++++ src/NodeParser/MappedTypeNodeParser.ts | 2 +- src/NodeParser/RestTypeNodeParser.ts | 4 +- src/Type/InferType.ts | 11 +++ src/Type/RestType.ts | 6 +- src/Type/TupleType.ts | 29 ++++++- src/TypeFormatter/TupleTypeFormatter.ts | 19 ++++- src/Utils/isAssignableTo.ts | 84 ++++++++++++++++--- test/unit/isAssignableTo.test.ts | 29 +++++++ test/valid-data-type.test.ts | 11 +++ .../type-conditional-infer-nested/main.ts | 3 + .../type-conditional-infer-nested/schema.json | 13 +++ .../type-conditional-infer-recursive/main.ts | 3 + .../schema.json | 31 +++++++ .../type-conditional-infer-rest/main.ts | 3 + .../type-conditional-infer-rest/schema.json | 22 +++++ .../main.ts | 12 +++ .../schema.json | 49 +++++++++++ .../type-conditional-infer-tuple-xor/main.ts | 13 +++ .../schema.json | 64 ++++++++++++++ .../valid-data/type-conditional-infer/main.ts | 3 + .../type-conditional-infer/schema.json | 9 ++ .../type-tuple-nested-rest-to-union/main.ts | 5 ++ .../schema.json | 58 +++++++++++++ .../type-tuple-nested-rest-uniform/main.ts | 1 + .../schema.json | 14 ++++ .../valid-data/type-tuple-nested-rest/main.ts | 1 + .../type-tuple-nested-rest/schema.json | 61 ++++++++++++++ 30 files changed, 595 insertions(+), 33 deletions(-) create mode 100644 src/NodeParser/InferTypeNodeParser.ts create mode 100644 src/Type/InferType.ts create mode 100644 test/valid-data/type-conditional-infer-nested/main.ts create mode 100644 test/valid-data/type-conditional-infer-nested/schema.json create mode 100644 test/valid-data/type-conditional-infer-recursive/main.ts create mode 100644 test/valid-data/type-conditional-infer-recursive/schema.json create mode 100644 test/valid-data/type-conditional-infer-rest/main.ts create mode 100644 test/valid-data/type-conditional-infer-rest/schema.json create mode 100644 test/valid-data/type-conditional-infer-tail-recursion/main.ts create mode 100644 test/valid-data/type-conditional-infer-tail-recursion/schema.json create mode 100644 test/valid-data/type-conditional-infer-tuple-xor/main.ts create mode 100644 test/valid-data/type-conditional-infer-tuple-xor/schema.json create mode 100644 test/valid-data/type-conditional-infer/main.ts create mode 100644 test/valid-data/type-conditional-infer/schema.json create mode 100644 test/valid-data/type-tuple-nested-rest-to-union/main.ts create mode 100644 test/valid-data/type-tuple-nested-rest-to-union/schema.json create mode 100644 test/valid-data/type-tuple-nested-rest-uniform/main.ts create mode 100644 test/valid-data/type-tuple-nested-rest-uniform/schema.json create mode 100644 test/valid-data/type-tuple-nested-rest/main.ts create mode 100644 test/valid-data/type-tuple-nested-rest/schema.json diff --git a/factory/parser.ts b/factory/parser.ts index 3ca76ab0b..87448f812 100644 --- a/factory/parser.ts +++ b/factory/parser.ts @@ -22,6 +22,7 @@ import { FunctionNodeParser } from "../src/NodeParser/FunctionNodeParser"; import { FunctionParser } from "../src/NodeParser/FunctionParser"; import { HiddenNodeParser } from "../src/NodeParser/HiddenTypeNodeParser"; import { IndexedAccessTypeNodeParser } from "../src/NodeParser/IndexedAccessTypeNodeParser"; +import { InferTypeNodeParser } from "../src/NodeParser/InferTypeNodeParser"; import { InterfaceAndClassNodeParser } from "../src/NodeParser/InterfaceAndClassNodeParser"; import { IntersectionNodeParser } from "../src/NodeParser/IntersectionNodeParser"; import { IntrinsicNodeParser } from "../src/NodeParser/IntrinsicNodeParser"; @@ -122,6 +123,8 @@ export function createParser(program: ts.Program, config: Config, augmentor?: Pa .addNodeParser(new TypeReferenceNodeParser(typeChecker, chainNodeParser)) .addNodeParser(new ExpressionWithTypeArgumentsNodeParser(typeChecker, chainNodeParser)) + .addNodeParser(new InferTypeNodeParser(typeChecker, chainNodeParser)) + .addNodeParser(new IndexedAccessTypeNodeParser(chainNodeParser)) .addNodeParser(new TypeofNodeParser(typeChecker, chainNodeParser)) .addNodeParser(new MappedTypeNodeParser(chainNodeParser, mergedConfig.additionalProperties)) diff --git a/src/NodeParser/ConditionalTypeNodeParser.ts b/src/NodeParser/ConditionalTypeNodeParser.ts index 7ebb5c036..1cd68ac27 100644 --- a/src/NodeParser/ConditionalTypeNodeParser.ts +++ b/src/NodeParser/ConditionalTypeNodeParser.ts @@ -6,6 +6,10 @@ import { isAssignableTo } from "../Utils/isAssignableTo"; import { narrowType } from "../Utils/narrowType"; import { UnionType } from "../Type/UnionType"; +class CheckType { + constructor(public parameterName: string, public type: BaseType | undefined) {} +} + export class ConditionalTypeNodeParser implements SubNodeParser { public constructor(protected typeChecker: ts.TypeChecker, protected childNodeParser: NodeParser) {} @@ -18,14 +22,19 @@ export class ConditionalTypeNodeParser implements SubNodeParser { const extendsType = this.childNodeParser.createType(node.extendsType, context); const checkTypeParameterName = this.getTypeParameterName(node.checkType); + const inferMap = new Map(); + // If check-type is not a type parameter then condition is very simple, no type narrowing needed if (checkTypeParameterName == null) { - const result = isAssignableTo(extendsType, checkType); - return this.childNodeParser.createType(result ? node.trueType : node.falseType, context); + const result = isAssignableTo(extendsType, checkType, inferMap); + return this.childNodeParser.createType( + result ? node.trueType : node.falseType, + this.createSubContext(node, context, undefined, result ? inferMap : new Map()) + ); } // Narrow down check type for both condition branches - const trueCheckType = narrowType(checkType, (type) => isAssignableTo(extendsType, type)); + const trueCheckType = narrowType(checkType, (type) => isAssignableTo(extendsType, type, inferMap)); const falseCheckType = narrowType(checkType, (type) => !isAssignableTo(extendsType, type)); // Follow the relevant branches and return the results from them @@ -33,7 +42,7 @@ export class ConditionalTypeNodeParser implements SubNodeParser { if (trueCheckType !== undefined) { const result = this.childNodeParser.createType( node.trueType, - this.createSubContext(node, checkTypeParameterName, trueCheckType, context) + this.createSubContext(node, context, new CheckType(checkTypeParameterName, trueCheckType), inferMap) ); if (result) { results.push(result); @@ -42,7 +51,7 @@ export class ConditionalTypeNodeParser implements SubNodeParser { if (falseCheckType !== undefined) { const result = this.childNodeParser.createType( node.falseType, - this.createSubContext(node, checkTypeParameterName, falseCheckType, context) + this.createSubContext(node, context, new CheckType(checkTypeParameterName, falseCheckType)) ); if (result) { results.push(result); @@ -72,25 +81,36 @@ export class ConditionalTypeNodeParser implements SubNodeParser { * the check-type is a type parameter which is then narrowed down by the extends-type. * * @param node - The reference node for the new context. - * @param checkTypeParameterName - The type parameter name of the check-type. - * @param narrowedCheckType - The narrowed down check type to use for the type parameter in sub parsers. + * @param checkType - An object containing the type parameter name of the check-type, and the narrowed + * down check type to use for the type parameter in sub parsers. + * @param inferMap - A map that links parameter names to their inferred types. * @return The created sub context. */ protected createSubContext( node: ts.ConditionalTypeNode, - checkTypeParameterName: string, - narrowedCheckType: BaseType, - parentContext: Context + parentContext: Context, + checkType?: CheckType, + inferMap: Map = new Map() ): Context { const subContext = new Context(node); - // Set new narrowed type for check type parameter - subContext.pushParameter(checkTypeParameterName); - subContext.pushArgument(narrowedCheckType); + // Newly inferred types take precedence over check and parent types. + inferMap.forEach((value, key) => { + subContext.pushParameter(key); + subContext.pushArgument(value); + }); + + if (checkType !== undefined) { + // Set new narrowed type for check type parameter + if (!(checkType.parameterName in inferMap)) { + subContext.pushParameter(checkType.parameterName); + subContext.pushArgument(checkType.type); + } + } // Copy all other type parameters from parent context parentContext.getParameters().forEach((parentParameter) => { - if (parentParameter !== checkTypeParameterName) { + if (parentParameter !== checkType?.parameterName && !(parentParameter in inferMap)) { subContext.pushParameter(parentParameter); subContext.pushArgument(parentContext.getArgument(parentParameter)); } diff --git a/src/NodeParser/InferTypeNodeParser.ts b/src/NodeParser/InferTypeNodeParser.ts new file mode 100644 index 000000000..5520205fd --- /dev/null +++ b/src/NodeParser/InferTypeNodeParser.ts @@ -0,0 +1,17 @@ +import ts from "typescript"; +import { Context, NodeParser } from "../NodeParser"; +import { SubNodeParser } from "../SubNodeParser"; +import { BaseType } from "../Type/BaseType"; +import { InferType } from "../Type/InferType"; + +export class InferTypeNodeParser implements SubNodeParser { + public constructor(protected typeChecker: ts.TypeChecker, protected childNodeParser: NodeParser) {} + + public supportsNode(node: ts.InferTypeNode): boolean { + return node.kind === ts.SyntaxKind.InferType; + } + + public createType(node: ts.InferTypeNode, _context: Context): BaseType | undefined { + return new InferType(node.typeParameter.name.escapedText.toString()); + } +} diff --git a/src/NodeParser/MappedTypeNodeParser.ts b/src/NodeParser/MappedTypeNodeParser.ts index 312332344..997459ea2 100644 --- a/src/NodeParser/MappedTypeNodeParser.ts +++ b/src/NodeParser/MappedTypeNodeParser.ts @@ -87,7 +87,7 @@ export class MappedTypeNodeParser implements SubNodeParser { protected getProperties(node: ts.MappedTypeNode, keyListType: UnionType, context: Context): ObjectProperty[] { return keyListType .getTypes() - .filter((type) => type instanceof LiteralType) + .filter((type): type is LiteralType => type instanceof LiteralType) .reduce((result: ObjectProperty[], key: LiteralType) => { const namedKey = this.mapKey(node, key, context); const propertyType = this.childNodeParser.createType( diff --git a/src/NodeParser/RestTypeNodeParser.ts b/src/NodeParser/RestTypeNodeParser.ts index c7767f120..357b7ed3a 100644 --- a/src/NodeParser/RestTypeNodeParser.ts +++ b/src/NodeParser/RestTypeNodeParser.ts @@ -3,7 +3,9 @@ import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { ArrayType } from "../Type/ArrayType"; import { BaseType } from "../Type/BaseType"; +import { InferType } from "../Type/InferType"; import { RestType } from "../Type/RestType"; +import { TupleType } from "../Type/TupleType"; export class RestTypeNodeParser implements SubNodeParser { public constructor(protected childNodeParser: NodeParser) {} @@ -11,6 +13,6 @@ export class RestTypeNodeParser implements SubNodeParser { return node.kind === ts.SyntaxKind.RestType; } public createType(node: ts.RestTypeNode, context: Context): BaseType { - return new RestType(this.childNodeParser.createType(node.type, context) as ArrayType); + return new RestType(this.childNodeParser.createType(node.type, context) as ArrayType | InferType | TupleType); } } diff --git a/src/Type/InferType.ts b/src/Type/InferType.ts new file mode 100644 index 000000000..6a2ed2be0 --- /dev/null +++ b/src/Type/InferType.ts @@ -0,0 +1,11 @@ +import { BaseType } from "./BaseType"; + +export class InferType extends BaseType { + constructor(private id: string) { + super(); + } + + public getId(): string { + return this.id; + } +} diff --git a/src/Type/RestType.ts b/src/Type/RestType.ts index 0af9ca74c..004c66d57 100644 --- a/src/Type/RestType.ts +++ b/src/Type/RestType.ts @@ -1,8 +1,10 @@ import { ArrayType } from "./ArrayType"; import { BaseType } from "./BaseType"; +import { InferType } from "./InferType"; +import { TupleType } from "./TupleType"; export class RestType extends BaseType { - public constructor(private item: ArrayType, private title: string | null = null) { + public constructor(private item: ArrayType | InferType | TupleType, private title: string | null = null) { super(); } @@ -14,7 +16,7 @@ export class RestType extends BaseType { return this.title; } - public getType(): ArrayType { + public getType(): ArrayType | InferType | TupleType { return this.item; } } diff --git a/src/Type/TupleType.ts b/src/Type/TupleType.ts index e728d2c52..7366c9b55 100644 --- a/src/Type/TupleType.ts +++ b/src/Type/TupleType.ts @@ -1,15 +1,40 @@ +import { derefType } from "../Utils/derefType"; +import { ArrayType } from "./ArrayType"; import { BaseType } from "./BaseType"; +import { InferType } from "./InferType"; +import { RestType } from "./RestType"; + +function normalize(types: Readonly>): Array { + let normalized: Array = []; + + for (const type of types) { + if (type instanceof RestType) { + const inner_type = derefType(type.getType()) as ArrayType | InferType | TupleType; + normalized = [ + ...normalized, + ...(inner_type instanceof TupleType ? normalize(inner_type.getTypes()) : [type]), + ]; + } else { + normalized.push(type); + } + } + return normalized; +} export class TupleType extends BaseType { - public constructor(private types: readonly (BaseType | undefined)[]) { + private types: Readonly>; + + public constructor(types: Readonly>) { super(); + + this.types = normalize(types); } public getId(): string { return `[${this.types.map((item) => item?.getId() ?? "never").join(",")}]`; } - public getTypes(): readonly (BaseType | undefined)[] { + public getTypes(): Readonly> { return this.types; } } diff --git a/src/TypeFormatter/TupleTypeFormatter.ts b/src/TypeFormatter/TupleTypeFormatter.ts index 53a1cbda3..400628073 100644 --- a/src/TypeFormatter/TupleTypeFormatter.ts +++ b/src/TypeFormatter/TupleTypeFormatter.ts @@ -1,5 +1,6 @@ import { Definition } from "../Schema/Definition"; import { SubTypeFormatter } from "../SubTypeFormatter"; +import { ArrayType } from "../Type/ArrayType"; import { BaseType } from "../Type/BaseType"; import { OptionalType } from "../Type/OptionalType"; import { RestType } from "../Type/RestType"; @@ -8,6 +9,21 @@ import { TypeFormatter } from "../TypeFormatter"; import { notUndefined } from "../Utils/notUndefined"; import { uniqueArray } from "../Utils/uniqueArray"; +function uniformRestType(type: RestType, check_type: BaseType): boolean { + const inner = type.getType(); + return ( + (inner instanceof ArrayType && inner.getItem().getId() === check_type.getId()) || + (inner instanceof TupleType && + inner.getTypes().every((tuple_type) => { + if (tuple_type instanceof RestType) { + return uniformRestType(tuple_type, check_type); + } else { + return tuple_type?.getId() === check_type.getId(); + } + })) + ); +} + export class TupleTypeFormatter implements SubTypeFormatter { public constructor(protected childTypeFormatter: TypeFormatter) {} @@ -20,6 +36,7 @@ export class TupleTypeFormatter implements SubTypeFormatter { const requiredElements = subTypes.filter((t) => !(t instanceof OptionalType) && !(t instanceof RestType)); const optionalElements = subTypes.filter((t): t is OptionalType => t instanceof OptionalType); + // NOTE: A maximum of one rest type is assumed. const restType = subTypes.find((t): t is RestType => t instanceof RestType); const firstItemType = requiredElements.length > 0 ? requiredElements[0] : optionalElements[0]?.getType(); @@ -32,7 +49,7 @@ export class TupleTypeFormatter implements SubTypeFormatter { firstItemType && requiredElements.every((item) => item.getId() === firstItemType.getId()) && optionalElements.every((item) => item.getType().getId() === firstItemType.getId()) && - (!restType || restType.getType().getItem().getId() === firstItemType.getId()); + (!restType || uniformRestType(restType, firstItemType)); // If so, generate a simple array with minItems (and possibly maxItems) instead. if (isUniformArray) { diff --git a/src/Utils/isAssignableTo.ts b/src/Utils/isAssignableTo.ts index 9b7d30afa..904a2c095 100644 --- a/src/Utils/isAssignableTo.ts +++ b/src/Utils/isAssignableTo.ts @@ -16,6 +16,8 @@ import { LiteralType, LiteralValue } from "../Type/LiteralType"; import { StringType } from "../Type/StringType"; import { NumberType } from "../Type/NumberType"; import { BooleanType } from "../Type/BooleanType"; +import { InferType } from "../Type/InferType"; +import { RestType } from "../Type/RestType"; /** * Returns the combined types from the given intersection. Currently only object types are combined. Maybe more @@ -80,12 +82,14 @@ function getPrimitiveType(value: LiteralValue) { * * @param source - The source type. * @param target - The target type. + * @param inferMap - Optional parameter that keeps track of the inferred types. * @param insideTypes - Optional parameter used internally to solve circular dependencies. * @return True if source type is assignable to target type. */ export function isAssignableTo( target: BaseType | undefined, source: BaseType | undefined, + inferMap: Map = new Map(), insideTypes: Set = new Set() ): boolean { // Dereference source and target @@ -102,6 +106,20 @@ export function isAssignableTo( return false; } + // Infer type can become anything + if (target instanceof InferType) { + const key = target.getName(); + const infer = inferMap.get(key); + + if (infer === undefined) { + inferMap.set(key, source); + } else { + inferMap.set(key, new UnionType([infer, source])); + } + + return true; + } + // Check for simple type equality if (source.getId() === target.getId()) { return true; @@ -129,22 +147,22 @@ export function isAssignableTo( // Union and enum type is assignable to target when all types in the union/enum are assignable to it if (source instanceof UnionType || source instanceof EnumType) { - return source.getTypes().every((type) => isAssignableTo(target, type, insideTypes)); + return source.getTypes().every((type) => isAssignableTo(target, type, inferMap, insideTypes)); } // When source is an intersection type then it can be assigned to target if any of the sub types matches. Object // types within the intersection must be combined first if (source instanceof IntersectionType) { - return combineIntersectingTypes(source).some((type) => isAssignableTo(target, type, insideTypes)); + return combineIntersectingTypes(source).some((type) => isAssignableTo(target, type, inferMap, insideTypes)); } // For arrays check if item types are assignable if (target instanceof ArrayType) { const targetItemType = target.getItem(); if (source instanceof ArrayType) { - return isAssignableTo(targetItemType, source.getItem(), insideTypes); + return isAssignableTo(targetItemType, source.getItem(), inferMap, insideTypes); } else if (source instanceof TupleType) { - return source.getTypes().every((type) => isAssignableTo(targetItemType, type, insideTypes)); + return isAssignableTo(targetItemType, new UnionType(source.getTypes()), inferMap, insideTypes); } else { return false; } @@ -152,18 +170,18 @@ export function isAssignableTo( // When target is a union or enum type then check if source type can be assigned to any variant if (target instanceof UnionType || target instanceof EnumType) { - return target.getTypes().some((type) => isAssignableTo(type, source, insideTypes)); + return target.getTypes().some((type) => isAssignableTo(type, source, inferMap, insideTypes)); } // When target is an intersection type then source can be assigned to it if it matches all sub types. Object // types within the intersection must be combined first if (target instanceof IntersectionType) { - return combineIntersectingTypes(target).every((type) => isAssignableTo(type, source, insideTypes)); + return combineIntersectingTypes(target).every((type) => isAssignableTo(type, source, inferMap, insideTypes)); } // Check literal types if (source instanceof LiteralType) { - return isAssignableTo(target, getPrimitiveType(source.getValue())); + return isAssignableTo(target, getPrimitiveType(source.getValue()), inferMap); } if (target instanceof ObjectType) { @@ -178,7 +196,7 @@ export function isAssignableTo( const targetMembers = getObjectProperties(target); if (targetMembers.length === 0) { // When target object is empty then anything except null and undefined can be assigned to it - return !isAssignableTo(new UnionType([new UndefinedType(), new NullType()]), source, insideTypes); + return !isAssignableTo(new UnionType([new UndefinedType(), new NullType()]), source, inferMap, insideTypes); } else if (source instanceof ObjectType) { const sourceMembers = getObjectProperties(source); @@ -201,6 +219,7 @@ export function isAssignableTo( return isAssignableTo( targetMember.getType(), sourceMember.getType(), + inferMap, new Set(insideTypes).add(source!).add(target!) ); }) @@ -231,19 +250,60 @@ export function isAssignableTo( if (target instanceof TupleType) { if (source instanceof TupleType) { const sourceMembers = source.getTypes(); - return target.getTypes().every((targetMember, i) => { + const targetMembers = target.getTypes(); + + // TODO: Currently, the final element of the target tuple may be a + // rest type. However, since TypeScript 4.0, a tuple may contain + // multiple rest types at arbitrary locations. + return targetMembers.every((targetMember, i) => { + const numTarget = targetMembers.length; + const numSource = sourceMembers.length; + + if (i == numTarget - 1) { + if (numTarget <= numSource + 1) { + if (targetMember instanceof RestType) { + const remaining: Array = []; + for (let j = i; j < numSource; j++) { + remaining.push(sourceMembers[j]); + } + return isAssignableTo( + targetMember.getType(), + new TupleType(remaining), + inferMap, + insideTypes + ); + } + // The type cannot be assigned if more than one source + // member is remaining and the final target type is not + // a rest type. + else if (numTarget < numSource) { + return false; + } + } + } + const sourceMember = sourceMembers[i]; if (targetMember instanceof OptionalType) { if (sourceMember) { return ( - isAssignableTo(targetMember, sourceMember, insideTypes) || - isAssignableTo(targetMember.getType(), sourceMember, insideTypes) + isAssignableTo(targetMember, sourceMember, inferMap, insideTypes) || + isAssignableTo(targetMember.getType(), sourceMember, inferMap, insideTypes) ); } else { return true; } } else { - return isAssignableTo(targetMember, sourceMember, insideTypes); + // FIXME: This clause is necessary because of the ambiguous + // definition of `undefined`. This function assumes that when + // source=undefined it may always be assigned, as + // `undefined` should refer to `never`. However in this + // case, source may be undefined because numTarget > + // numSource, and this function should return false + // instead. + if (sourceMember === undefined) { + return false; + } + return isAssignableTo(targetMember, sourceMember, inferMap, insideTypes); } }); } diff --git a/test/unit/isAssignableTo.test.ts b/test/unit/isAssignableTo.test.ts index 2b4247427..8efb2190f 100644 --- a/test/unit/isAssignableTo.test.ts +++ b/test/unit/isAssignableTo.test.ts @@ -4,6 +4,7 @@ import { AnyType } from "../../src/Type/AnyType"; import { ArrayType } from "../../src/Type/ArrayType"; import { BooleanType } from "../../src/Type/BooleanType"; import { DefinitionType } from "../../src/Type/DefinitionType"; +import { InferType } from "../../src/Type/InferType"; import { IntersectionType } from "../../src/Type/IntersectionType"; import { LiteralType } from "../../src/Type/LiteralType"; import { NullType } from "../../src/Type/NullType"; @@ -11,6 +12,7 @@ import { NumberType } from "../../src/Type/NumberType"; import { ObjectProperty, ObjectType } from "../../src/Type/ObjectType"; import { OptionalType } from "../../src/Type/OptionalType"; import { ReferenceType } from "../../src/Type/ReferenceType"; +import { RestType } from "../../src/Type/RestType"; import { StringType } from "../../src/Type/StringType"; import { TupleType } from "../../src/Type/TupleType"; import { UndefinedType } from "../../src/Type/UndefinedType"; @@ -304,6 +306,33 @@ describe("isAssignableTo", () => { new TupleType([new StringType(), new StringType()]) ) ).toBe(true); + expect( + isAssignableTo( + new TupleType([new StringType(), new InferType("T")]), + new TupleType([new StringType(), new NumberType(), new StringType()]) + ) + ).toBe(false); + expect( + isAssignableTo( + new TupleType([new StringType(), new InferType("T")]), + new TupleType([new StringType(), new NumberType()]) + ) + ).toBe(true); + expect( + isAssignableTo(new TupleType([new StringType(), new InferType("T")]), new TupleType([new StringType()])) + ).toBe(false); + expect( + isAssignableTo( + new TupleType([new StringType(), new RestType(new InferType("T"))]), + new TupleType([new StringType()]) + ) + ).toBe(true); + expect( + isAssignableTo( + new TupleType([new StringType(), new RestType(new InferType("T"))]), + new TupleType([new StringType(), new NumberType(), new StringType()]) + ) + ).toBe(true); }); it("lets anything except null and undefined to be assigned to empty object type", () => { const empty = new ObjectType("empty", [], [], false); diff --git a/test/valid-data-type.test.ts b/test/valid-data-type.test.ts index b6cc2800c..8818166ee 100644 --- a/test/valid-data-type.test.ts +++ b/test/valid-data-type.test.ts @@ -113,6 +113,17 @@ describe("valid-data-type", () => { it("type-conditional-omit", assertValidSchema("type-conditional-omit", "MyObject")); it("type-conditional-jsdoc", assertValidSchema("type-conditional-jsdoc", "MyObject", "extended")); + it("type-conditional-infer", assertValidSchema("type-conditional-infer", "MyType")); + it("type-conditional-infer-nested", assertValidSchema("type-conditional-infer-nested", "MyType")); + it("type-conditional-infer-recursive", assertValidSchema("type-conditional-infer-recursive", "MyType")); + it("type-conditional-infer-rest", assertValidSchema("type-conditional-infer-rest", "MyType")); + it("type-conditional-infer-tail-recursion", assertValidSchema("type-conditional-infer-tail-recursion", "MyType")); + it("type-conditional-infer-tuple-xor", assertValidSchema("type-conditional-infer-tuple-xor", "MyType")); + + it("type-tuple-nested-rest", assertValidSchema("type-tuple-nested-rest", "MyType")); + it("type-tuple-nested-rest-to-union", assertValidSchema("type-tuple-nested-rest-to-union", "MyType")); + it("type-tuple-nested-rest-uniform", assertValidSchema("type-tuple-nested-rest-uniform", "MyType")); + it("type-recursive-deep-exclude", assertValidSchema("type-recursive-deep-exclude", "MyType")); it("ignore-export", assertValidSchema("ignore-export", "*")); }); diff --git a/test/valid-data/type-conditional-infer-nested/main.ts b/test/valid-data/type-conditional-infer-nested/main.ts new file mode 100644 index 000000000..0073796c3 --- /dev/null +++ b/test/valid-data/type-conditional-infer-nested/main.ts @@ -0,0 +1,3 @@ +type Nested>> = T extends Array ? (A extends Array ? B : never) : never; + +export type MyType = Nested<[[string, number], [boolean]]>; diff --git a/test/valid-data/type-conditional-infer-nested/schema.json b/test/valid-data/type-conditional-infer-nested/schema.json new file mode 100644 index 000000000..12c1a1f79 --- /dev/null +++ b/test/valid-data/type-conditional-infer-nested/schema.json @@ -0,0 +1,13 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "type": [ + "string", + "number", + "boolean" + ] + } + } +} \ No newline at end of file diff --git a/test/valid-data/type-conditional-infer-recursive/main.ts b/test/valid-data/type-conditional-infer-recursive/main.ts new file mode 100644 index 000000000..5531393bf --- /dev/null +++ b/test/valid-data/type-conditional-infer-recursive/main.ts @@ -0,0 +1,3 @@ +type Recursive = T extends Array ? Recursive : T; + +export type MyType = Recursive<[[[string], [number, { a: string }]], [[boolean]]]>; diff --git a/test/valid-data/type-conditional-infer-recursive/schema.json b/test/valid-data/type-conditional-infer-recursive/schema.json new file mode 100644 index 000000000..7d45b9d73 --- /dev/null +++ b/test/valid-data/type-conditional-infer-recursive/schema.json @@ -0,0 +1,31 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "additionalProperties": false, + "properties": { + "a": { + "type": "string" + } + }, + "required": [ + "a" + ], + "type": "object" + }, + { + "type": "boolean" + } + ] + } + } +} \ No newline at end of file diff --git a/test/valid-data/type-conditional-infer-rest/main.ts b/test/valid-data/type-conditional-infer-rest/main.ts new file mode 100644 index 000000000..7270f76eb --- /dev/null +++ b/test/valid-data/type-conditional-infer-rest/main.ts @@ -0,0 +1,3 @@ +type GetRest = T extends [any, ...infer T] ? T : never; + +export type MyType = GetRest<[string, string, number, boolean]>; diff --git a/test/valid-data/type-conditional-infer-rest/schema.json b/test/valid-data/type-conditional-infer-rest/schema.json new file mode 100644 index 000000000..f40adc5a1 --- /dev/null +++ b/test/valid-data/type-conditional-infer-rest/schema.json @@ -0,0 +1,22 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "items": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + } + } +} \ No newline at end of file diff --git a/test/valid-data/type-conditional-infer-tail-recursion/main.ts b/test/valid-data/type-conditional-infer-tail-recursion/main.ts new file mode 100644 index 000000000..4f1cfecfb --- /dev/null +++ b/test/valid-data/type-conditional-infer-tail-recursion/main.ts @@ -0,0 +1,12 @@ +type GetFirst = T extends [infer A, any] ? A : never; +type GetSecond = T extends [any, infer A] ? A : never; + +type Augment = { [K in GetFirst]: GetSecond }; + +type TailRecursion = T extends [infer Head, ...infer Tail] + ? Head extends [string, any] + ? [Augment, ...TailRecursion] + : never + : []; + +export type MyType = TailRecursion<[["a", string], ["b", number], ["c", boolean]]>; diff --git a/test/valid-data/type-conditional-infer-tail-recursion/schema.json b/test/valid-data/type-conditional-infer-tail-recursion/schema.json new file mode 100644 index 000000000..c71b1b763 --- /dev/null +++ b/test/valid-data/type-conditional-infer-tail-recursion/schema.json @@ -0,0 +1,49 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "items": [ + { + "additionalProperties": false, + "properties": { + "a": { + "type": "string" + } + }, + "required": [ + "a" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "b": { + "type": "number" + } + }, + "required": [ + "b" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "c": { + "type": "boolean" + } + }, + "required": [ + "c" + ], + "type": "object" + } + ], + "maxItems": 3, + "minItems": 3, + "type": "array" + } + } +} \ No newline at end of file diff --git a/test/valid-data/type-conditional-infer-tuple-xor/main.ts b/test/valid-data/type-conditional-infer-tuple-xor/main.ts new file mode 100644 index 000000000..aea645ba1 --- /dev/null +++ b/test/valid-data/type-conditional-infer-tuple-xor/main.ts @@ -0,0 +1,13 @@ +type Without = { + [P in Exclude]?: never; +}; + +type XOR = T | U extends object ? (Without & U) | (Without & T) : T | U; + +type TupleXOR = T extends [infer Only] + ? Only + : T extends [infer A, infer B, ...infer Rest] + ? TupleXOR<[XOR, ...Rest]> + : never; + +export type MyType = TupleXOR<[{ a: string }, { b: number }, { c: boolean }]>; diff --git a/test/valid-data/type-conditional-infer-tuple-xor/schema.json b/test/valid-data/type-conditional-infer-tuple-xor/schema.json new file mode 100644 index 000000000..b27e878ab --- /dev/null +++ b/test/valid-data/type-conditional-infer-tuple-xor/schema.json @@ -0,0 +1,64 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "a": { + "not": {} + }, + "b": { + "not": {} + }, + "c": { + "type": "boolean" + } + }, + "required": [ + "c" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "a": { + "not": {} + }, + "b": { + "type": "number" + }, + "c": { + "not": {} + } + }, + "required": [ + "b" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "a": { + "type": "string" + }, + "b": { + "not": {} + }, + "c": { + "not": {} + } + }, + "required": [ + "a" + ], + "type": "object" + } + ] + } + } +} \ No newline at end of file diff --git a/test/valid-data/type-conditional-infer/main.ts b/test/valid-data/type-conditional-infer/main.ts new file mode 100644 index 000000000..f64b475ef --- /dev/null +++ b/test/valid-data/type-conditional-infer/main.ts @@ -0,0 +1,3 @@ +type InferObject = T extends { a: infer A } ? A : never; + +export type MyType = InferObject<{ a: string }>; diff --git a/test/valid-data/type-conditional-infer/schema.json b/test/valid-data/type-conditional-infer/schema.json new file mode 100644 index 000000000..54f859a5c --- /dev/null +++ b/test/valid-data/type-conditional-infer/schema.json @@ -0,0 +1,9 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "type": "string" + } + } +} \ No newline at end of file diff --git a/test/valid-data/type-tuple-nested-rest-to-union/main.ts b/test/valid-data/type-tuple-nested-rest-to-union/main.ts new file mode 100644 index 000000000..42b45c9cd --- /dev/null +++ b/test/valid-data/type-tuple-nested-rest-to-union/main.ts @@ -0,0 +1,5 @@ +type NestedTuple = [{ a: number }, ...[{ b: string }, { c: number }, ...[{ d: boolean }, ...[]]]]; + +type ToUnion = T extends Array ? A : never; + +export type MyType = ToUnion; diff --git a/test/valid-data/type-tuple-nested-rest-to-union/schema.json b/test/valid-data/type-tuple-nested-rest-to-union/schema.json new file mode 100644 index 000000000..689bc2a9d --- /dev/null +++ b/test/valid-data/type-tuple-nested-rest-to-union/schema.json @@ -0,0 +1,58 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "a": { + "type": "number" + } + }, + "required": [ + "a" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "b": { + "type": "string" + } + }, + "required": [ + "b" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "c": { + "type": "number" + } + }, + "required": [ + "c" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "d": { + "type": "boolean" + } + }, + "required": [ + "d" + ], + "type": "object" + } + ] + } + } +} \ No newline at end of file diff --git a/test/valid-data/type-tuple-nested-rest-uniform/main.ts b/test/valid-data/type-tuple-nested-rest-uniform/main.ts new file mode 100644 index 000000000..99a6ba1b3 --- /dev/null +++ b/test/valid-data/type-tuple-nested-rest-uniform/main.ts @@ -0,0 +1 @@ +export type MyType = [number, ...[number, number, ...[number, ...[]]]]; diff --git a/test/valid-data/type-tuple-nested-rest-uniform/schema.json b/test/valid-data/type-tuple-nested-rest-uniform/schema.json new file mode 100644 index 000000000..9e45bc34c --- /dev/null +++ b/test/valid-data/type-tuple-nested-rest-uniform/schema.json @@ -0,0 +1,14 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "items": { + "type": "number" + }, + "maxItems": 4, + "minItems": 4, + "type": "array" + } + } +} \ No newline at end of file diff --git a/test/valid-data/type-tuple-nested-rest/main.ts b/test/valid-data/type-tuple-nested-rest/main.ts new file mode 100644 index 000000000..fac5772ba --- /dev/null +++ b/test/valid-data/type-tuple-nested-rest/main.ts @@ -0,0 +1 @@ +export type MyType = [{ a: number }, ...[{ b: string }, { c: number }, ...[{ d: boolean }, ...[]]]]; diff --git a/test/valid-data/type-tuple-nested-rest/schema.json b/test/valid-data/type-tuple-nested-rest/schema.json new file mode 100644 index 000000000..5ff273c06 --- /dev/null +++ b/test/valid-data/type-tuple-nested-rest/schema.json @@ -0,0 +1,61 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "items": [ + { + "additionalProperties": false, + "properties": { + "a": { + "type": "number" + } + }, + "required": [ + "a" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "b": { + "type": "string" + } + }, + "required": [ + "b" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "c": { + "type": "number" + } + }, + "required": [ + "c" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "d": { + "type": "boolean" + } + }, + "required": [ + "d" + ], + "type": "object" + } + ], + "maxItems": 4, + "minItems": 4, + "type": "array" + } + } +} \ No newline at end of file