diff --git a/factory/formatter.ts b/factory/formatter.ts index faaa3f589..c288616fa 100644 --- a/factory/formatter.ts +++ b/factory/formatter.ts @@ -12,6 +12,7 @@ import { EnumTypeFormatter } from "../src/TypeFormatter/EnumTypeFormatter"; import { IntersectionTypeFormatter } from "../src/TypeFormatter/IntersectionTypeFormatter"; import { LiteralTypeFormatter } from "../src/TypeFormatter/LiteralTypeFormatter"; import { LiteralUnionTypeFormatter } from "../src/TypeFormatter/LiteralUnionTypeFormatter"; +import { NeverTypeFormatter } from "../src/TypeFormatter/NeverTypeFormatter"; import { NullTypeFormatter } from "../src/TypeFormatter/NullTypeFormatter"; import { NumberTypeFormatter } from "../src/TypeFormatter/NumberTypeFormatter"; import { ObjectTypeFormatter } from "../src/TypeFormatter/ObjectTypeFormatter"; @@ -23,6 +24,7 @@ import { StringTypeFormatter } from "../src/TypeFormatter/StringTypeFormatter"; import { TupleTypeFormatter } from "../src/TypeFormatter/TupleTypeFormatter"; import { UndefinedTypeFormatter } from "../src/TypeFormatter/UndefinedTypeFormatter"; import { UnionTypeFormatter } from "../src/TypeFormatter/UnionTypeFormatter"; +import { UnknownTypeFormatter } from "../src/TypeFormatter/UnknownTypeFormatter"; @@ -39,7 +41,9 @@ export function createFormatter(config: Config): TypeFormatter { .addTypeFormatter(new NullTypeFormatter()) .addTypeFormatter(new AnyTypeFormatter()) + .addTypeFormatter(new NeverTypeFormatter()) .addTypeFormatter(new UndefinedTypeFormatter()) + .addTypeFormatter(new UnknownTypeFormatter()) .addTypeFormatter(new LiteralTypeFormatter()) .addTypeFormatter(new EnumTypeFormatter()) diff --git a/factory/parser.ts b/factory/parser.ts index df5e95f1d..5e6eab7d8 100644 --- a/factory/parser.ts +++ b/factory/parser.ts @@ -12,6 +12,7 @@ import { ArrayNodeParser } from "../src/NodeParser/ArrayNodeParser"; import { BooleanLiteralNodeParser } from "../src/NodeParser/BooleanLiteralNodeParser"; import { BooleanTypeNodeParser } from "../src/NodeParser/BooleanTypeNodeParser"; import { CallExpressionParser } from "../src/NodeParser/CallExpressionParser"; +import { ConditionalTypeNodeParser } from "../src/NodeParser/ConditionalTypeNodeParser"; import { EnumNodeParser } from "../src/NodeParser/EnumNodeParser"; import { ExpressionWithTypeArgumentsNodeParser } from "../src/NodeParser/ExpressionWithTypeArgumentsNodeParser"; import { IndexedAccessTypeNodeParser } from "../src/NodeParser/IndexedAccessTypeNodeParser"; @@ -19,6 +20,7 @@ import { InterfaceAndClassNodeParser } from "../src/NodeParser/InterfaceAndClass import { IntersectionNodeParser } from "../src/NodeParser/IntersectionNodeParser"; import { LiteralNodeParser } from "../src/NodeParser/LiteralNodeParser"; import { MappedTypeNodeParser } from "../src/NodeParser/MappedTypeNodeParser"; +import { NeverTypeNodeParser } from "../src/NodeParser/NeverTypeNodeParser"; import { NullLiteralNodeParser } from "../src/NodeParser/NullLiteralNodeParser"; import { NumberLiteralNodeParser } from "../src/NodeParser/NumberLiteralNodeParser"; import { NumberTypeNodeParser } from "../src/NodeParser/NumberTypeNodeParser"; @@ -37,11 +39,11 @@ import { TypeOperatorNodeParser } from "../src/NodeParser/TypeOperatorNodeParser import { TypeReferenceNodeParser } from "../src/NodeParser/TypeReferenceNodeParser"; import { UndefinedTypeNodeParser } from "../src/NodeParser/UndefinedTypeNodeParser"; import { UnionNodeParser } from "../src/NodeParser/UnionNodeParser"; +import { UnknownTypeNodeParser } from "../src/NodeParser/UnknownTypeNodeParser"; import { SubNodeParser } from "../src/SubNodeParser"; import { TopRefNodeParser } from "../src/TopRefNodeParser"; import { FunctionNodeParser } from "./../src/NodeParser/FunctionNodeParser"; - export function createParser(program: ts.Program, config: Config): NodeParser { const typeChecker = program.getTypeChecker(); const chainNodeParser = new ChainNodeParser(typeChecker, []); @@ -72,7 +74,9 @@ export function createParser(program: ts.Program, config: Config): NodeParser { .addNodeParser(new NumberTypeNodeParser()) .addNodeParser(new BooleanTypeNodeParser()) .addNodeParser(new AnyTypeNodeParser()) + .addNodeParser(new UnknownTypeNodeParser()) .addNodeParser(new UndefinedTypeNodeParser()) + .addNodeParser(new NeverTypeNodeParser()) .addNodeParser(new ObjectTypeNodeParser()) .addNodeParser(new StringLiteralNodeParser()) @@ -92,6 +96,7 @@ export function createParser(program: ts.Program, config: Config): NodeParser { .addNodeParser(new IndexedAccessTypeNodeParser(chainNodeParser)) .addNodeParser(new TypeofNodeParser(typeChecker, chainNodeParser)) .addNodeParser(new MappedTypeNodeParser(chainNodeParser)) + .addNodeParser(new ConditionalTypeNodeParser(typeChecker, chainNodeParser)) .addNodeParser(new TypeOperatorNodeParser(chainNodeParser)) .addNodeParser(new UnionNodeParser(typeChecker, chainNodeParser)) diff --git a/index.ts b/index.ts index 1431d5071..9b654497d 100644 --- a/index.ts +++ b/index.ts @@ -45,8 +45,10 @@ export * from "./src/SubTypeFormatter"; export * from "./src/ChainTypeFormatter"; export * from "./src/CircularReferenceTypeFormatter"; export * from "./src/TypeFormatter/AnyTypeFormatter"; +export * from "./src/TypeFormatter/UnknownTypeFormatter"; export * from "./src/TypeFormatter/NullTypeFormatter"; export * from "./src/TypeFormatter/UndefinedTypeFormatter"; +export * from "./src/TypeFormatter/NeverTypeFormatter"; export * from "./src/TypeFormatter/BooleanTypeFormatter"; export * from "./src/TypeFormatter/NumberTypeFormatter"; export * from "./src/TypeFormatter/StringTypeFormatter"; @@ -71,9 +73,11 @@ export * from "./src/ExposeNodeParser"; export * from "./src/TopRefNodeParser"; export * from "./src/CircularReferenceNodeParser"; export * from "./src/NodeParser/AnyTypeNodeParser"; +export * from "./src/NodeParser/UnknownTypeNodeParser"; export * from "./src/NodeParser/LiteralNodeParser"; export * from "./src/NodeParser/NullLiteralNodeParser"; export * from "./src/NodeParser/UndefinedTypeNodeParser"; +export * from "./src/NodeParser/NeverTypeNodeParser"; export * from "./src/NodeParser/NumberLiteralNodeParser"; export * from "./src/NodeParser/StringLiteralNodeParser"; export * from "./src/NodeParser/BooleanLiteralNodeParser"; @@ -93,6 +97,7 @@ export * from "./src/NodeParser/UnionNodeParser"; export * from "./src/NodeParser/TupleNodeParser"; export * from "./src/NodeParser/AnnotatedNodeParser"; export * from "./src/NodeParser/CallExpressionParser"; +export * from "./src/NodeParser/ConditionalTypeNodeParser"; export * from "./src/NodeParser/PrefixUnaryExpressionNodeParser"; export * from "./src/SchemaGenerator"; diff --git a/src/NodeParser/AnnotatedNodeParser.ts b/src/NodeParser/AnnotatedNodeParser.ts index b44818e37..b12c9beb2 100644 --- a/src/NodeParser/AnnotatedNodeParser.ts +++ b/src/NodeParser/AnnotatedNodeParser.ts @@ -20,6 +20,9 @@ export class AnnotatedNodeParser implements SubNodeParser { public createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType { const baseType = this.childNodeParser.createType(node, context, reference); + if (node.getSourceFile().fileName.match(/[\/\\]typescript[\/\\]lib[\/\\]lib\.[^/\\]+\.d\.ts$/i)) { + return baseType; + } const annotatedNode = this.getAnnotatedNode(node); const annotations = this.annotationsReader.getAnnotations(annotatedNode); const nullable = this.annotationsReader instanceof ExtendedAnnotationsReader ? diff --git a/src/NodeParser/AnyTypeNodeParser.ts b/src/NodeParser/AnyTypeNodeParser.ts index f15707304..d99327743 100644 --- a/src/NodeParser/AnyTypeNodeParser.ts +++ b/src/NodeParser/AnyTypeNodeParser.ts @@ -6,7 +6,7 @@ import { BaseType } from "../Type/BaseType"; export class AnyTypeNodeParser implements SubNodeParser { public supportsNode(node: ts.KeywordTypeNode): boolean { - return node.kind === ts.SyntaxKind.AnyKeyword || node.kind === ts.SyntaxKind.UnknownKeyword; + return node.kind === ts.SyntaxKind.AnyKeyword; } public createType(node: ts.KeywordTypeNode, context: Context): BaseType { return new AnyType(); diff --git a/src/NodeParser/ConditionalTypeNodeParser.ts b/src/NodeParser/ConditionalTypeNodeParser.ts new file mode 100644 index 000000000..b2c0095ad --- /dev/null +++ b/src/NodeParser/ConditionalTypeNodeParser.ts @@ -0,0 +1,50 @@ +import * as ts from "typescript"; +import { Context, NodeParser } from "../NodeParser"; +import { SubNodeParser } from "../SubNodeParser"; +import { BaseType } from "../Type/BaseType"; +import { isAssignableTo } from "../Utils/isAssignableTo"; +import { narrowType } from "../Utils/narrowType"; + +export class ConditionalTypeNodeParser implements SubNodeParser { + public constructor( + private typeChecker: ts.TypeChecker, + private childNodeParser: NodeParser, + ) {} + + public supportsNode(node: ts.ConditionalTypeNode): boolean { + return node.kind === ts.SyntaxKind.ConditionalType; + } + + public createType(node: ts.ConditionalTypeNode, context: Context): BaseType { + const checkType = this.childNodeParser.createType(node.checkType, context); + const extendsType = this.childNodeParser.createType(node.extendsType, context); + const result = isAssignableTo(extendsType, checkType); + const tsResultType = result ? node.trueType : node.falseType; + const resultType = this.childNodeParser.createType(tsResultType, context); + + // If result type is the same type parameter as the check type then narrow down the result type + const checkTypeParameterName = this.getTypeParameterName(node.checkType); + const resultTypeParameterName = this.getTypeParameterName(tsResultType); + if (resultTypeParameterName != null && resultTypeParameterName === checkTypeParameterName) { + return narrowType(resultType, type => isAssignableTo(extendsType, type) === result); + } + + return resultType; + } + + /** + * Returns the type parameter name of the given type node if any. + * + * @param node - The type node for which to return the type parameter name. + * @return The type parameter name or null if specified type node is not a type parameter. + */ + private getTypeParameterName(node: ts.TypeNode): string | null { + if (ts.isTypeReferenceNode(node)) { + const typeSymbol = this.typeChecker.getSymbolAtLocation(node.typeName)!; + if (typeSymbol.flags & ts.SymbolFlags.TypeParameter) { + return typeSymbol.name; + } + } + return null; + } +} diff --git a/src/NodeParser/NeverTypeNodeParser.ts b/src/NodeParser/NeverTypeNodeParser.ts new file mode 100644 index 000000000..dea2016c0 --- /dev/null +++ b/src/NodeParser/NeverTypeNodeParser.ts @@ -0,0 +1,14 @@ +import * as ts from "typescript"; +import { Context } from "../NodeParser"; +import { SubNodeParser } from "../SubNodeParser"; +import { BaseType } from "../Type/BaseType"; +import { NeverType } from "../Type/NeverType"; + +export class NeverTypeNodeParser implements SubNodeParser { + public supportsNode(node: ts.KeywordTypeNode): boolean { + return node.kind === ts.SyntaxKind.NeverKeyword; + } + public createType(node: ts.KeywordTypeNode, context: Context): BaseType { + return new NeverType(); + } +} diff --git a/src/NodeParser/UnknownTypeNodeParser.ts b/src/NodeParser/UnknownTypeNodeParser.ts new file mode 100644 index 000000000..1dd97be2a --- /dev/null +++ b/src/NodeParser/UnknownTypeNodeParser.ts @@ -0,0 +1,14 @@ +import * as ts from "typescript"; +import { Context } from "../NodeParser"; +import { SubNodeParser } from "../SubNodeParser"; +import { BaseType } from "../Type/BaseType"; +import { UnknownType } from "../Type/UnknownType"; + +export class UnknownTypeNodeParser implements SubNodeParser { + public supportsNode(node: ts.KeywordTypeNode): boolean { + return node.kind === ts.SyntaxKind.UnknownKeyword; + } + public createType(node: ts.KeywordTypeNode, context: Context): BaseType { + return new UnknownType(); + } +} diff --git a/src/Type/EnumType.ts b/src/Type/EnumType.ts index e6083f862..1c21e8a9b 100644 --- a/src/Type/EnumType.ts +++ b/src/Type/EnumType.ts @@ -1,13 +1,18 @@ import { BaseType } from "./BaseType"; +import { LiteralType } from "./LiteralType"; +import { NullType } from "./NullType"; export type EnumValue = string|boolean|number|null; export class EnumType extends BaseType { + private types: BaseType[]; + public constructor( private id: string, private values: EnumValue[], ) { super(); + this.types = values.map(value => value == null ? new NullType() : new LiteralType(value)); } public getId(): string { @@ -17,4 +22,8 @@ export class EnumType extends BaseType { public getValues(): EnumValue[] { return this.values; } + + public getTypes(): BaseType[] { + return this.types; + } } diff --git a/src/Type/NeverType.ts b/src/Type/NeverType.ts new file mode 100644 index 000000000..f2f8c7e78 --- /dev/null +++ b/src/Type/NeverType.ts @@ -0,0 +1,7 @@ +import { BaseType } from "./BaseType"; + +export class NeverType extends BaseType { + public getId(): string { + return "never"; + } +} diff --git a/src/Type/UnknownType.ts b/src/Type/UnknownType.ts new file mode 100644 index 000000000..24967695b --- /dev/null +++ b/src/Type/UnknownType.ts @@ -0,0 +1,7 @@ +import { BaseType } from "./BaseType"; + +export class UnknownType extends BaseType { + public getId(): string { + return "unknown"; + } +} diff --git a/src/TypeFormatter/NeverTypeFormatter.ts b/src/TypeFormatter/NeverTypeFormatter.ts new file mode 100644 index 000000000..3b5e13cf9 --- /dev/null +++ b/src/TypeFormatter/NeverTypeFormatter.ts @@ -0,0 +1,16 @@ +import { Definition } from "../Schema/Definition"; +import { SubTypeFormatter } from "../SubTypeFormatter"; +import { BaseType } from "../Type/BaseType"; +import { NeverType } from "../Type/NeverType"; + +export class NeverTypeFormatter implements SubTypeFormatter { + public supportsType(type: NeverType): boolean { + return type instanceof NeverType; + } + public getDefinition(type: NeverType): Definition { + return {not: {}}; + } + public getChildren(type: NeverType): BaseType[] { + return []; + } +} diff --git a/src/TypeFormatter/ObjectTypeFormatter.ts b/src/TypeFormatter/ObjectTypeFormatter.ts index 19cdae0c1..c40349a7a 100644 --- a/src/TypeFormatter/ObjectTypeFormatter.ts +++ b/src/TypeFormatter/ObjectTypeFormatter.ts @@ -7,6 +7,7 @@ import { UndefinedType } from "../Type/UndefinedType"; import { UnionType } from "../Type/UnionType"; import { TypeFormatter } from "../TypeFormatter"; import { getAllOfDefinitionReducer } from "../Utils/allOfDefinition"; +import { derefType } from "../Utils/derefType"; import { StringMap } from "../Utils/StringMap"; export class ObjectTypeFormatter implements SubTypeFormatter { @@ -75,7 +76,7 @@ export class ObjectTypeFormatter implements SubTypeFormatter { } private prepareObjectProperty(property: ObjectProperty): ObjectProperty { - const propType = property.getType(); + const propType = derefType(property.getType()); if (propType instanceof UndefinedType) { return new ObjectProperty(property.getName(), new UndefinedType(), false); } else if (!(propType instanceof UnionType)) { diff --git a/src/TypeFormatter/UnknownTypeFormatter.ts b/src/TypeFormatter/UnknownTypeFormatter.ts new file mode 100644 index 000000000..444b9f00b --- /dev/null +++ b/src/TypeFormatter/UnknownTypeFormatter.ts @@ -0,0 +1,16 @@ +import { Definition } from "../Schema/Definition"; +import { SubTypeFormatter } from "../SubTypeFormatter"; +import { BaseType } from "../Type/BaseType"; +import { UnknownType } from "../Type/UnknownType"; + +export class UnknownTypeFormatter implements SubTypeFormatter { + public supportsType(type: UnknownType): boolean { + return type instanceof UnknownType; + } + public getDefinition(type: UnknownType): Definition { + return {}; + } + public getChildren(type: UnknownType): BaseType[] { + return []; + } +} diff --git a/src/Utils/isAssignableTo.ts b/src/Utils/isAssignableTo.ts new file mode 100644 index 000000000..7130b0b72 --- /dev/null +++ b/src/Utils/isAssignableTo.ts @@ -0,0 +1,190 @@ +import { AnyType } from "../Type/AnyType"; +import { ArrayType } from "../Type/ArrayType"; +import { BaseType } from "../Type/BaseType"; +import { EnumType } from "../Type/EnumType"; +import { IntersectionType } from "../Type/IntersectionType"; +import { LiteralType } from "../Type/LiteralType"; +import { NeverType } from "../Type/NeverType"; +import { NullType } from "../Type/NullType"; +import { ObjectProperty, ObjectType } from "../Type/ObjectType"; +import { OptionalType } from "../Type/OptionalType"; +import { TupleType } from "../Type/TupleType"; +import { UndefinedType } from "../Type/UndefinedType"; +import { UnionType } from "../Type/UnionType"; +import { UnknownType } from "../Type/UnknownType"; +import { derefType } from "./derefType"; +import { uniqueArray } from "./uniqueArray"; + +/** + * Returns the combined types from the given intersection. Currently only object types are combined. Maybe more + * types needs to be combined to properly support complex intersections. + * + * @param intersection - The intersection type to combine. + * @return The combined types within the intersection. + */ +function combineIntersectingTypes(intersection: IntersectionType): BaseType[] { + const objectTypes: ObjectType[] = []; + const combined = intersection.getTypes().filter(type => { + if (type instanceof ObjectType) { + objectTypes.push(type); + } else { + return true; + } + return false; + }); + if (objectTypes.length === 1) { + combined.push(objectTypes[0]); + } else if (objectTypes.length > 1) { + combined.push(new ObjectType("combined-objects-" + intersection.getId(), objectTypes, [], false)); + } + return combined; +} + +/** + * Returns all object properties of the given type and all its base types. + * + * @param type - The type for which to return the properties. If type is not an object type or object has no properties + * Then an empty list ist returned. + * @return All object properties of the type. Empty if none. + */ +function getObjectProperties(type: BaseType): ObjectProperty[] { + type = derefType(type); + const properties = []; + if (type instanceof ObjectType) { + properties.push(...type.getProperties()); + for (const baseType of type.getBaseTypes()) { + properties.push(...getObjectProperties(baseType)); + } + } + return properties; +} + +/** + * Checks if given source type is assignable to given target type. + * + * The logic of this function is heavily inspired by + * https://github.com/runem/ts-simple-type/blob/master/src/is-assignable-to-simple-type.ts + * + * @param source - The source type. + * @param target - The target type. + * @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, source: BaseType, insideTypes: Set = new Set()): boolean { + // Dereference source and target + source = derefType(source); + target = derefType(target); + + // Check for simple type equality + if (source.getId() === target.getId()) { + return true; + } + + /** Don't check types when already inside them. This solves circular dependencies. */ + if (insideTypes.has(source) || insideTypes.has(target)) { + return true; + } + + // Nothing can be assigned to never-type + if (target instanceof NeverType) { + return false; + } + + // Assigning from or to any-type is always possible + if (source instanceof AnyType || target instanceof AnyType) { + return true; + } + + // assigning to unknown type is always possible + if (target instanceof UnknownType) { + return true; + } + + // Type "never" can be assigned to anything + if (source instanceof NeverType) { + return true; + } + + // 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)); + } + + // 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)); + } + + // 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); + } else if (source instanceof TupleType) { + return source.getTypes().every(type => isAssignableTo(targetItemType, type, insideTypes)); + } else { + return false; + } + } + + // 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)); + } + + // 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)); + } + + if (target instanceof ObjectType) { + 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); + } else if (source instanceof ObjectType) { + const sourceMembers = getObjectProperties(source); + + // Check if target has properties in common with source + const inCommon = targetMembers.some(targetMember => sourceMembers.some(sourceMember => + targetMember.getName() === sourceMember.getName())); + + return targetMembers.every(targetMember => { + // Make sure that every required property in target type is present + const sourceMember = sourceMembers.find(member => targetMember.getName() === member.getName()); + return sourceMember == null ? (inCommon && !targetMember.isRequired()) : true; + }) && sourceMembers.every(sourceMember => { + const targetMember = targetMembers.find(member => member.getName() === sourceMember.getName()); + if (targetMember == null) { + return true; + } + return isAssignableTo(targetMember.getType(), sourceMember.getType(), + new Set(insideTypes).add(source).add(target)); + }); + } + } + + // Check if tuple types are compatible + if (target instanceof TupleType) { + if (source instanceof TupleType) { + const sourceMembers = source.getTypes(); + return target.getTypes().every((targetMember, i) => { + const sourceMember = sourceMembers[i]; + if (targetMember instanceof OptionalType) { + if (sourceMember) { + return isAssignableTo(targetMember, sourceMember, insideTypes) || + isAssignableTo(targetMember.getType(), sourceMember, insideTypes); + } else { + return true; + } + } else { + return isAssignableTo(targetMember, sourceMember, insideTypes); + } + }); + } + } + + return false; +} diff --git a/src/Utils/narrowType.ts b/src/Utils/narrowType.ts new file mode 100644 index 000000000..07e64c1c1 --- /dev/null +++ b/src/Utils/narrowType.ts @@ -0,0 +1,55 @@ +import { BaseType } from "../Type/BaseType"; +import { EnumType } from "../Type/EnumType"; +import { NeverType } from "../Type/NeverType"; +import { UnionType } from "../Type/UnionType"; +import { derefType } from "./derefType"; + +/** + * Narrows the given type by passing all variants to the given predicate function. So when type is a union type then + * the predicate function is called for each type within the union and only the types for which this function returns + * true will remain in the returned type. Union types with only one sub type left are replaced by this one-and-only + * type. Empty union types are removed completely. Definition types are kept if possible. When in the end none of + * the type candidates match the predicate then NeverType is returned. + * + * @param type - The type to narrow down. + * @param predicate - The predicate function to filter the type variants. If it returns true then the type variant is + * kept, when returning false it is removed. + * @return The narrowed down type. + */ +export function narrowType(type: BaseType, predicate: (type: BaseType) => boolean): BaseType { + const derefed = derefType(type); + if (derefed instanceof UnionType || derefed instanceof EnumType) { + let changed = false; + const types: BaseType[] = []; + for (const sub of derefed.getTypes()) { + const derefedSub = derefType(sub); + + // Recursively narrow down all types within the union + const narrowed = narrowType(derefedSub, predicate); + if (!(narrowed instanceof NeverType)) { + if (narrowed === derefedSub) { + types.push(sub); + } else { + types.push(narrowed); + changed = true; + } + } else { + changed = true; + } + } + + // When union types were changed then return new narrowed-down type, otherwise return the original one to + // keep definitions + if (changed) { + if (types.length === 0) { + return new NeverType(); + } else if (types.length === 1) { + return types[0]; + } else { + return new UnionType(types); + } + } + return type; + } + return predicate(derefed) ? type : new NeverType(); +} diff --git a/src/Utils/typeKeys.ts b/src/Utils/typeKeys.ts index b5f19399a..bd8c26597 100644 --- a/src/Utils/typeKeys.ts +++ b/src/Utils/typeKeys.ts @@ -72,9 +72,14 @@ export function getTypeByKey(type: BaseType, index: LiteralType | StringType): B const property = type.getProperties().find((it) => it.getName() === index.getValue()); if (property) { const propertyType = property.getType(); - if (!property.isRequired() && !(propertyType instanceof UnionType && - propertyType.getTypes().some(subType => subType instanceof UndefinedType))) { - return new UnionType([propertyType, new UndefinedType() ]); + if (!property.isRequired()) { + if (propertyType instanceof UnionType) { + if (!propertyType.getTypes().some(subType => subType instanceof UndefinedType)) { + return new UnionType([ ...propertyType.getTypes(), new UndefinedType() ]); + } + } else { + return new UnionType([ propertyType, new UndefinedType() ]); + } } return propertyType; } diff --git a/test/unit/isAssignableTo.test.ts b/test/unit/isAssignableTo.test.ts new file mode 100644 index 000000000..5fec4aedc --- /dev/null +++ b/test/unit/isAssignableTo.test.ts @@ -0,0 +1,326 @@ +import { AliasType } from "../../src/Type/AliasType"; +import { AnnotatedType } from "../../src/Type/AnnotatedType"; +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 { IntersectionType } from "../../src/Type/IntersectionType"; +import { LiteralType } from "../../src/Type/LiteralType"; +import { NeverType } from "../../src/Type/NeverType"; +import { NullType } from "../../src/Type/NullType"; +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 { StringType } from "../../src/Type/StringType"; +import { TupleType } from "../../src/Type/TupleType"; +import { UndefinedType } from "../../src/Type/UndefinedType"; +import { UnionType } from "../../src/Type/UnionType"; +import { UnknownType } from "../../src/Type/UnknownType"; +import { isAssignableTo } from "../../src/Utils/isAssignableTo"; + +describe("isAssignableTo", () => { + it("returns true for same types", () => { + expect(isAssignableTo(new BooleanType(), new BooleanType())).toBe(true); + expect(isAssignableTo(new NullType(), new NullType())).toBe(true); + expect(isAssignableTo(new NumberType(), new NumberType())).toBe(true); + expect(isAssignableTo(new BooleanType(), new BooleanType())).toBe(true); + expect(isAssignableTo(new StringType(), new StringType())).toBe(true); + expect(isAssignableTo(new UndefinedType(), new UndefinedType())).toBe(true); + }); + it("returns false for different types", () => { + expect(isAssignableTo(new BooleanType(), new NullType())).toBe(false); + expect(isAssignableTo(new NullType(), new NumberType())).toBe(false); + expect(isAssignableTo(new NumberType(), new BooleanType())).toBe(false); + expect(isAssignableTo(new BooleanType(), new StringType())).toBe(false); + expect(isAssignableTo(new StringType(), new UndefinedType())).toBe(false); + expect(isAssignableTo(new UndefinedType(), new BooleanType())).toBe(false); + expect(isAssignableTo(new ArrayType(new StringType()), new StringType())).toBe(false); + }); + it("returns true for arrays with same item type", () => { + expect(isAssignableTo(new ArrayType(new StringType()), new ArrayType(new StringType()))).toBe(true); + }); + it("returns false when array item types do not match", () => { + expect(isAssignableTo(new ArrayType(new StringType()), new ArrayType(new NumberType()))).toBe(false); + }); + it("returns true when source type is compatible to target union type", () => { + const union = new UnionType([ + new StringType(), + new NumberType(), + ]); + expect(isAssignableTo(union, new StringType())).toBe(true); + expect(isAssignableTo(union, new NumberType())).toBe(true); + }); + it("returns false when source type is not compatible to target union type", () => { + const union = new UnionType([ + new StringType(), + new NumberType(), + ]); + expect(isAssignableTo(union, new BooleanType())).toBe(false); + }); + it("derefs reference types", () => { + const stringRef = new ReferenceType(); + stringRef.setType(new StringType()); + const anotherStringRef = new ReferenceType(); + anotherStringRef.setType(new StringType()); + const numberRef = new ReferenceType(); + numberRef.setType(new NumberType()); + expect(isAssignableTo(stringRef, new StringType())).toBe(true); + expect(isAssignableTo(stringRef, new NumberType())).toBe(false); + expect(isAssignableTo(new StringType(), stringRef)).toBe(true); + expect(isAssignableTo(new NumberType(), stringRef)).toBe(false); + expect(isAssignableTo(stringRef, anotherStringRef)).toBe(true); + expect(isAssignableTo(numberRef, stringRef)).toBe(false); + }); + it("derefs alias types", () => { + const stringAlias = new AliasType("a", new StringType()); + const anotherStringAlias = new AliasType("b", new StringType()); + const numberAlias = new AliasType("c", new NumberType()); + expect(isAssignableTo(stringAlias, new StringType())).toBe(true); + expect(isAssignableTo(stringAlias, new NumberType())).toBe(false); + expect(isAssignableTo(new StringType(), stringAlias)).toBe(true); + expect(isAssignableTo(new NumberType(), stringAlias)).toBe(false); + expect(isAssignableTo(stringAlias, anotherStringAlias)).toBe(true); + expect(isAssignableTo(numberAlias, stringAlias)).toBe(false); + }); + it("derefs annotated types", () => { + const annotatedString = new AnnotatedType(new StringType(), {}, false); + const anotherAnnotatedString = new AnnotatedType(new StringType(), {}, false); + const annotatedNumber = new AnnotatedType(new NumberType(), {}, false); + expect(isAssignableTo(annotatedString, new StringType())).toBe(true); + expect(isAssignableTo(annotatedString, new NumberType())).toBe(false); + expect(isAssignableTo(new StringType(), annotatedString)).toBe(true); + expect(isAssignableTo(new NumberType(), annotatedString)).toBe(false); + expect(isAssignableTo(annotatedString, anotherAnnotatedString)).toBe(true); + expect(isAssignableTo(annotatedNumber, annotatedString)).toBe(false); + }); + it("derefs definition types", () => { + const stringDefinition = new DefinitionType("a", new StringType()); + const anotherStringDefinition = new DefinitionType("b", new StringType()); + const numberDefinition = new DefinitionType("c", new NumberType()); + expect(isAssignableTo(stringDefinition, new StringType())).toBe(true); + expect(isAssignableTo(stringDefinition, new NumberType())).toBe(false); + expect(isAssignableTo(new StringType(), stringDefinition)).toBe(true); + expect(isAssignableTo(new NumberType(), stringDefinition)).toBe(false); + expect(isAssignableTo(stringDefinition, anotherStringDefinition)).toBe(true); + expect(isAssignableTo(numberDefinition, stringDefinition)).toBe(false); + }); + it("lets type 'any' to be assigned to anything except 'never'", () => { + expect(isAssignableTo(new AnyType(), new AnyType())).toBe(true); + expect(isAssignableTo(new ArrayType(new NumberType()), new AnyType())).toBe(true); + expect(isAssignableTo(new IntersectionType([ new StringType(), new NullType() ]), new AnyType())).toBe(true); + expect(isAssignableTo(new LiteralType("literal"), new AnyType())).toBe(true); + expect(isAssignableTo(new NeverType(), new AnyType())).toBe(false); + expect(isAssignableTo(new NullType(), new AnyType())).toBe(true); + expect(isAssignableTo(new ObjectType("obj", [], [ new ObjectProperty("foo", new StringType(), true) ], true), + new AnyType())).toBe(true); + expect(isAssignableTo(new BooleanType(), new AnyType())).toBe(true); + expect(isAssignableTo(new NumberType(), new AnyType())).toBe(true); + expect(isAssignableTo(new BooleanType(), new AnyType())).toBe(true); + expect(isAssignableTo(new StringType(), new AnyType())).toBe(true); + expect(isAssignableTo(new TupleType([new StringType(), new NumberType() ]), new AnyType())).toBe(true); + expect(isAssignableTo(new UndefinedType(), new AnyType())).toBe(true); + }); + it("lets type 'never' to be assigned to anything", () => { + expect(isAssignableTo(new AnyType(), new NeverType())).toBe(true); + expect(isAssignableTo(new ArrayType(new NumberType()), new NeverType())).toBe(true); + expect(isAssignableTo(new IntersectionType([ new StringType(), new NullType() ]), new NeverType())).toBe(true); + expect(isAssignableTo(new LiteralType("literal"), new NeverType())).toBe(true); + expect(isAssignableTo(new NeverType(), new NeverType())).toBe(true); + expect(isAssignableTo(new NullType(), new NeverType())).toBe(true); + expect(isAssignableTo(new ObjectType("obj", [], [ new ObjectProperty("foo", new StringType(), true) ], true), + new NeverType())).toBe(true); + expect(isAssignableTo(new BooleanType(), new NeverType())).toBe(true); + expect(isAssignableTo(new NumberType(), new NeverType())).toBe(true); + expect(isAssignableTo(new BooleanType(), new NeverType())).toBe(true); + expect(isAssignableTo(new StringType(), new NeverType())).toBe(true); + expect(isAssignableTo(new TupleType([new StringType(), new NumberType() ]), new NeverType())).toBe(true); + expect(isAssignableTo(new UndefinedType(), new NeverType())).toBe(true); + }); + it("lets anything to be assigned to type 'any'", () => { + expect(isAssignableTo(new AnyType(), new AnyType())).toBe(true); + expect(isAssignableTo(new AnyType(), new ArrayType(new NumberType()))).toBe(true); + expect(isAssignableTo(new AnyType(), new IntersectionType([ new StringType(), new NullType() ]))).toBe(true); + expect(isAssignableTo(new AnyType(), new LiteralType("literal"))).toBe(true); + expect(isAssignableTo(new AnyType(), new NeverType())).toBe(true); + expect(isAssignableTo(new AnyType(), new NullType())).toBe(true); + expect(isAssignableTo(new AnyType(), + new ObjectType("obj", [], [ new ObjectProperty("foo", new StringType(), true) ], true))).toBe(true); + expect(isAssignableTo(new AnyType(), new BooleanType())).toBe(true); + expect(isAssignableTo(new AnyType(), new NumberType())).toBe(true); + expect(isAssignableTo(new AnyType(), new BooleanType())).toBe(true); + expect(isAssignableTo(new AnyType(), new StringType())).toBe(true); + expect(isAssignableTo(new AnyType(), new TupleType([new StringType(), new NumberType() ]))).toBe(true); + expect(isAssignableTo(new AnyType(), new UndefinedType())).toBe(true); + }); + it("lets anything to be assigned to type 'unknown'", () => { + expect(isAssignableTo(new UnknownType(), new AnyType())).toBe(true); + expect(isAssignableTo(new UnknownType(), new ArrayType(new NumberType()))).toBe(true); + expect(isAssignableTo(new UnknownType(), + new IntersectionType([ new StringType(), new NullType() ]))).toBe(true); + expect(isAssignableTo(new UnknownType(), new LiteralType("literal"))).toBe(true); + expect(isAssignableTo(new UnknownType(), new NeverType())).toBe(true); + expect(isAssignableTo(new UnknownType(), new NullType())).toBe(true); + expect(isAssignableTo(new UnknownType(), + new ObjectType("obj", [], [ new ObjectProperty("foo", new StringType(), true) ], true))).toBe(true); + expect(isAssignableTo(new UnknownType(), new BooleanType())).toBe(true); + expect(isAssignableTo(new UnknownType(), new NumberType())).toBe(true); + expect(isAssignableTo(new UnknownType(), new BooleanType())).toBe(true); + expect(isAssignableTo(new UnknownType(), new StringType())).toBe(true); + expect(isAssignableTo(new UnknownType(), new TupleType([new StringType(), new NumberType() ]))).toBe(true); + expect(isAssignableTo(new UnknownType(), new UndefinedType())).toBe(true); + }); + it("lets 'unknown' only to be assigned to type 'unknown' or 'any'", () => { + expect(isAssignableTo(new AnyType(), new UnknownType())).toBe(true); + expect(isAssignableTo(new ArrayType(new NumberType()), new UnknownType())).toBe(false); + expect(isAssignableTo(new IntersectionType([ new StringType(), new NullType() ]), + new UnknownType())).toBe(false); + expect(isAssignableTo(new LiteralType("literal"), new UnknownType())).toBe(false); + expect(isAssignableTo(new NeverType(), new UnknownType())).toBe(false); + expect(isAssignableTo(new NullType(), new UnknownType())).toBe(false); + expect(isAssignableTo(new UnknownType(), new UnknownType())).toBe(true); + expect(isAssignableTo(new ObjectType("obj", [], [ new ObjectProperty("foo", new StringType(), true) ], false), + new UnknownType())).toBe(false); + expect(isAssignableTo(new BooleanType(), new UnknownType())).toBe(false); + expect(isAssignableTo(new NumberType(), new UnknownType())).toBe(false); + expect(isAssignableTo(new BooleanType(), new UnknownType())).toBe(false); + expect(isAssignableTo(new StringType(), new UnknownType())).toBe(false); + expect(isAssignableTo(new TupleType([new StringType(), new NumberType() ]), new UnknownType())).toBe(false); + expect(isAssignableTo(new UndefinedType(), new UnknownType())).toBe(false); + }); + it("lets union type to be assigned if all sub types are compatible to target type", () => { + const typeA = new ObjectType("a", [], [ new ObjectProperty("a", new StringType(), true) ], true); + const typeB = new ObjectType("b", [], [ new ObjectProperty("b", new StringType(), true) ], true); + const typeC = new ObjectType("c", [], [ new ObjectProperty("c", new StringType(), true) ], true); + const typeAB = new ObjectType("ab", [ typeA, typeB ], [], true); + const typeAorB = new UnionType([ typeA, typeB ]); + expect(isAssignableTo(typeAB, new UnionType([ typeA, typeA ]))).toBe(false); + expect(isAssignableTo(typeAB, new UnionType([ typeB, typeB ]))).toBe(false); + expect(isAssignableTo(typeAB, new UnionType([ typeA, typeB ]))).toBe(false); + expect(isAssignableTo(typeAB, new UnionType([ typeB, typeA ]))).toBe(false); + expect(isAssignableTo(typeAB, new UnionType([ typeB, typeA, typeC ]))).toBe(false); + expect(isAssignableTo(typeAorB, new UnionType([ typeB, typeA ]))).toBe(true); + expect(isAssignableTo(typeAorB, new UnionType([ typeA, typeB ]))).toBe(true); + expect(isAssignableTo(typeAorB, new UnionType([ typeAB, typeB, typeC ]))).toBe(false); + }); + it("lets tuple type to be assigned to array type if item types match", () => { + expect(isAssignableTo(new ArrayType(new StringType()), new TupleType([ new StringType(), new StringType() ]))) + .toBe(true); + expect(isAssignableTo(new ArrayType(new NumberType()), new TupleType([ new StringType(), new StringType() ]))) + .toBe(false); + expect(isAssignableTo(new ArrayType(new StringType()), new TupleType([ new StringType(), new NumberType() ]))) + .toBe(false); + }); + it("lets only compatible tuple type to be assigned to tuple type", () => { + expect(isAssignableTo(new TupleType([ new StringType(), new StringType() ]), new ArrayType(new StringType()))) + .toBe(false); + expect(isAssignableTo(new TupleType([ new StringType(), new StringType() ]), new StringType())).toBe(false); + expect(isAssignableTo(new TupleType([ new StringType(), new StringType() ]), + new TupleType([ new StringType(), new NumberType() ]))).toBe(false); + expect(isAssignableTo(new TupleType([ new StringType(), new StringType() ]), + new TupleType([ new StringType(), new StringType() ]))).toBe(true); + expect(isAssignableTo(new TupleType([ new StringType(), new OptionalType(new StringType()) ]), + new TupleType([ new StringType() ]))).toBe(true); + expect(isAssignableTo(new TupleType([ new StringType(), new OptionalType(new StringType()) ]), + new TupleType([ new StringType(), new StringType() ]))).toBe(true); + }); + it("lets anything except null and undefined to be assigned to empty object type", () => { + const empty = new ObjectType("empty", [], [], false); + expect(isAssignableTo(empty, new AnyType())).toBe(true); + expect(isAssignableTo(empty, new ArrayType(new NumberType()))).toBe(true); + expect(isAssignableTo(empty, new IntersectionType([ new StringType(), new NullType() ]))).toBe(true); + expect(isAssignableTo(empty, new LiteralType("literal"))).toBe(true); + expect(isAssignableTo(empty, new NeverType())).toBe(true); + expect(isAssignableTo(empty, new NullType())).toBe(false); + expect(isAssignableTo(empty, + new ObjectType("obj", [], [ new ObjectProperty("foo", new StringType(), true) ], true))).toBe(true); + expect(isAssignableTo(empty, new BooleanType())).toBe(true); + expect(isAssignableTo(empty, new NumberType())).toBe(true); + expect(isAssignableTo(empty, new BooleanType())).toBe(true); + expect(isAssignableTo(empty, new StringType())).toBe(true); + expect(isAssignableTo(empty, new TupleType([new StringType(), new NumberType() ]))).toBe(true); + expect(isAssignableTo(empty, new UndefinedType())).toBe(false); + }); + it("lets only compatible object types to be assigned to object type", () => { + const typeA = new ObjectType("a", [], [ new ObjectProperty("a", new StringType(), true) ], false); + const typeB = new ObjectType("b", [], [ new ObjectProperty("b", new StringType(), true) ], false); + const typeC = new ObjectType("c", [], [ new ObjectProperty("c", new StringType(), true) ], false); + const typeAB = new ObjectType("ab", [ typeA, typeB ], [], false); + expect(isAssignableTo(typeA, new StringType())).toBe(false); + expect(isAssignableTo(typeA, typeAB)).toBe(true); + expect(isAssignableTo(typeB, typeAB)).toBe(true); + expect(isAssignableTo(typeC, typeAB)).toBe(false); + expect(isAssignableTo(typeAB, typeA)).toBe(false); + expect(isAssignableTo(typeAB, typeB)).toBe(false); + }); + it("does let object to be assigned to object with optional properties and at least one property in common", () => { + const typeA = new ObjectType("a", [], [ + new ObjectProperty("a", new StringType(), false), + new ObjectProperty("b", new StringType(), false), + ], false); + const typeB = new ObjectType("b", [], [ new ObjectProperty("b", new StringType(), false) ], false); + expect(isAssignableTo(typeB, typeA)).toBe(true); + }); + it("does not let object to be assigned to object with only optional properties and no properties in common", () => { + const typeA = new ObjectType("a", [], [ new ObjectProperty("a", new StringType(), true) ], false); + const typeB = new ObjectType("b", [], [ new ObjectProperty("b", new StringType(), false) ], false); + expect(isAssignableTo(typeB, typeA)).toBe(false); + }); + it("correctly handles primitive source intersection types", () => { + const numberAndString = new IntersectionType([ new StringType(), new NumberType() ]); + expect(isAssignableTo(new StringType(), numberAndString)).toBe(true); + expect(isAssignableTo(new NumberType(), numberAndString)).toBe(true); + expect(isAssignableTo(new BooleanType(), numberAndString)).toBe(false); + }); + it("correctly handles intersection types with objects", () => { + const a = new ObjectType("a", [], [ new ObjectProperty("a", new StringType(), true) ], false); + const b = new ObjectType("b", [], [ new ObjectProperty("b", new StringType(), true) ], false); + const c = new ObjectType("c", [], [ new ObjectProperty("c", new StringType(), true) ], false); + const ab = new ObjectType("ab", [], [ + new ObjectProperty("a", new StringType(), true), + new ObjectProperty("b", new StringType(), true), + ], false); + const aAndB = new IntersectionType([ a, b] ); + expect(isAssignableTo(a, aAndB)).toBe(true); + expect(isAssignableTo(b, aAndB)).toBe(true); + expect(isAssignableTo(c, aAndB)).toBe(false); + expect(isAssignableTo(ab, aAndB)).toBe(true); + expect(isAssignableTo(aAndB, a)).toBe(false); + expect(isAssignableTo(aAndB, b)).toBe(false); + expect(isAssignableTo(aAndB, c)).toBe(false); + expect(isAssignableTo(aAndB, ab)).toBe(true); + expect(isAssignableTo(aAndB, aAndB)).toBe(true); + }); + it("correctly handles circular dependencies", () => { + const nodeTypeARef = new ReferenceType(); + const nodeTypeA = new ObjectType("a", [], [ new ObjectProperty("parent", nodeTypeARef, false) ], false); + nodeTypeARef.setType(nodeTypeA); + + const nodeTypeBRef = new ReferenceType(); + const nodeTypeB = new ObjectType("b", [], [ new ObjectProperty("parent", nodeTypeBRef, false) ], false); + nodeTypeBRef.setType(nodeTypeB); + + const nodeTypeCRef = new ReferenceType(); + const nodeTypeC = new ObjectType("c", [], [ new ObjectProperty("child", nodeTypeCRef, false) ], false); + nodeTypeCRef.setType(nodeTypeC); + + expect(isAssignableTo(nodeTypeA, nodeTypeA)).toBe(true); + expect(isAssignableTo(nodeTypeA, nodeTypeB)).toBe(true); + expect(isAssignableTo(nodeTypeB, nodeTypeA)).toBe(true); + expect(isAssignableTo(nodeTypeC, nodeTypeA)).toBe(false); + expect(isAssignableTo(nodeTypeC, nodeTypeB)).toBe(false); + expect(isAssignableTo(nodeTypeA, nodeTypeC)).toBe(false); + expect(isAssignableTo(nodeTypeB, nodeTypeC)).toBe(false); + }); + it("can handle deep union structures", () => { + const objectType = new ObjectType("interface-src/test.ts-0-53-src/test.ts-0-317", [], + [ new ObjectProperty("a", new StringType(), true) ], false); + const innerDefinition = new DefinitionType("NumericValueRef", objectType); + const innerUnion = new UnionType([ new NumberType(), innerDefinition ]); + const alias = new AliasType("alias-src/test.ts-53-106-src/test.ts-0-317", innerUnion); + const outerDefinition = new DefinitionType("NumberValue", alias); + const outerUnion = new UnionType([ outerDefinition, new UndefinedType() ]); + const def = new DefinitionType("NumericValueRef", objectType); + expect(isAssignableTo(outerUnion, def)).toBe(true); + }); +}); diff --git a/test/valid-data.test.ts b/test/valid-data.test.ts index 96be9234a..ed0035205 100644 --- a/test/valid-data.test.ts +++ b/test/valid-data.test.ts @@ -164,4 +164,15 @@ describe("valid-data", () => { it("undefined-property", assertSchema("undefined-property", "MyType")); it("any-unknown", assertSchema("any-unknown", "MyObject")); + + it("type-conditional-simple", assertSchema("type-conditional-simple", "MyObject")); + it("type-conditional-inheritance", assertSchema("type-conditional-inheritance", "MyObject")); + it("type-conditional-union", assertSchema("type-conditional-union", "MyObject")); + it("type-conditional-enum", assertSchema("type-conditional-enum", "IParameter")); + it("type-conditional-intersection", assertSchema("type-conditional-intersection", "MyObject")); + it("type-conditional-exclude", assertSchema("type-conditional-exclude", "MyObject")); + it("type-conditional-exclude-complex", assertSchema("type-conditional-exclude-complex", "BaseAxisNoSignals")); + it("type-conditional-exclude-narrowing", assertSchema("type-conditional-exclude-narrowing", "MyObject")); + it("type-conditional-omit", assertSchema("type-conditional-omit", "MyObject")); + it("type-conditional-jsdoc", assertSchema("type-conditional-jsdoc", "MyObject", "extended")); }); diff --git a/test/valid-data/type-conditional-enum/main.ts b/test/valid-data/type-conditional-enum/main.ts new file mode 100644 index 000000000..c367119e4 --- /dev/null +++ b/test/valid-data/type-conditional-enum/main.ts @@ -0,0 +1,10 @@ +enum ParameterType { + Enum = "enum", + Number = "number", + String = "string", + Date = "date", +} + +export interface IParameter { + type: Exclude; +} diff --git a/test/valid-data/type-conditional-enum/schema.json b/test/valid-data/type-conditional-enum/schema.json new file mode 100644 index 000000000..ace0a4a98 --- /dev/null +++ b/test/valid-data/type-conditional-enum/schema.json @@ -0,0 +1,22 @@ +{ + "$ref": "#/definitions/IParameter", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "IParameter": { + "additionalProperties": false, + "properties": { + "type": { + "enum": [ + "string", + "date" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/type-conditional-exclude-complex/main.ts b/test/valid-data/type-conditional-exclude-complex/main.ts new file mode 100644 index 000000000..bbc81b70a --- /dev/null +++ b/test/valid-data/type-conditional-exclude-complex/main.ts @@ -0,0 +1,23 @@ +export interface NumericValueRef { + name: "numeric"; + ref: string; +} + +export interface StringValueRef { + name: "string"; + ref: string; +} + +export type NumberValue = number | NumericValueRef; +export type StringValue = string | StringValueRef; + +export interface BaseAxis { + minExtent?: N; + titleFont?: S; +} + +type OmitValueRef = { + [P in keyof T]?: Exclude, StringValueRef> +}; + +export type BaseAxisNoSignals = OmitValueRef; diff --git a/test/valid-data/type-conditional-exclude-complex/schema.json b/test/valid-data/type-conditional-exclude-complex/schema.json new file mode 100644 index 000000000..ff049f392 --- /dev/null +++ b/test/valid-data/type-conditional-exclude-complex/schema.json @@ -0,0 +1,18 @@ +{ + "$ref": "#/definitions/BaseAxisNoSignals", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "BaseAxisNoSignals": { + "additionalProperties": false, + "properties": { + "minExtent": { + "type": "number" + }, + "titleFont": { + "type": "string" + } + }, + "type": "object" + } + } +} diff --git a/test/valid-data/type-conditional-exclude-narrowing/main.ts b/test/valid-data/type-conditional-exclude-narrowing/main.ts new file mode 100644 index 000000000..fb967f5c7 --- /dev/null +++ b/test/valid-data/type-conditional-exclude-narrowing/main.ts @@ -0,0 +1,22 @@ +export type Align = "left" | "right" | "center"; + +export type Text = { + align?: Align | number; +}; + +type OmitPropertyType = { + [P in keyof T]: Exclude; +}; + +export type GoodPrimitives = string | number; +export type BadPrimitives = null | boolean; +export type Primitives = GoodPrimitives | BadPrimitives; + +export interface MyObject { + textWithoutAlign: OmitPropertyType; + textWithoutNumbers: OmitPropertyType; + allPrims: Exclude; + goodPrims: Exclude; + badPrims: Exclude, number>; + stringOrNull: Exclude; +} diff --git a/test/valid-data/type-conditional-exclude-narrowing/schema.json b/test/valid-data/type-conditional-exclude-narrowing/schema.json new file mode 100644 index 000000000..aaaff37cf --- /dev/null +++ b/test/valid-data/type-conditional-exclude-narrowing/schema.json @@ -0,0 +1,83 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Align": { + "enum": [ + "left", + "right", + "center" + ], + "type": "string" + }, + "BadPrimitives": { + "type": [ + "null", + "boolean" + ] + }, + "GoodPrimitives": { + "type": [ + "string", + "number" + ] + }, + "MyObject": { + "additionalProperties": false, + "properties": { + "allPrims": { + "$ref": "#/definitions/Primitives" + }, + "badPrims": { + "$ref": "#/definitions/BadPrimitives" + }, + "goodPrims": { + "$ref": "#/definitions/GoodPrimitives" + }, + "stringOrNull": { + "type": [ + "string", + "null" + ] + }, + "textWithoutAlign": { + "additionalProperties": false, + "properties": { + "align": { + "type": "number" + } + }, + "type": "object" + }, + "textWithoutNumbers": { + "additionalProperties": false, + "properties": { + "align": { + "$ref": "#/definitions/Align" + } + }, + "type": "object" + } + }, + "required": [ + "textWithoutAlign", + "textWithoutNumbers", + "allPrims", + "goodPrims", + "badPrims", + "stringOrNull" + ], + "type": "object" + }, + "Primitives": { + "anyOf": [ + { + "$ref": "#/definitions/GoodPrimitives" + }, + { + "$ref": "#/definitions/BadPrimitives" + } + ] + } + } +} diff --git a/test/valid-data/type-conditional-exclude/main.ts b/test/valid-data/type-conditional-exclude/main.ts new file mode 100644 index 000000000..beff73079 --- /dev/null +++ b/test/valid-data/type-conditional-exclude/main.ts @@ -0,0 +1,8 @@ +export type Primitives = string | number | boolean; + +export type MyObject = { + primitives: Primitives; + noNumber: Exclude; + noNumberAndBoolean: Exclude; + noStringAndNumber: Exclude, string>; +}; diff --git a/test/valid-data/type-conditional-exclude/schema.json b/test/valid-data/type-conditional-exclude/schema.json new file mode 100644 index 000000000..9516d8c39 --- /dev/null +++ b/test/valid-data/type-conditional-exclude/schema.json @@ -0,0 +1,40 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "noNumber": { + "type": [ + "string", + "boolean" + ] + }, + "noNumberAndBoolean": { + "type": "string" + }, + "noStringAndNumber": { + "type": "boolean" + }, + "primitives": { + "$ref": "#/definitions/Primitives" + } + }, + "required": [ + "primitives", + "noNumber", + "noNumberAndBoolean", + "noStringAndNumber" + ], + "type": "object" + }, + "Primitives": { + "type": [ + "string", + "number", + "boolean" + ] + } + } +} diff --git a/test/valid-data/type-conditional-inheritance/main.ts b/test/valid-data/type-conditional-inheritance/main.ts new file mode 100644 index 000000000..48290207c --- /dev/null +++ b/test/valid-data/type-conditional-inheritance/main.ts @@ -0,0 +1,41 @@ +export interface A { + a: string; +} + +export interface B extends A { + b: string; +} + +export interface C extends B { + a: string; + c: string; +} + +export interface D { + a: number; + d: string; +} + +export interface E extends D {} + +export interface F extends D { + f: boolean; +} + +export type Map = + T extends A ? "a" : + T extends B ? "b" : + T extends C ? "c" : + T extends F ? "f" : + T extends D ? "d" : + T extends E ? "e" : + "unknown"; + +export type MyObject = { + a: Map; + b: Map; + c: Map; + d: Map; + e: Map; + f: Map; +}; diff --git a/test/valid-data/type-conditional-inheritance/schema.json b/test/valid-data/type-conditional-inheritance/schema.json new file mode 100644 index 000000000..705d790c8 --- /dev/null +++ b/test/valid-data/type-conditional-inheritance/schema.json @@ -0,0 +1,74 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Map": { + "enum": [ + "a" + ], + "type": "string" + }, + "Map": { + "enum": [ + "a" + ], + "type": "string" + }, + "Map": { + "enum": [ + "a" + ], + "type": "string" + }, + "Map": { + "enum": [ + "d" + ], + "type": "string" + }, + "Map": { + "enum": [ + "d" + ], + "type": "string" + }, + "Map": { + "enum": [ + "f" + ], + "type": "string" + }, + "MyObject": { + "additionalProperties": false, + "properties": { + "a": { + "$ref": "#/definitions/Map" + }, + "b": { + "$ref": "#/definitions/Map" + }, + "c": { + "$ref": "#/definitions/Map" + }, + "d": { + "$ref": "#/definitions/Map" + }, + "e": { + "$ref": "#/definitions/Map" + }, + "f": { + "$ref": "#/definitions/Map" + } + }, + "required": [ + "a", + "b", + "c", + "d", + "e", + "f" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/type-conditional-intersection/main.ts b/test/valid-data/type-conditional-intersection/main.ts new file mode 100644 index 000000000..f7db9ca0d --- /dev/null +++ b/test/valid-data/type-conditional-intersection/main.ts @@ -0,0 +1,30 @@ +interface A { + a: string; +} + +interface B { + b: string; +} + +interface C extends A, B {} + +interface D extends A, B { + d: string; +} + +type Map = + T extends D ? "D" : + T extends A & B ? "a and b" : + T extends A ? "a" : + T extends B ? "b" : + T extends C ? "c" : + "unknown"; + +export type MyObject = { + a: Map; + b: Map; + c: Map; + d: Map; + e: Map; + f: Map; +}; diff --git a/test/valid-data/type-conditional-intersection/schema.json b/test/valid-data/type-conditional-intersection/schema.json new file mode 100644 index 000000000..9dee72747 --- /dev/null +++ b/test/valid-data/type-conditional-intersection/schema.json @@ -0,0 +1,56 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "a": { + "enum": [ + "a" + ], + "type": "string" + }, + "b": { + "enum": [ + "b" + ], + "type": "string" + }, + "c": { + "enum": [ + "a and b" + ], + "type": "string" + }, + "d": { + "enum": [ + "D" + ], + "type": "string" + }, + "e": { + "enum": [ + "a and b" + ], + "type": "string" + }, + "f": { + "enum": [ + "D" + ], + "type": "string" + } + }, + "required": [ + "a", + "b", + "c", + "d", + "e", + "f" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/type-conditional-jsdoc/main.ts b/test/valid-data/type-conditional-jsdoc/main.ts new file mode 100644 index 000000000..336690e29 --- /dev/null +++ b/test/valid-data/type-conditional-jsdoc/main.ts @@ -0,0 +1,38 @@ +/** + * Number or string + * @pattern foo + */ +type NumberOrString = number | string; + +type NoString = T extends string ? never : T; + +type BooleanOrNumberOrString = NumberOrString | boolean; + +/** + * No string + * @pattern bar + */ +type NoStringDocumented = T extends string ? never : T; + +export type MyObject = { + a: NumberOrString extends number ? never : NumberOrString; + + /** Description of b */ + b: NumberOrString extends number ? never : NumberOrString; + + c: NoString; + + d: NoStringDocumented; + + /** Description of e */ + e: NoString; + + /** Description of f */ + f: NoStringDocumented; + + g: Exclude; + + h: Exclude; + + i: Exclude; +}; diff --git a/test/valid-data/type-conditional-jsdoc/schema.json b/test/valid-data/type-conditional-jsdoc/schema.json new file mode 100644 index 000000000..2ba254610 --- /dev/null +++ b/test/valid-data/type-conditional-jsdoc/schema.json @@ -0,0 +1,75 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "a": { + "description": "Number or string", + "pattern": "foo", + "type": [ + "number", + "string" + ] + }, + "b": { + "description": "Description of b", + "pattern": "foo", + "type": [ + "number", + "string" + ] + }, + "c": { + "type": "number" + }, + "d": { + "description": "No string", + "pattern": "bar", + "type": "number" + }, + "e": { + "description": "Description of e", + "type": "number" + }, + "f": { + "description": "Description of f", + "pattern": "bar", + "type": "number" + }, + "g": { + "description": "Number or string", + "pattern": "foo", + "type": [ + "number", + "string" + ] + }, + "h": { + "type": "string" + }, + "i": { + "description": "Number or string", + "pattern": "foo", + "type": [ + "number", + "string" + ] + } + }, + "required": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/type-conditional-omit/main.ts b/test/valid-data/type-conditional-omit/main.ts new file mode 100644 index 000000000..d56e97dbb --- /dev/null +++ b/test/valid-data/type-conditional-omit/main.ts @@ -0,0 +1,8 @@ +interface Test { + a: string; + b: number; + c: boolean; + d: string[]; +} + +export type MyObject = Omit; diff --git a/test/valid-data/type-conditional-omit/schema.json b/test/valid-data/type-conditional-omit/schema.json new file mode 100644 index 000000000..719f8c4db --- /dev/null +++ b/test/valid-data/type-conditional-omit/schema.json @@ -0,0 +1,22 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "a": { + "type": "string" + }, + "c": { + "type": "boolean" + } + }, + "required": [ + "a", + "c" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/type-conditional-simple/main.ts b/test/valid-data/type-conditional-simple/main.ts new file mode 100644 index 000000000..df0e446fb --- /dev/null +++ b/test/valid-data/type-conditional-simple/main.ts @@ -0,0 +1,10 @@ +type TypeName = + T extends string ? "string" : + T extends number ? "number" : + "unknown"; + +export type MyObject = { + a: TypeName; + b: TypeName; + c: TypeName; +}; diff --git a/test/valid-data/type-conditional-simple/schema.json b/test/valid-data/type-conditional-simple/schema.json new file mode 100644 index 000000000..cb0116d05 --- /dev/null +++ b/test/valid-data/type-conditional-simple/schema.json @@ -0,0 +1,35 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "a": { + "enum": [ + "string" + ], + "type": "string" + }, + "b": { + "enum": [ + "number" + ], + "type": "string" + }, + "c": { + "enum": [ + "unknown" + ], + "type": "string" + } + }, + "required": [ + "a", + "b", + "c" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/type-conditional-union/main.ts b/test/valid-data/type-conditional-union/main.ts new file mode 100644 index 000000000..482bb5427 --- /dev/null +++ b/test/valid-data/type-conditional-union/main.ts @@ -0,0 +1,22 @@ +interface A { + a: string; +} + +interface B { + b: string; +} + +interface C { + c: string; +} + +type Map = + T extends (A | B) ? "a or b" : + "unknown"; + +export type MyObject = { + a: Map; + b: Map; + c: Map; + d: Map; +}; diff --git a/test/valid-data/type-conditional-union/schema.json b/test/valid-data/type-conditional-union/schema.json new file mode 100644 index 000000000..9d8d74804 --- /dev/null +++ b/test/valid-data/type-conditional-union/schema.json @@ -0,0 +1,42 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "a": { + "enum": [ + "a or b" + ], + "type": "string" + }, + "b": { + "enum": [ + "a or b" + ], + "type": "string" + }, + "c": { + "enum": [ + "unknown" + ], + "type": "string" + }, + "d": { + "enum": [ + "a or b" + ], + "type": "string" + } + }, + "required": [ + "a", + "b", + "c", + "d" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/type-mapped-generic/main.ts b/test/valid-data/type-mapped-generic/main.ts index 463937de0..3c56d6d16 100644 --- a/test/valid-data/type-mapped-generic/main.ts +++ b/test/valid-data/type-mapped-generic/main.ts @@ -9,4 +9,4 @@ export type NullableAndPartial = { [K in keyof T]?: T[K] | null; }; -export type MyObject = NullableAndPartial; \ No newline at end of file +export type MyObject = NullableAndPartial;