ts-json-schema-generator
Version:
Generate JSON schema from your Typescript sources
130 lines (115 loc) • 5.4 kB
text/typescript
import ts from "typescript";
import type { NodeParser } from "../NodeParser.js";
import { Context } from "../NodeParser.js";
import type { SubNodeParser } from "../SubNodeParser.js";
import type { BaseType } from "../Type/BaseType.js";
import { isAssignableTo } from "../Utils/isAssignableTo.js";
import { narrowType } from "../Utils/narrowType.js";
import { UnionType } from "../Type/UnionType.js";
import { NeverType } from "../Type/NeverType.js";
class CheckType {
constructor(
public parameterName: string,
public type: BaseType,
) {}
}
export class ConditionalTypeNodeParser implements SubNodeParser {
public constructor(
protected typeChecker: ts.TypeChecker,
protected 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 checkTypeParameterName = this.getTypeParameterName(node.checkType);
const inferMap = new Map();
// If check-type is not a type parameter then condition is very simple, no type narrowing needed
if (checkTypeParameterName == null) {
const result = isAssignableTo(extendsType, checkType, inferMap);
return this.childNodeParser.createType(
result ? node.trueType : node.falseType,
this.createSubContext(node, context, undefined, result ? inferMap : new Map()),
);
}
// Narrow down check type for both condition branches
const trueCheckType = narrowType(checkType, (type) => isAssignableTo(extendsType, type, inferMap));
const falseCheckType = narrowType(checkType, (type) => !isAssignableTo(extendsType, type));
// Follow the relevant branches and return the results from them
const results: BaseType[] = [];
if (!(trueCheckType instanceof NeverType)) {
const result = this.childNodeParser.createType(
node.trueType,
this.createSubContext(node, context, new CheckType(checkTypeParameterName, trueCheckType), inferMap),
);
if (result) {
results.push(result);
}
}
if (!(falseCheckType instanceof NeverType)) {
const result = this.childNodeParser.createType(
node.falseType,
this.createSubContext(node, context, new CheckType(checkTypeParameterName, falseCheckType)),
);
if (result) {
results.push(result);
}
}
return new UnionType(results).normalize();
}
/**
* 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.
*/
protected 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;
}
/**
* Creates a sub context for evaluating the sub types of the conditional type. A sub context is needed in case
* the check-type is a type parameter which is then narrowed down by the extends-type.
*
* @param node - The reference node for the new context.
* @param checkType - An object containing the type parameter name of the check-type, and the narrowed
* down check type to use for the type parameter in sub parsers.
* @param inferMap - A map that links parameter names to their inferred types.
* @return The created sub context.
*/
protected createSubContext(
node: ts.ConditionalTypeNode,
parentContext: Context,
checkType?: CheckType,
inferMap: Map<string, BaseType> = new Map(),
): Context {
const subContext = new Context(node);
// Newly inferred types take precedence over check and parent types.
inferMap.forEach((value, key) => {
subContext.pushParameter(key);
subContext.pushArgument(value);
});
if (checkType !== undefined) {
// Set new narrowed type for check type parameter
if (!(checkType.parameterName in inferMap)) {
subContext.pushParameter(checkType.parameterName);
subContext.pushArgument(checkType.type);
}
}
// Copy all other type parameters from parent context
parentContext.getParameters().forEach((parentParameter) => {
if (parentParameter !== checkType?.parameterName && !(parentParameter in inferMap)) {
subContext.pushParameter(parentParameter);
subContext.pushArgument(parentContext.getArgument(parentParameter));
}
});
return subContext;
}
}