From 3ded513e81a28f740456ff8c045d7bc5bbf08ef9 Mon Sep 17 00:00:00 2001 From: Daan Boer <39552979+daanboer@users.noreply.github.com> Date: Fri, 19 Aug 2022 04:28:39 +0200 Subject: [PATCH] fix: consistently use NeverType (#1367) --- index.ts | 2 +- src/ChainNodeParser.ts | 6 +-- src/CircularReferenceNodeParser.ts | 4 +- src/CircularReferenceTypeFormatter.ts | 4 +- src/Error/UnknownTypeError.ts | 6 +-- src/ExposeNodeParser.ts | 6 +-- src/NodeParser.ts | 15 +++---- src/NodeParser/AnnotatedNodeParser.ts | 6 +-- .../ArrayLiteralExpressionNodeParser.ts | 13 ++----- src/NodeParser/ArrayNodeParser.ts | 5 +-- src/NodeParser/AsExpressionNodeParser.ts | 2 +- src/NodeParser/ConditionalTypeNodeParser.ts | 9 +++-- .../ExpressionWithTypeArgumentsNodeParser.ts | 2 +- src/NodeParser/HiddenTypeNodeParser.ts | 5 ++- src/NodeParser/IndexedAccessTypeNodeParser.ts | 11 +++--- src/NodeParser/InferTypeNodeParser.ts | 2 +- src/NodeParser/InterfaceAndClassNodeParser.ts | 20 ++++------ src/NodeParser/IntersectionNodeParser.ts | 13 ++++--- src/NodeParser/IntrinsicNodeParser.ts | 2 +- src/NodeParser/LiteralNodeParser.ts | 2 +- src/NodeParser/MappedTypeNodeParser.ts | 21 +++------- src/NodeParser/NamedTupleMemberNodeParser.ts | 2 +- src/NodeParser/NeverTypeNodeParser.ts | 2 +- .../ObjectLiteralExpressionNodeParser.ts | 25 +++++------- src/NodeParser/OptionalTypeNodeParser.ts | 5 +-- src/NodeParser/ParameterParser.ts | 2 +- src/NodeParser/ParenthesizedNodeParser.ts | 8 +--- .../PropertyAccessExpressionParser.ts | 4 +- src/NodeParser/TypeAliasNodeParser.ts | 11 ++---- src/NodeParser/TypeLiteralNodeParser.ts | 9 +++-- src/NodeParser/TypeReferenceNodeParser.ts | 7 ++-- src/NodeParser/TypeofNodeParser.ts | 4 +- src/NodeParser/UnionNodeParser.ts | 9 +++-- src/SchemaGenerator.ts | 10 ++--- src/TopRefNodeParser.ts | 6 +-- src/Type/ObjectType.ts | 4 +- src/Type/TupleType.ts | 10 ++--- src/Type/UnionType.ts | 18 ++++++--- src/TypeFormatter/ObjectTypeFormatter.ts | 18 +++++---- src/TypeFormatter/TupleTypeFormatter.ts | 6 +-- src/TypeFormatter/UnionTypeFormatter.ts | 7 +++- src/Utils/derefType.ts | 2 +- src/Utils/extractLiterals.ts | 4 +- src/Utils/isAssignableTo.ts | 20 ++++------ src/Utils/narrowType.ts | 13 ++++--- src/Utils/notNever.ts | 6 +++ src/Utils/notUndefined.ts | 3 -- src/Utils/typeKeys.ts | 6 +-- test/config.test.ts | 4 +- test/unit/isAssignableTo.test.ts | 39 ++++++++++--------- .../schema.json | 18 --------- 51 files changed, 193 insertions(+), 245 deletions(-) create mode 100644 src/Utils/notNever.ts delete mode 100644 src/Utils/notUndefined.ts diff --git a/index.ts b/index.ts index 3fc717fa2..97672a7d8 100644 --- a/index.ts +++ b/index.ts @@ -22,7 +22,7 @@ export * from "./src/Utils/isHidden"; export * from "./src/Utils/modifiers"; export * from "./src/Utils/narrowType"; export * from "./src/Utils/nodeKey"; -export * from "./src/Utils/notUndefined"; +export * from "./src/Utils/notNever"; export * from "./src/Utils/preserveAnnotation"; export * from "./src/Utils/removeUndefined"; export * from "./src/Utils/removeUnreachable"; diff --git a/src/ChainNodeParser.ts b/src/ChainNodeParser.ts index 017db7eb9..1c940397b 100644 --- a/src/ChainNodeParser.ts +++ b/src/ChainNodeParser.ts @@ -7,7 +7,7 @@ import { BaseType } from "./Type/BaseType"; import { ReferenceType } from "./Type/ReferenceType"; export class ChainNodeParser implements SubNodeParser, MutableParser { - protected readonly typeCaches = new WeakMap>(); + protected readonly typeCaches = new WeakMap>(); public constructor(protected typeChecker: ts.TypeChecker, protected nodeParsers: SubNodeParser[]) {} @@ -20,10 +20,10 @@ export class ChainNodeParser implements SubNodeParser, MutableParser { return this.nodeParsers.some((nodeParser) => nodeParser.supportsNode(node)); } - public createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType | undefined { + public createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType { let typeCache = this.typeCaches.get(node); if (typeCache == null) { - typeCache = new Map(); + typeCache = new Map(); this.typeCaches.set(node, typeCache); } const contextCacheKey = context.getCacheKey(); diff --git a/src/CircularReferenceNodeParser.ts b/src/CircularReferenceNodeParser.ts index 762e14354..28c033568 100644 --- a/src/CircularReferenceNodeParser.ts +++ b/src/CircularReferenceNodeParser.ts @@ -6,14 +6,14 @@ import { ReferenceType } from "./Type/ReferenceType"; import { getKey } from "./Utils/nodeKey"; export class CircularReferenceNodeParser implements SubNodeParser { - protected circular = new Map(); + protected circular: Map = new Map(); public constructor(protected childNodeParser: SubNodeParser) {} public supportsNode(node: ts.Node): boolean { return this.childNodeParser.supportsNode(node); } - public createType(node: ts.Node, context: Context): BaseType | undefined { + public createType(node: ts.Node, context: Context): BaseType { const key = getKey(node, context); if (this.circular.has(key)) { return this.circular.get(key)!; diff --git a/src/CircularReferenceTypeFormatter.ts b/src/CircularReferenceTypeFormatter.ts index bdafd383d..e7de1c02d 100644 --- a/src/CircularReferenceTypeFormatter.ts +++ b/src/CircularReferenceTypeFormatter.ts @@ -4,8 +4,8 @@ import { BaseType } from "./Type/BaseType"; import { uniqueArray } from "./Utils/uniqueArray"; export class CircularReferenceTypeFormatter implements SubTypeFormatter { - protected definition = new Map(); - protected children = new Map(); + protected definition: Map = new Map(); + protected children: Map = new Map(); public constructor(protected childTypeFormatter: SubTypeFormatter) {} diff --git a/src/Error/UnknownTypeError.ts b/src/Error/UnknownTypeError.ts index 5b3cda593..9c06923e3 100644 --- a/src/Error/UnknownTypeError.ts +++ b/src/Error/UnknownTypeError.ts @@ -2,11 +2,11 @@ import { BaseType } from "../Type/BaseType"; import { BaseError } from "./BaseError"; export class UnknownTypeError extends BaseError { - public constructor(private type: BaseType | undefined) { - super(`Unknown type "${type ? type.getId() : undefined}"`); + public constructor(private type: BaseType) { + super(`Unknown type "${type.getId()}"`); } - public getType(): BaseType | undefined { + public getType(): BaseType { return this.type; } } diff --git a/src/ExposeNodeParser.ts b/src/ExposeNodeParser.ts index dbdfcaebf..58d341984 100644 --- a/src/ExposeNodeParser.ts +++ b/src/ExposeNodeParser.ts @@ -19,13 +19,9 @@ export class ExposeNodeParser implements SubNodeParser { return this.subNodeParser.supportsNode(node); } - public createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType | undefined { + public createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType { const baseType = this.subNodeParser.createType(node, context, reference); - if (baseType === undefined) { - return undefined; - } - if (!this.isExportNode(node)) { return baseType; } diff --git a/src/NodeParser.ts b/src/NodeParser.ts index d350dbad4..c649a8706 100644 --- a/src/NodeParser.ts +++ b/src/NodeParser.ts @@ -6,16 +6,16 @@ import { getKey } from "./Utils/nodeKey"; export class Context { private cacheKey: string | null = null; - private arguments: (BaseType | undefined)[] = []; + private arguments: BaseType[] = []; private parameters: string[] = []; private reference?: ts.Node; - private defaultArgument = new Map(); + private defaultArgument = new Map(); public constructor(reference?: ts.Node) { this.reference = reference; } - public pushArgument(argumentType: BaseType | undefined): void { + public pushArgument(argumentType: BaseType): void { this.arguments.push(argumentType); this.cacheKey = null; } @@ -24,7 +24,7 @@ export class Context { this.parameters.push(parameterName); } - public setDefault(parameterName: string, argumentType: BaseType | undefined): void { + public setDefault(parameterName: string, argumentType: BaseType): void { this.defaultArgument.set(parameterName, argumentType); } @@ -38,8 +38,9 @@ export class Context { return this.cacheKey; } - public getArgument(parameterName: string): BaseType | undefined { + public getArgument(parameterName: string): BaseType { const index: number = this.parameters.indexOf(parameterName); + if ((index < 0 || !this.arguments[index]) && this.defaultArgument.has(parameterName)) { return this.defaultArgument.get(parameterName)!; } @@ -50,7 +51,7 @@ export class Context { public getParameters(): readonly string[] { return this.parameters; } - public getArguments(): readonly (BaseType | undefined)[] { + public getArguments(): readonly BaseType[] { return this.arguments; } @@ -60,5 +61,5 @@ export class Context { } export interface NodeParser { - createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType | undefined; + createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType; } diff --git a/src/NodeParser/AnnotatedNodeParser.ts b/src/NodeParser/AnnotatedNodeParser.ts index ae4b5534d..f46e11309 100644 --- a/src/NodeParser/AnnotatedNodeParser.ts +++ b/src/NodeParser/AnnotatedNodeParser.ts @@ -18,7 +18,7 @@ export class AnnotatedNodeParser implements SubNodeParser { return this.childNodeParser.supportsNode(node); } - public createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType | undefined { + public createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType { const annotatedNode = this.getAnnotatedNode(node); let annotations = this.annotationsReader.getAnnotations(annotatedNode); const nullable = this.getNullable(annotatedNode); @@ -30,10 +30,6 @@ export class AnnotatedNodeParser implements SubNodeParser { const baseType = this.childNodeParser.createType(node, context, reference); - if (baseType === undefined) { - return undefined; - } - // Don't return annotations for lib types such as Exclude. if (node.getSourceFile().fileName.match(/[/\\]typescript[/\\]lib[/\\]lib\.[^/\\]+\.d\.ts$/i)) { let specialCase = false; diff --git a/src/NodeParser/ArrayLiteralExpressionNodeParser.ts b/src/NodeParser/ArrayLiteralExpressionNodeParser.ts index cfb243b12..f114b298c 100644 --- a/src/NodeParser/ArrayLiteralExpressionNodeParser.ts +++ b/src/NodeParser/ArrayLiteralExpressionNodeParser.ts @@ -2,7 +2,6 @@ import ts from "typescript"; import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { BaseType } from "../Type/BaseType"; -import { notUndefined } from "../Utils/notUndefined"; import { TupleType } from "../Type/TupleType"; export class ArrayLiteralExpressionNodeParser implements SubNodeParser { @@ -12,14 +11,8 @@ export class ArrayLiteralExpressionNodeParser implements SubNodeParser { return node.kind === ts.SyntaxKind.ArrayLiteralExpression; } - public createType(node: ts.ArrayLiteralExpression, context: Context): BaseType | undefined { - if (node.elements) { - const elements = node.elements.map((t) => this.childNodeParser.createType(t, context)).filter(notUndefined); - - return new TupleType(elements); - } - - // TODO: implement this? - return undefined; + public createType(node: ts.ArrayLiteralExpression, context: Context): BaseType { + const elements = node.elements.map((t) => this.childNodeParser.createType(t, context)); + return new TupleType(elements); } } diff --git a/src/NodeParser/ArrayNodeParser.ts b/src/NodeParser/ArrayNodeParser.ts index e64318282..eb9fd357a 100644 --- a/src/NodeParser/ArrayNodeParser.ts +++ b/src/NodeParser/ArrayNodeParser.ts @@ -11,11 +11,8 @@ export class ArrayNodeParser implements SubNodeParser { return node.kind === ts.SyntaxKind.ArrayType; } - public createType(node: ts.ArrayTypeNode, context: Context): BaseType | undefined { + public createType(node: ts.ArrayTypeNode, context: Context): BaseType { const type = this.childNodeParser.createType(node.elementType, context); - if (type === undefined) { - return undefined; - } return new ArrayType(type); } } diff --git a/src/NodeParser/AsExpressionNodeParser.ts b/src/NodeParser/AsExpressionNodeParser.ts index b5ac574be..e6b7929ec 100644 --- a/src/NodeParser/AsExpressionNodeParser.ts +++ b/src/NodeParser/AsExpressionNodeParser.ts @@ -10,7 +10,7 @@ export class AsExpressionNodeParser implements SubNodeParser { public supportsNode(node: ts.AsExpression): boolean { return node.kind === ts.SyntaxKind.AsExpression; } - public createType(node: ts.AsExpression, context: Context): BaseType | undefined { + public createType(node: ts.AsExpression, context: Context): BaseType { // only implement `as const` for now where we just ignore the as expression return this.childNodeParser.createType(node.expression, context); } diff --git a/src/NodeParser/ConditionalTypeNodeParser.ts b/src/NodeParser/ConditionalTypeNodeParser.ts index 1cd68ac27..e2104d4b8 100644 --- a/src/NodeParser/ConditionalTypeNodeParser.ts +++ b/src/NodeParser/ConditionalTypeNodeParser.ts @@ -5,9 +5,10 @@ import { BaseType } from "../Type/BaseType"; import { isAssignableTo } from "../Utils/isAssignableTo"; import { narrowType } from "../Utils/narrowType"; import { UnionType } from "../Type/UnionType"; +import { NeverType } from "../Type/NeverType"; class CheckType { - constructor(public parameterName: string, public type: BaseType | undefined) {} + constructor(public parameterName: string, public type: BaseType) {} } export class ConditionalTypeNodeParser implements SubNodeParser { @@ -17,7 +18,7 @@ export class ConditionalTypeNodeParser implements SubNodeParser { return node.kind === ts.SyntaxKind.ConditionalType; } - public createType(node: ts.ConditionalTypeNode, context: Context): BaseType | undefined { + 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 checkTypeParameterName = this.getTypeParameterName(node.checkType); @@ -39,7 +40,7 @@ export class ConditionalTypeNodeParser implements SubNodeParser { // Follow the relevant branches and return the results from them const results: BaseType[] = []; - if (trueCheckType !== undefined) { + if (!(trueCheckType instanceof NeverType)) { const result = this.childNodeParser.createType( node.trueType, this.createSubContext(node, context, new CheckType(checkTypeParameterName, trueCheckType), inferMap) @@ -48,7 +49,7 @@ export class ConditionalTypeNodeParser implements SubNodeParser { results.push(result); } } - if (falseCheckType !== undefined) { + if (!(falseCheckType instanceof NeverType)) { const result = this.childNodeParser.createType( node.falseType, this.createSubContext(node, context, new CheckType(checkTypeParameterName, falseCheckType)) diff --git a/src/NodeParser/ExpressionWithTypeArgumentsNodeParser.ts b/src/NodeParser/ExpressionWithTypeArgumentsNodeParser.ts index 93450e4fe..d036feaf6 100644 --- a/src/NodeParser/ExpressionWithTypeArgumentsNodeParser.ts +++ b/src/NodeParser/ExpressionWithTypeArgumentsNodeParser.ts @@ -9,7 +9,7 @@ export class ExpressionWithTypeArgumentsNodeParser implements SubNodeParser { public supportsNode(node: ts.ExpressionWithTypeArguments): boolean { return node.kind === ts.SyntaxKind.ExpressionWithTypeArguments; } - public createType(node: ts.ExpressionWithTypeArguments, context: Context): BaseType | undefined { + public createType(node: ts.ExpressionWithTypeArguments, context: Context): BaseType { const typeSymbol = this.typeChecker.getSymbolAtLocation(node.expression)!; if (typeSymbol.flags & ts.SymbolFlags.Alias) { const aliasedSymbol = this.typeChecker.getAliasedSymbol(typeSymbol); diff --git a/src/NodeParser/HiddenTypeNodeParser.ts b/src/NodeParser/HiddenTypeNodeParser.ts index 61d5aada0..6fc7a2f17 100644 --- a/src/NodeParser/HiddenTypeNodeParser.ts +++ b/src/NodeParser/HiddenTypeNodeParser.ts @@ -2,6 +2,7 @@ import ts from "typescript"; import { Context } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { BaseType } from "../Type/BaseType"; +import { NeverType } from "../Type/NeverType"; import { isNodeHidden } from "../Utils/isHidden"; export class HiddenNodeParser implements SubNodeParser { @@ -11,7 +12,7 @@ export class HiddenNodeParser implements SubNodeParser { return isNodeHidden(node); } - public createType(node: ts.KeywordTypeNode, context: Context): BaseType | undefined { - return undefined; + public createType(_node: ts.KeywordTypeNode, _context: Context): BaseType { + return new NeverType(); } } diff --git a/src/NodeParser/IndexedAccessTypeNodeParser.ts b/src/NodeParser/IndexedAccessTypeNodeParser.ts index a16f96dd9..b1d09bbb5 100644 --- a/src/NodeParser/IndexedAccessTypeNodeParser.ts +++ b/src/NodeParser/IndexedAccessTypeNodeParser.ts @@ -4,6 +4,7 @@ import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { BaseType } from "../Type/BaseType"; import { LiteralType } from "../Type/LiteralType"; +import { NeverType } from "../Type/NeverType"; import { NumberType } from "../Type/NumberType"; import { StringType } from "../Type/StringType"; import { TupleType } from "../Type/TupleType"; @@ -18,7 +19,7 @@ export class IndexedAccessTypeNodeParser implements SubNodeParser { return node.kind === ts.SyntaxKind.IndexedAccessType; } - private createIndexedType(objectType: ts.TypeNode, context: Context, indexType: BaseType) { + private createIndexedType(objectType: ts.TypeNode, context: Context, indexType: BaseType): BaseType | undefined { if (ts.isTypeReferenceNode(objectType) && indexType instanceof LiteralType) { const declaration = this.typeChecker.getSymbolAtLocation(objectType.typeName)?.declarations?.[0]; @@ -40,17 +41,17 @@ export class IndexedAccessTypeNodeParser implements SubNodeParser { return undefined; } - public createType(node: ts.IndexedAccessTypeNode, context: Context): BaseType | undefined { + public createType(node: ts.IndexedAccessTypeNode, context: Context): BaseType { const indexType = derefType(this.childNodeParser.createType(node.indexType, context)); - const indexedType = indexType && this.createIndexedType(node.objectType, context, indexType); + const indexedType = this.createIndexedType(node.objectType, context, indexType); if (indexedType) { return indexedType; } const objectType = derefType(this.childNodeParser.createType(node.objectType, context)); - if (objectType === undefined || indexType === undefined) { - return undefined; + if (objectType instanceof NeverType || indexType instanceof NeverType) { + return new NeverType(); } const indexTypes = indexType instanceof UnionType ? indexType.getTypes() : [indexType]; diff --git a/src/NodeParser/InferTypeNodeParser.ts b/src/NodeParser/InferTypeNodeParser.ts index 5520205fd..5f707f5f5 100644 --- a/src/NodeParser/InferTypeNodeParser.ts +++ b/src/NodeParser/InferTypeNodeParser.ts @@ -11,7 +11,7 @@ export class InferTypeNodeParser implements SubNodeParser { return node.kind === ts.SyntaxKind.InferType; } - public createType(node: ts.InferTypeNode, _context: Context): BaseType | undefined { + public createType(node: ts.InferTypeNode, _context: Context): BaseType { return new InferType(node.typeParameter.name.escapedText.toString()); } } diff --git a/src/NodeParser/InterfaceAndClassNodeParser.ts b/src/NodeParser/InterfaceAndClassNodeParser.ts index 48f49e390..4b0287231 100644 --- a/src/NodeParser/InterfaceAndClassNodeParser.ts +++ b/src/NodeParser/InterfaceAndClassNodeParser.ts @@ -3,12 +3,12 @@ import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { ArrayType } from "../Type/ArrayType"; import { BaseType } from "../Type/BaseType"; +import { NeverType } from "../Type/NeverType"; import { ObjectProperty, ObjectType } from "../Type/ObjectType"; import { ReferenceType } from "../Type/ReferenceType"; import { isNodeHidden } from "../Utils/isHidden"; import { isPublic, isStatic } from "../Utils/modifiers"; import { getKey } from "../Utils/nodeKey"; -import { notUndefined } from "../Utils/notUndefined"; export class InterfaceAndClassNodeParser implements SubNodeParser { public constructor( @@ -25,7 +25,7 @@ export class InterfaceAndClassNodeParser implements SubNodeParser { node: ts.InterfaceDeclaration | ts.ClassDeclaration, context: Context, reference?: ReferenceType - ): BaseType | undefined { + ): BaseType { if (node.typeParameters?.length) { node.typeParameters.forEach((typeParam) => { const nameSymbol = this.typeChecker.getSymbolAtLocation(typeParam.name)!; @@ -47,7 +47,7 @@ export class InterfaceAndClassNodeParser implements SubNodeParser { const properties = this.getProperties(node, context); if (properties === undefined) { - return undefined; + return new NeverType(); } const additionalProperties = this.getAdditionalProperties(node, context); @@ -56,11 +56,7 @@ export class InterfaceAndClassNodeParser implements SubNodeParser { if (properties.length === 0 && additionalProperties === false) { const arrayItemType = this.getArrayItemType(node); if (arrayItemType) { - const type = this.childNodeParser.createType(arrayItemType, context); - if (type === undefined) { - return undefined; - } - return new ArrayType(type); + return new ArrayType(this.childNodeParser.createType(arrayItemType, context)); } } @@ -99,9 +95,7 @@ export class InterfaceAndClassNodeParser implements SubNodeParser { return node.heritageClauses.reduce( (result: BaseType[], baseType) => [ ...result, - ...baseType.types - .map((expression) => this.childNodeParser.createType(expression, context)) - .filter(notUndefined), + ...baseType.types.map((expression) => this.childNodeParser.createType(expression, context)), ], [] ); @@ -135,10 +129,10 @@ export class InterfaceAndClassNodeParser implements SubNodeParser { ) ) .filter((prop) => { - if (prop.isRequired() && prop.getType() === undefined) { + if (prop.isRequired() && prop.getType() instanceof NeverType) { hasRequiredNever = true; } - return prop.getType() !== undefined; + return !(prop.getType() instanceof NeverType); }); if (hasRequiredNever) { diff --git a/src/NodeParser/IntersectionNodeParser.ts b/src/NodeParser/IntersectionNodeParser.ts index e3c37c860..f5d74f5f0 100644 --- a/src/NodeParser/IntersectionNodeParser.ts +++ b/src/NodeParser/IntersectionNodeParser.ts @@ -8,6 +8,7 @@ import { UnionType } from "../Type/UnionType"; import { derefType } from "../Utils/derefType"; import { uniqueTypeArray } from "../Utils/uniqueTypeArray"; import { UndefinedType } from "../Type/UndefinedType"; +import { NeverType } from "../Type/NeverType"; export class IntersectionNodeParser implements SubNodeParser { public constructor(protected typeChecker: ts.TypeChecker, protected childNodeParser: NodeParser) {} @@ -16,12 +17,12 @@ export class IntersectionNodeParser implements SubNodeParser { return node.kind === ts.SyntaxKind.IntersectionType; } - public createType(node: ts.IntersectionTypeNode, context: Context): BaseType | undefined { + public createType(node: ts.IntersectionTypeNode, context: Context): BaseType { const types = node.types.map((subnode) => this.childNodeParser.createType(subnode, context)); - // if any type is undefined (never), an intersection type should be undefined (never) - if (types.filter((t) => t === undefined).length) { - return undefined; + // if any type is never, the intersection type resolves to never + if (types.filter((t) => t instanceof NeverType).length) { + return new NeverType(); } return translate(types as BaseType[]); @@ -42,7 +43,7 @@ function derefAndFlattenUnions(type: BaseType): BaseType[] { * Translates the given intersection type into a union type if necessary so `A & (B | C)` becomes * `(A & B) | (A & C)`. If no translation is needed then the original intersection type is returned. */ -export function translate(types: BaseType[]): BaseType | undefined { +export function translate(types: BaseType[]): BaseType { types = uniqueTypeArray(types as BaseType[]); if (types.length == 1) { @@ -85,5 +86,5 @@ export function translate(types: BaseType[]): BaseType | undefined { return new UnionType(result); } - return undefined; + throw new Error("Could not translate intersection to union."); } diff --git a/src/NodeParser/IntrinsicNodeParser.ts b/src/NodeParser/IntrinsicNodeParser.ts index 51b016cb1..3d8f76ac6 100644 --- a/src/NodeParser/IntrinsicNodeParser.ts +++ b/src/NodeParser/IntrinsicNodeParser.ts @@ -18,7 +18,7 @@ export class IntrinsicNodeParser implements SubNodeParser { public supportsNode(node: ts.KeywordTypeNode): boolean { return node.kind === ts.SyntaxKind.IntrinsicKeyword; } - public createType(node: ts.KeywordTypeNode, context: Context): BaseType | undefined { + public createType(node: ts.KeywordTypeNode, context: Context): BaseType { const methodName = getParentName(node); const method = intrinsicMethods[methodName]; assert(method, `Unknown intrinsic method: ${methodName}`); diff --git a/src/NodeParser/LiteralNodeParser.ts b/src/NodeParser/LiteralNodeParser.ts index 86c852ac4..bf81a977f 100644 --- a/src/NodeParser/LiteralNodeParser.ts +++ b/src/NodeParser/LiteralNodeParser.ts @@ -9,7 +9,7 @@ export class LiteralNodeParser implements SubNodeParser { public supportsNode(node: ts.LiteralTypeNode): boolean { return node.kind === ts.SyntaxKind.LiteralType; } - public createType(node: ts.LiteralTypeNode, context: Context): BaseType | undefined { + public createType(node: ts.LiteralTypeNode, context: Context): BaseType { return this.childNodeParser.createType(node.literal, context); } } diff --git a/src/NodeParser/MappedTypeNodeParser.ts b/src/NodeParser/MappedTypeNodeParser.ts index 1bc7caaae..ef0331db2 100644 --- a/src/NodeParser/MappedTypeNodeParser.ts +++ b/src/NodeParser/MappedTypeNodeParser.ts @@ -16,7 +16,6 @@ import { UnionType } from "../Type/UnionType"; import assert from "../Utils/assert"; import { derefAnnotatedType, derefType } from "../Utils/derefType"; import { getKey } from "../Utils/nodeKey"; -import { notUndefined } from "../Utils/notUndefined"; import { preserveAnnotation } from "../Utils/preserveAnnotation"; import { removeUndefined } from "../Utils/removeUndefined"; @@ -27,7 +26,7 @@ export class MappedTypeNodeParser implements SubNodeParser { return node.kind === ts.SyntaxKind.MappedType; } - public createType(node: ts.MappedTypeNode, context: Context): BaseType | undefined { + public createType(node: ts.MappedTypeNode, context: Context): BaseType { const constraintType = this.childNodeParser.createType(node.typeParameter.constraint!, context); const keyListType = derefType(constraintType); const id = `indexed-type-${getKey(node, context)}`; @@ -53,11 +52,12 @@ export class MappedTypeNodeParser implements SubNodeParser { node.type!, this.createSubContext(node, keyListType, context) ); - return type === undefined ? undefined : new ArrayType(type); + return type instanceof NeverType ? new NeverType() : new ArrayType(type); } // Key type widens to `string` const type = this.childNodeParser.createType(node.type!, context); - const resultType = type === undefined ? undefined : new ObjectType(id, [], [], type); + // const resultType = type instanceof NeverType ? new NeverType() : new ObjectType(id, [], [], type); + const resultType = new ObjectType(id, [], [], type); if (resultType && constraintType instanceof AnnotatedType) { const annotations = constraintType.getAnnotations(); if (annotations) { @@ -69,8 +69,6 @@ export class MappedTypeNodeParser implements SubNodeParser { return new ObjectType(id, [], this.getValues(node, keyListType, context), false); } else if (keyListType instanceof NeverType) { return new ObjectType(id, [], [], false); - } else if (keyListType === undefined) { - return new ObjectType(id, [], [], false); } else { throw new LogicError( // eslint-disable-next-line max-len @@ -103,10 +101,6 @@ export class MappedTypeNodeParser implements SubNodeParser { this.createSubContext(node, key, context) ); - if (propertyType === undefined) { - return result; - } - let newType = derefAnnotatedType(propertyType); let hasUndefined = false; if (newType instanceof UnionType) { @@ -136,13 +130,8 @@ export class MappedTypeNodeParser implements SubNodeParser { this.createSubContext(node, new LiteralType(value!), context) ); - if (type === undefined) { - return undefined; - } - return new ObjectProperty(value!.toString(), type, !node.questionToken); - }) - .filter(notUndefined); + }); } protected getAdditionalProperties( diff --git a/src/NodeParser/NamedTupleMemberNodeParser.ts b/src/NodeParser/NamedTupleMemberNodeParser.ts index 2f3478d53..5f33c2216 100644 --- a/src/NodeParser/NamedTupleMemberNodeParser.ts +++ b/src/NodeParser/NamedTupleMemberNodeParser.ts @@ -14,7 +14,7 @@ export class NamedTupleMemberNodeParser implements SubNodeParser { return node.kind === ts.SyntaxKind.NamedTupleMember; } - public createType(node: ts.NamedTupleMember, context: Context, reference?: ReferenceType): BaseType | undefined { + public createType(node: ts.NamedTupleMember, context: Context, reference?: ReferenceType): BaseType { const baseType = this.childNodeParser.createType(node.type, context, reference); if (baseType instanceof ArrayType && node.getChildAt(0).kind === ts.SyntaxKind.DotDotDotToken) { diff --git a/src/NodeParser/NeverTypeNodeParser.ts b/src/NodeParser/NeverTypeNodeParser.ts index 364738b0e..46d4c4fb6 100644 --- a/src/NodeParser/NeverTypeNodeParser.ts +++ b/src/NodeParser/NeverTypeNodeParser.ts @@ -8,7 +8,7 @@ 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 | undefined { + public createType(_node: ts.KeywordTypeNode, _context: Context): BaseType { return new NeverType(); } } diff --git a/src/NodeParser/ObjectLiteralExpressionNodeParser.ts b/src/NodeParser/ObjectLiteralExpressionNodeParser.ts index 0019a13bb..e741e43ac 100644 --- a/src/NodeParser/ObjectLiteralExpressionNodeParser.ts +++ b/src/NodeParser/ObjectLiteralExpressionNodeParser.ts @@ -13,21 +13,16 @@ export class ObjectLiteralExpressionNodeParser implements SubNodeParser { return node.kind === ts.SyntaxKind.ObjectLiteralExpression; } - public createType(node: ts.ObjectLiteralExpression, context: Context): BaseType | undefined { - if (node.properties) { - const properties = node.properties.map( - (t) => - new ObjectProperty( - t.name!.getText(), - this.childNodeParser.createType((t as any).initializer, context), - !(t as any).questionToken - ) - ); + public createType(node: ts.ObjectLiteralExpression, context: Context): BaseType { + const properties = node.properties.map( + (t) => + new ObjectProperty( + t.name!.getText(), + this.childNodeParser.createType((t as any).initializer, context), + !(t as any).questionToken + ) + ); - return new ObjectType(`object-${getKey(node, context)}`, [], properties, false); - } - - // TODO: implement this? - return undefined; + return new ObjectType(`object-${getKey(node, context)}`, [], properties, false); } } diff --git a/src/NodeParser/OptionalTypeNodeParser.ts b/src/NodeParser/OptionalTypeNodeParser.ts index 1a4a7b375..ed0ab2ea5 100644 --- a/src/NodeParser/OptionalTypeNodeParser.ts +++ b/src/NodeParser/OptionalTypeNodeParser.ts @@ -9,11 +9,8 @@ export class OptionalTypeNodeParser implements SubNodeParser { public supportsNode(node: ts.OptionalTypeNode): boolean { return node.kind === ts.SyntaxKind.OptionalType; } - public createType(node: ts.OptionalTypeNode, context: Context): BaseType | undefined { + public createType(node: ts.OptionalTypeNode, context: Context): BaseType { const type = this.childNodeParser.createType(node.type, context); - if (!type) { - return undefined; - } return new OptionalType(type); } } diff --git a/src/NodeParser/ParameterParser.ts b/src/NodeParser/ParameterParser.ts index 9713744a7..b98776406 100644 --- a/src/NodeParser/ParameterParser.ts +++ b/src/NodeParser/ParameterParser.ts @@ -10,7 +10,7 @@ export class ParameterParser implements SubNodeParser { public supportsNode(node: ts.ParameterDeclaration): boolean { return node.kind === ts.SyntaxKind.Parameter; } - public createType(node: ts.FunctionTypeNode, context: Context): BaseType | undefined { + public createType(node: ts.FunctionTypeNode, context: Context): BaseType { return this.childNodeParser.createType(node.type, context); } } diff --git a/src/NodeParser/ParenthesizedNodeParser.ts b/src/NodeParser/ParenthesizedNodeParser.ts index 6e23553ef..4957f965b 100644 --- a/src/NodeParser/ParenthesizedNodeParser.ts +++ b/src/NodeParser/ParenthesizedNodeParser.ts @@ -9,11 +9,7 @@ export class ParenthesizedNodeParser implements SubNodeParser { public supportsNode(node: ts.ParenthesizedTypeNode): boolean { return node.kind === ts.SyntaxKind.ParenthesizedType; } - public createType(node: ts.ParenthesizedTypeNode, context: Context): BaseType | undefined { - const type = this.childNodeParser.createType(node.type, context); - if (!type) { - return undefined; - } - return type; + public createType(node: ts.ParenthesizedTypeNode, context: Context): BaseType { + return this.childNodeParser.createType(node.type, context); } } diff --git a/src/NodeParser/PropertyAccessExpressionParser.ts b/src/NodeParser/PropertyAccessExpressionParser.ts index 59c2d7e75..3048c1dd3 100644 --- a/src/NodeParser/PropertyAccessExpressionParser.ts +++ b/src/NodeParser/PropertyAccessExpressionParser.ts @@ -1,4 +1,4 @@ -import * as ts from "typescript"; +import ts from "typescript"; import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { BaseType } from "../Type/BaseType"; @@ -10,7 +10,7 @@ export class PropertyAccessExpressionParser implements SubNodeParser { return node.kind === ts.SyntaxKind.PropertyAccessExpression; } - public createType(node: ts.PropertyAccessExpression, context: Context): BaseType | undefined { + public createType(node: ts.PropertyAccessExpression, context: Context): BaseType { const type = this.typeChecker.getTypeAtLocation(node); return this.childNodeParser.createType(type.symbol.declarations![0], context); } diff --git a/src/NodeParser/TypeAliasNodeParser.ts b/src/NodeParser/TypeAliasNodeParser.ts index 38d01508a..8dbb21037 100644 --- a/src/NodeParser/TypeAliasNodeParser.ts +++ b/src/NodeParser/TypeAliasNodeParser.ts @@ -3,6 +3,7 @@ import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { AliasType } from "../Type/AliasType"; import { BaseType } from "../Type/BaseType"; +import { NeverType } from "../Type/NeverType"; import { ReferenceType } from "../Type/ReferenceType"; import { getKey } from "../Utils/nodeKey"; @@ -13,11 +14,7 @@ export class TypeAliasNodeParser implements SubNodeParser { return node.kind === ts.SyntaxKind.TypeAliasDeclaration; } - public createType( - node: ts.TypeAliasDeclaration, - context: Context, - reference?: ReferenceType - ): BaseType | undefined { + public createType(node: ts.TypeAliasDeclaration, context: Context, reference?: ReferenceType): BaseType { if (node.typeParameters?.length) { for (const typeParam of node.typeParameters) { const nameSymbol = this.typeChecker.getSymbolAtLocation(typeParam.name)!; @@ -38,8 +35,8 @@ export class TypeAliasNodeParser implements SubNodeParser { } const type = this.childNodeParser.createType(node.type, context); - if (type === undefined) { - return undefined; + if (type instanceof NeverType) { + return new NeverType(); } return new AliasType(id, type); } diff --git a/src/NodeParser/TypeLiteralNodeParser.ts b/src/NodeParser/TypeLiteralNodeParser.ts index d5c395fc0..a533f5543 100644 --- a/src/NodeParser/TypeLiteralNodeParser.ts +++ b/src/NodeParser/TypeLiteralNodeParser.ts @@ -2,6 +2,7 @@ import ts from "typescript"; import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { BaseType } from "../Type/BaseType"; +import { NeverType } from "../Type/NeverType"; import { ObjectProperty, ObjectType } from "../Type/ObjectType"; import { ReferenceType } from "../Type/ReferenceType"; import { isNodeHidden } from "../Utils/isHidden"; @@ -13,7 +14,7 @@ export class TypeLiteralNodeParser implements SubNodeParser { public supportsNode(node: ts.TypeLiteralNode): boolean { return node.kind === ts.SyntaxKind.TypeLiteral; } - public createType(node: ts.TypeLiteralNode, context: Context, reference?: ReferenceType): BaseType | undefined { + public createType(node: ts.TypeLiteralNode, context: Context, reference?: ReferenceType): BaseType { const id = this.getTypeId(node, context); if (reference) { reference.setId(id); @@ -23,7 +24,7 @@ export class TypeLiteralNodeParser implements SubNodeParser { const properties = this.getProperties(node, context); if (properties === undefined) { - return undefined; + return new NeverType(); } return new ObjectType(id, [], properties, this.getAdditionalProperties(node, context)); @@ -43,10 +44,10 @@ export class TypeLiteralNodeParser implements SubNodeParser { return objectProperty; }) .filter((prop) => { - if (prop.isRequired() && prop.getType() === undefined) { + if (prop.isRequired() && prop.getType() instanceof NeverType) { hasRequiredNever = true; } - return prop.getType() !== undefined; + return !(prop.getType() instanceof NeverType); }); if (hasRequiredNever) { diff --git a/src/NodeParser/TypeReferenceNodeParser.ts b/src/NodeParser/TypeReferenceNodeParser.ts index f961eb408..2e9b14c64 100644 --- a/src/NodeParser/TypeReferenceNodeParser.ts +++ b/src/NodeParser/TypeReferenceNodeParser.ts @@ -4,6 +4,7 @@ import { SubNodeParser } from "../SubNodeParser"; import { AnnotatedType } from "../Type/AnnotatedType"; import { ArrayType } from "../Type/ArrayType"; import { BaseType } from "../Type/BaseType"; +import { NeverType } from "../Type/NeverType"; import { StringType } from "../Type/StringType"; const invalidTypes: { [index: number]: boolean } = { @@ -18,7 +19,7 @@ export class TypeReferenceNodeParser implements SubNodeParser { return node.kind === ts.SyntaxKind.TypeReference; } - public createType(node: ts.TypeReferenceNode, context: Context): BaseType | undefined { + public createType(node: ts.TypeReferenceNode, context: Context): BaseType { const typeSymbol = this.typeChecker.getSymbolAtLocation(node.typeName)!; if (typeSymbol.flags & ts.SymbolFlags.Alias) { const aliasedSymbol = this.typeChecker.getAliasedSymbol(typeSymbol); @@ -30,8 +31,8 @@ export class TypeReferenceNodeParser implements SubNodeParser { return context.getArgument(typeSymbol.name); } else if (typeSymbol.name === "Array" || typeSymbol.name === "ReadonlyArray") { const type = this.createSubContext(node, context).getArguments()[0]; - if (type === undefined) { - return undefined; + if (type === undefined || type instanceof NeverType) { + return new NeverType(); } return new ArrayType(type); } else if (typeSymbol.name === "Date") { diff --git a/src/NodeParser/TypeofNodeParser.ts b/src/NodeParser/TypeofNodeParser.ts index a1c229d6b..8746a50b6 100644 --- a/src/NodeParser/TypeofNodeParser.ts +++ b/src/NodeParser/TypeofNodeParser.ts @@ -16,7 +16,7 @@ export class TypeofNodeParser implements SubNodeParser { return node.kind === ts.SyntaxKind.TypeQuery; } - public createType(node: ts.TypeQueryNode, context: Context, reference?: ReferenceType): BaseType | undefined { + public createType(node: ts.TypeQueryNode, context: Context, reference?: ReferenceType): BaseType { let symbol = this.typeChecker.getSymbolAtLocation(node.exprName)!; if (symbol.flags & ts.SymbolFlags.Alias) { symbol = this.typeChecker.getAliasedSymbol(symbol); @@ -59,7 +59,7 @@ export class TypeofNodeParser implements SubNodeParser { reference.setName(id); } - let type: BaseType | null | undefined = null; + let type: BaseType | null = null; const properties = node.members.map((member) => { const name = member.name.getText(); if (member.initializer) { diff --git a/src/NodeParser/UnionNodeParser.ts b/src/NodeParser/UnionNodeParser.ts index ab109eaf4..e7a7123dd 100644 --- a/src/NodeParser/UnionNodeParser.ts +++ b/src/NodeParser/UnionNodeParser.ts @@ -3,7 +3,8 @@ import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { UnionType } from "../Type/UnionType"; import { BaseType } from "../Type/BaseType"; -import { notUndefined } from "../Utils/notUndefined"; +import { notNever } from "../Utils/notNever"; +import { NeverType } from "../Type/NeverType"; export class UnionNodeParser implements SubNodeParser { public constructor(protected typeChecker: ts.TypeChecker, protected childNodeParser: NodeParser) {} @@ -12,17 +13,17 @@ export class UnionNodeParser implements SubNodeParser { return node.kind === ts.SyntaxKind.UnionType; } - public createType(node: ts.UnionTypeNode, context: Context): BaseType | undefined { + public createType(node: ts.UnionTypeNode, context: Context): BaseType { const types = node.types .map((subnode) => { return this.childNodeParser.createType(subnode, context); }) - .filter(notUndefined); + .filter(notNever); if (types.length === 1) { return types[0]; } else if (types.length === 0) { - return undefined; + return new NeverType(); } return new UnionType(types); diff --git a/src/SchemaGenerator.ts b/src/SchemaGenerator.ts index 62db8b6e8..00f4d5802 100644 --- a/src/SchemaGenerator.ts +++ b/src/SchemaGenerator.ts @@ -8,7 +8,6 @@ import { DefinitionType } from "./Type/DefinitionType"; import { TypeFormatter } from "./TypeFormatter"; import { StringMap } from "./Utils/StringMap"; import { localSymbolAtNode, symbolAtNode } from "./Utils/symbolAtNode"; -import { notUndefined } from "./Utils/notUndefined"; import { removeUnreachable } from "./Utils/removeUnreachable"; import { Config } from "./Config"; import { hasJsDocTag } from "./Utils/hasJsDocTag"; @@ -27,11 +26,10 @@ export class SchemaGenerator { } public createSchemaFromNodes(rootNodes: ts.Node[]): Schema { - const rootTypes = rootNodes - .map((rootNode) => { - return this.nodeParser.createType(rootNode, new Context()); - }) - .filter(notUndefined); + const rootTypes = rootNodes.map((rootNode) => { + return this.nodeParser.createType(rootNode, new Context()); + }); + const rootTypeDefinition = rootTypes.length === 1 ? this.getRootTypeDefinition(rootTypes[0]) : undefined; const definitions: StringMap = {}; rootTypes.forEach((rootType) => this.appendRootChildDefinitions(rootType, definitions)); diff --git a/src/TopRefNodeParser.ts b/src/TopRefNodeParser.ts index b6280d216..1a6b9a4a9 100644 --- a/src/TopRefNodeParser.ts +++ b/src/TopRefNodeParser.ts @@ -10,13 +10,9 @@ export class TopRefNodeParser implements NodeParser { protected topRef: boolean ) {} - public createType(node: ts.Node, context: Context): BaseType | undefined { + public createType(node: ts.Node, context: Context): BaseType { const baseType = this.childNodeParser.createType(node, context); - if (baseType === undefined) { - return undefined; - } - if (this.topRef && !(baseType instanceof DefinitionType)) { return new DefinitionType(this.fullName, baseType); } else if (!this.topRef && baseType instanceof DefinitionType) { diff --git a/src/Type/ObjectType.ts b/src/Type/ObjectType.ts index 872452c53..7ce8836b4 100644 --- a/src/Type/ObjectType.ts +++ b/src/Type/ObjectType.ts @@ -2,12 +2,12 @@ import { BaseType } from "./BaseType"; import { strip } from "../Utils/String"; export class ObjectProperty { - public constructor(private name: string, private type: BaseType | undefined, private required: boolean) {} + public constructor(private name: string, private type: BaseType, private required: boolean) {} public getName(): string { return strip(this.name); } - public getType(): BaseType | undefined { + public getType(): BaseType { return this.type; } public isRequired(): boolean { diff --git a/src/Type/TupleType.ts b/src/Type/TupleType.ts index 7366c9b55..31b8e5f7a 100644 --- a/src/Type/TupleType.ts +++ b/src/Type/TupleType.ts @@ -4,8 +4,8 @@ import { BaseType } from "./BaseType"; import { InferType } from "./InferType"; import { RestType } from "./RestType"; -function normalize(types: Readonly>): Array { - let normalized: Array = []; +function normalize(types: Readonly>): Array { + let normalized: Array = []; for (const type of types) { if (type instanceof RestType) { @@ -22,9 +22,9 @@ function normalize(types: Readonly>): Array>; + private types: Readonly>; - public constructor(types: Readonly>) { + public constructor(types: Readonly>) { super(); this.types = normalize(types); @@ -34,7 +34,7 @@ export class TupleType extends BaseType { return `[${this.types.map((item) => item?.getId() ?? "never").join(",")}]`; } - public getTypes(): Readonly> { + public getTypes(): Readonly> { return this.types; } } diff --git a/src/Type/UnionType.ts b/src/Type/UnionType.ts index 9cd0f8cb8..3f28023d3 100644 --- a/src/Type/UnionType.ts +++ b/src/Type/UnionType.ts @@ -1,17 +1,18 @@ import { BaseType } from "./BaseType"; import { uniqueTypeArray } from "../Utils/uniqueTypeArray"; import { NeverType } from "./NeverType"; +import { derefType } from "../Utils/derefType"; export class UnionType extends BaseType { private readonly types: BaseType[]; - public constructor(types: readonly (BaseType | undefined)[]) { + public constructor(types: readonly BaseType[]) { super(); this.types = uniqueTypeArray( types.reduce((flatTypes, type) => { if (type instanceof UnionType) { flatTypes.push(...type.getTypes()); - } else if (type !== undefined && !(type instanceof NeverType)) { + } else if (!(type instanceof NeverType)) { flatTypes.push(type); } return flatTypes; @@ -31,13 +32,20 @@ export class UnionType extends BaseType { return this.types; } - public normalize(): BaseType | undefined { + public normalize(): BaseType { if (this.types.length === 0) { - return undefined; + return new NeverType(); } else if (this.types.length === 1) { return this.types[0]; } else { - return this; + const union = new UnionType(this.types.filter((type) => !(derefType(type) instanceof NeverType))); + + if (union.getTypes().length > 1) { + return union; + } else { + return union.normalize(); + } + // return this; } } } diff --git a/src/TypeFormatter/ObjectTypeFormatter.ts b/src/TypeFormatter/ObjectTypeFormatter.ts index 8125e7f0a..af34e77a5 100644 --- a/src/TypeFormatter/ObjectTypeFormatter.ts +++ b/src/TypeFormatter/ObjectTypeFormatter.ts @@ -13,6 +13,7 @@ import { preserveAnnotation } from "../Utils/preserveAnnotation"; import { removeUndefined } from "../Utils/removeUndefined"; import { StringMap } from "../Utils/StringMap"; import { uniqueArray } from "../Utils/uniqueArray"; +import { NeverType } from "../Type/NeverType"; export class ObjectTypeFormatter implements SubTypeFormatter { public constructor(protected childTypeFormatter: TypeFormatter) {} @@ -46,7 +47,7 @@ export class ObjectTypeFormatter implements SubTypeFormatter { const childrenOfProps = properties.reduce((result: BaseType[], property) => { const propertyType = property.getType(); - if (propertyType === undefined) { + if (propertyType instanceof NeverType) { return result; } @@ -59,9 +60,15 @@ export class ObjectTypeFormatter implements SubTypeFormatter { } protected getObjectDefinition(type: ObjectType): Definition { - const objectProperties = type.getProperties(); + let objectProperties = type.getProperties(); const additionalProperties: BaseType | boolean = type.getAdditionalProperties(); + if (additionalProperties === false) { + objectProperties = objectProperties.filter( + (property) => !(derefType(property.getType()) instanceof NeverType) + ); + } + const preparedProperties = objectProperties.map((property) => this.prepareObjectProperty(property)); const required = preparedProperties @@ -69,12 +76,7 @@ export class ObjectTypeFormatter implements SubTypeFormatter { .map((property) => property.getName()); const properties = preparedProperties.reduce((result: StringMap, property) => { - const propertyType = property.getType(); - - if (propertyType !== undefined) { - result[property.getName()] = this.childTypeFormatter.getDefinition(propertyType); - } - + result[property.getName()] = this.childTypeFormatter.getDefinition(property.getType()); return result; }, {}); diff --git a/src/TypeFormatter/TupleTypeFormatter.ts b/src/TypeFormatter/TupleTypeFormatter.ts index 400628073..b0a459615 100644 --- a/src/TypeFormatter/TupleTypeFormatter.ts +++ b/src/TypeFormatter/TupleTypeFormatter.ts @@ -6,7 +6,7 @@ import { OptionalType } from "../Type/OptionalType"; import { RestType } from "../Type/RestType"; import { TupleType } from "../Type/TupleType"; import { TypeFormatter } from "../TypeFormatter"; -import { notUndefined } from "../Utils/notUndefined"; +import { notNever } from "../Utils/notNever"; import { uniqueArray } from "../Utils/uniqueArray"; function uniformRestType(type: RestType, check_type: BaseType): boolean { @@ -32,7 +32,7 @@ export class TupleTypeFormatter implements SubTypeFormatter { } public getDefinition(type: TupleType): Definition { - const subTypes = type.getTypes().filter(notUndefined); + const subTypes = type.getTypes().filter(notNever); const requiredElements = subTypes.filter((t) => !(t instanceof OptionalType) && !(t instanceof RestType)); const optionalElements = subTypes.filter((t): t is OptionalType => t instanceof OptionalType); @@ -83,7 +83,7 @@ export class TupleTypeFormatter implements SubTypeFormatter { return uniqueArray( type .getTypes() - .filter(notUndefined) + .filter(notNever) .reduce((result: BaseType[], item) => [...result, ...this.childTypeFormatter.getChildren(item)], []) ); } diff --git a/src/TypeFormatter/UnionTypeFormatter.ts b/src/TypeFormatter/UnionTypeFormatter.ts index ac60523ff..2e4d1ef22 100644 --- a/src/TypeFormatter/UnionTypeFormatter.ts +++ b/src/TypeFormatter/UnionTypeFormatter.ts @@ -2,8 +2,10 @@ import { JSONSchema7 } from "json-schema"; import { Definition } from "../Schema/Definition"; import { SubTypeFormatter } from "../SubTypeFormatter"; import { BaseType } from "../Type/BaseType"; +import { NeverType } from "../Type/NeverType"; import { UnionType } from "../Type/UnionType"; import { TypeFormatter } from "../TypeFormatter"; +import { derefType } from "../Utils/derefType"; import { uniqueArray } from "../Utils/uniqueArray"; export class UnionTypeFormatter implements SubTypeFormatter { @@ -13,7 +15,10 @@ export class UnionTypeFormatter implements SubTypeFormatter { return type instanceof UnionType; } public getDefinition(type: UnionType): Definition { - const definitions = type.getTypes().map((item) => this.childTypeFormatter.getDefinition(item)); + const definitions = type + .getTypes() + .filter((item) => !(derefType(item) instanceof NeverType)) + .map((item) => this.childTypeFormatter.getDefinition(item)); // TODO: why is this not covered by LiteralUnionTypeFormatter? // special case for string literals | string -> string diff --git a/src/Utils/derefType.ts b/src/Utils/derefType.ts index f62f4e499..183950244 100644 --- a/src/Utils/derefType.ts +++ b/src/Utils/derefType.ts @@ -4,7 +4,7 @@ import { BaseType } from "../Type/BaseType"; import { DefinitionType } from "../Type/DefinitionType"; import { ReferenceType } from "../Type/ReferenceType"; -export function derefType(type: BaseType | undefined): BaseType | undefined { +export function derefType(type: BaseType): BaseType { if ( type instanceof ReferenceType || type instanceof DefinitionType || diff --git a/src/Utils/extractLiterals.ts b/src/Utils/extractLiterals.ts index 0716b4493..189686fad 100644 --- a/src/Utils/extractLiterals.ts +++ b/src/Utils/extractLiterals.ts @@ -4,7 +4,7 @@ import { BaseType } from "../Type/BaseType"; import { LiteralType } from "../Type/LiteralType"; import { UnionType } from "../Type/UnionType"; -function* _extractLiterals(type: BaseType | undefined): Iterable { +function* _extractLiterals(type: BaseType): Iterable { if (!type) { return; } @@ -26,6 +26,6 @@ function* _extractLiterals(type: BaseType | undefined): Iterable { throw new UnknownTypeError(type); } -export function extractLiterals(type: BaseType | undefined): string[] { +export function extractLiterals(type: BaseType): string[] { return [..._extractLiterals(type)]; } diff --git a/src/Utils/isAssignableTo.ts b/src/Utils/isAssignableTo.ts index 904a2c095..f368a9efb 100644 --- a/src/Utils/isAssignableTo.ts +++ b/src/Utils/isAssignableTo.ts @@ -18,6 +18,7 @@ import { NumberType } from "../Type/NumberType"; import { BooleanType } from "../Type/BooleanType"; import { InferType } from "../Type/InferType"; import { RestType } from "../Type/RestType"; +import { NeverType } from "../Type/NeverType"; /** * Returns the combined types from the given intersection. Currently only object types are combined. Maybe more @@ -87,8 +88,8 @@ function getPrimitiveType(value: LiteralValue) { * @return True if source type is assignable to target type. */ export function isAssignableTo( - target: BaseType | undefined, - source: BaseType | undefined, + target: BaseType, + source: BaseType, inferMap: Map = new Map(), insideTypes: Set = new Set() ): boolean { @@ -97,12 +98,12 @@ export function isAssignableTo( target = derefType(target); // Type "never" can be assigned to anything - if (source === undefined) { + if (source instanceof NeverType) { return true; } - // Nothing can be assigned to undefined (e.g. never-type) - if (target === undefined) { + // Nothing can be assigned to "never" + if (target instanceof NeverType) { return false; } @@ -262,7 +263,7 @@ export function isAssignableTo( if (i == numTarget - 1) { if (numTarget <= numSource + 1) { if (targetMember instanceof RestType) { - const remaining: Array = []; + const remaining: Array = []; for (let j = i; j < numSource; j++) { remaining.push(sourceMembers[j]); } @@ -293,13 +294,6 @@ export function isAssignableTo( return true; } } else { - // 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; } diff --git a/src/Utils/narrowType.ts b/src/Utils/narrowType.ts index 1dee7cfe6..52d67e9fe 100644 --- a/src/Utils/narrowType.ts +++ b/src/Utils/narrowType.ts @@ -1,5 +1,6 @@ import { BaseType } from "../Type/BaseType"; import { EnumType } from "../Type/EnumType"; +import { NeverType } from "../Type/NeverType"; import { UnionType } from "../Type/UnionType"; import { derefType } from "./derefType"; @@ -16,11 +17,11 @@ import { derefType } from "./derefType"; * @return The narrowed down type. */ export function narrowType( - type: BaseType | undefined, + type: BaseType, // TODO: remove the next line // eslint-disable-next-line no-shadow - predicate: (type: BaseType | undefined) => boolean -): BaseType | undefined { + predicate: (type: BaseType) => boolean +): BaseType { const derefed = derefType(type); if (derefed instanceof UnionType || derefed instanceof EnumType) { let changed = false; @@ -30,7 +31,7 @@ export function narrowType( // Recursively narrow down all types within the union const narrowed = narrowType(derefedSub, predicate); - if (narrowed !== undefined) { + if (!(narrowed instanceof NeverType)) { if (narrowed === derefedSub) { types.push(sub); } else { @@ -46,7 +47,7 @@ export function narrowType( // keep definitions if (changed) { if (types.length === 0) { - return undefined; + return new NeverType(); } else if (types.length === 1) { return types[0]; } else { @@ -55,5 +56,5 @@ export function narrowType( } return type; } - return predicate(derefed) ? type : undefined; + return predicate(derefed) ? type : new NeverType(); } diff --git a/src/Utils/notNever.ts b/src/Utils/notNever.ts new file mode 100644 index 000000000..acb70baee --- /dev/null +++ b/src/Utils/notNever.ts @@ -0,0 +1,6 @@ +import { BaseType } from "../Type/BaseType"; +import { NeverType } from "../Type/NeverType"; + +export function notNever(x: BaseType): boolean { + return !(x instanceof NeverType); +} diff --git a/src/Utils/notUndefined.ts b/src/Utils/notUndefined.ts deleted file mode 100644 index 460bcb13a..000000000 --- a/src/Utils/notUndefined.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function notUndefined(x: T | undefined): x is T { - return x !== undefined; -} diff --git a/src/Utils/typeKeys.ts b/src/Utils/typeKeys.ts index 0093deb1d..328dc277f 100644 --- a/src/Utils/typeKeys.ts +++ b/src/Utils/typeKeys.ts @@ -20,7 +20,7 @@ function uniqueLiterals(types: LiteralType[]): LiteralType[] { return uniqueArray(values).map((value) => new LiteralType(value)); } -export function getTypeKeys(type: BaseType | undefined): LiteralType[] { +export function getTypeKeys(type: BaseType): LiteralType[] { type = derefType(type); if (type instanceof IntersectionType || type instanceof UnionType) { @@ -30,7 +30,7 @@ export function getTypeKeys(type: BaseType | undefined): LiteralType[] { } if (type instanceof TupleType) { - return type.getTypes().map((it, idx) => new LiteralType(idx)); + return type.getTypes().map((_it, idx) => new LiteralType(idx)); } if (type instanceof ObjectType) { const objectProperties = type.getProperties().map((it) => new LiteralType(it.getName())); @@ -47,7 +47,7 @@ export function getTypeKeys(type: BaseType | undefined): LiteralType[] { return []; } -export function getTypeByKey(type: BaseType | undefined, index: LiteralType | StringType): BaseType | undefined { +export function getTypeByKey(type: BaseType, index: LiteralType | StringType): BaseType | undefined { type = derefType(type); if (type instanceof IntersectionType || type instanceof UnionType) { diff --git a/test/config.test.ts b/test/config.test.ts index c58bbab4b..4cdce5f38 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -129,7 +129,7 @@ export class ExampleConstructorParser implements SubNodeParser { supportsNode(node: ts.Node): boolean { return node.kind === ts.SyntaxKind.ConstructorType; } - createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType | undefined { + createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType { return new StringType(); } } @@ -138,7 +138,7 @@ export class ExampleNullParser implements SubNodeParser { supportsNode(node: ts.Node): boolean { return node.kind === ts.SyntaxKind.NullKeyword; } - createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType | undefined { + createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType { return new StringType(); } } diff --git a/test/unit/isAssignableTo.test.ts b/test/unit/isAssignableTo.test.ts index 8efb2190f..c7e75e9f5 100644 --- a/test/unit/isAssignableTo.test.ts +++ b/test/unit/isAssignableTo.test.ts @@ -7,6 +7,7 @@ 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 { NeverType } from "../../src/Type/NeverType"; import { NullType } from "../../src/Type/NullType"; import { NumberType } from "../../src/Type/NumberType"; import { ObjectProperty, ObjectType } from "../../src/Type/ObjectType"; @@ -107,7 +108,7 @@ describe("isAssignableTo", () => { 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(undefined, new AnyType())).toBe(false); + expect(isAssignableTo(new NeverType(), new AnyType())).toBe(false); expect(isAssignableTo(new NullType(), new AnyType())).toBe(true); expect( isAssignableTo( @@ -123,31 +124,31 @@ describe("isAssignableTo", () => { expect(isAssignableTo(new UndefinedType(), new AnyType())).toBe(true); }); it("lets type 'never' to be assigned to anything", () => { - expect(isAssignableTo(new AnyType(), undefined)).toBe(true); - expect(isAssignableTo(new ArrayType(new NumberType()), undefined)).toBe(true); - expect(isAssignableTo(new IntersectionType([new StringType(), new NullType()]), undefined)).toBe(true); - expect(isAssignableTo(new LiteralType("literal"), undefined)).toBe(true); - expect(isAssignableTo(undefined, undefined)).toBe(true); - expect(isAssignableTo(new NullType(), undefined)).toBe(true); + 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), - undefined + new NeverType() ) ).toBe(true); - expect(isAssignableTo(new BooleanType(), undefined)).toBe(true); - expect(isAssignableTo(new NumberType(), undefined)).toBe(true); - expect(isAssignableTo(new BooleanType(), undefined)).toBe(true); - expect(isAssignableTo(new StringType(), undefined)).toBe(true); - expect(isAssignableTo(new TupleType([new StringType(), new NumberType()]), undefined)).toBe(true); - expect(isAssignableTo(new UndefinedType(), undefined)).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(), undefined)).toBe(true); + expect(isAssignableTo(new AnyType(), new NeverType())).toBe(true); expect(isAssignableTo(new AnyType(), new NullType())).toBe(true); expect( isAssignableTo( @@ -167,7 +168,7 @@ describe("isAssignableTo", () => { 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(), undefined)).toBe(true); + expect(isAssignableTo(new UnknownType(), new NeverType())).toBe(true); expect(isAssignableTo(new UnknownType(), new NullType())).toBe(true); expect( isAssignableTo( @@ -187,7 +188,7 @@ describe("isAssignableTo", () => { 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(undefined, 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( @@ -206,7 +207,7 @@ describe("isAssignableTo", () => { it("lets 'any', 'never', 'null', and 'undefined' be assigned to type 'void'", () => { expect(isAssignableTo(new VoidType(), new AnyType())).toBe(true); - expect(isAssignableTo(new VoidType(), undefined)).toBe(true); + expect(isAssignableTo(new VoidType(), new NeverType())).toBe(true); expect(isAssignableTo(new VoidType(), new NullType())).toBe(true); expect(isAssignableTo(new VoidType(), new UndefinedType())).toBe(true); expect(isAssignableTo(new VoidType(), new UnknownType())).toBe(false); @@ -340,7 +341,7 @@ describe("isAssignableTo", () => { 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, undefined)).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)) diff --git a/test/valid-data/type-conditional-infer-tuple-xor/schema.json b/test/valid-data/type-conditional-infer-tuple-xor/schema.json index b27e878ab..8a99bfcc9 100644 --- a/test/valid-data/type-conditional-infer-tuple-xor/schema.json +++ b/test/valid-data/type-conditional-infer-tuple-xor/schema.json @@ -7,12 +7,6 @@ { "additionalProperties": false, "properties": { - "a": { - "not": {} - }, - "b": { - "not": {} - }, "c": { "type": "boolean" } @@ -25,14 +19,8 @@ { "additionalProperties": false, "properties": { - "a": { - "not": {} - }, "b": { "type": "number" - }, - "c": { - "not": {} } }, "required": [ @@ -45,12 +33,6 @@ "properties": { "a": { "type": "string" - }, - "b": { - "not": {} - }, - "c": { - "not": {} } }, "required": [