ts-json-schema-generator
Version:
Generate JSON schema from your Typescript sources
102 lines (90 loc) • 4.38 kB
text/typescript
import 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";
import { UnionType } from "../Type/UnionType";
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 | undefined {
const checkType = this.childNodeParser.createType(node.checkType, context);
const extendsType = this.childNodeParser.createType(node.extendsType, context);
const checkTypeParameterName = this.getTypeParameterName(node.checkType);
// If check-type is not a type parameter then condition is very simple, no type narrowing needed
if (checkTypeParameterName == null) {
const result = isAssignableTo(extendsType, checkType);
return this.childNodeParser.createType(result ? node.trueType : node.falseType, context);
}
// Narrow down check type for both condition branches
const trueCheckType = narrowType(checkType, (type) => isAssignableTo(extendsType, type));
const falseCheckType = narrowType(checkType, (type) => !isAssignableTo(extendsType, type));
// Follow the relevant branches and return the results from them
const results: BaseType[] = [];
if (trueCheckType !== undefined) {
const result = this.childNodeParser.createType(
node.trueType,
this.createSubContext(node, checkTypeParameterName, trueCheckType, context)
);
if (result) {
results.push(result);
}
}
if (falseCheckType !== undefined) {
const result = this.childNodeParser.createType(
node.falseType,
this.createSubContext(node, checkTypeParameterName, falseCheckType, context)
);
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.
*/
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;
}
/**
* 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 checkTypeParameterName - The type parameter name of the check-type.
* @param narrowedCheckType - The narrowed down check type to use for the type parameter in sub parsers.
* @return The created sub context.
*/
private createSubContext(
node: ts.ConditionalTypeNode,
checkTypeParameterName: string,
narrowedCheckType: BaseType,
parentContext: Context
): Context {
const subContext = new Context(node);
// Set new narrowed type for check type parameter
subContext.pushParameter(checkTypeParameterName);
subContext.pushArgument(narrowedCheckType);
// Copy all other type parameters from parent context
parentContext.getParameters().forEach((parentParameter) => {
if (parentParameter !== checkTypeParameterName) {
subContext.pushParameter(parentParameter);
subContext.pushArgument(parentContext.getArgument(parentParameter));
}
});
return subContext;
}
}