ts-json-schema-generator
Version:
Generate JSON schema from your Typescript sources
192 lines (168 loc) • 7.84 kB
text/typescript
import type { PropertyName } from "typescript";
import ts from "typescript";
import type { Context, NodeParser } from "../NodeParser.js";
import type { SubNodeParser } from "../SubNodeParser.js";
import { ArrayType } from "../Type/ArrayType.js";
import type { BaseType } from "../Type/BaseType.js";
import { NeverType } from "../Type/NeverType.js";
import { ObjectProperty, ObjectType } from "../Type/ObjectType.js";
import type { ReferenceType } from "../Type/ReferenceType.js";
import { isNodeHidden } from "../Utils/isHidden.js";
import { isPublic, isStatic } from "../Utils/modifiers.js";
import { getKey } from "../Utils/nodeKey.js";
export class InterfaceAndClassNodeParser implements SubNodeParser {
public constructor(
protected typeChecker: ts.TypeChecker,
protected childNodeParser: NodeParser,
protected readonly additionalProperties: boolean,
) {}
public supportsNode(node: ts.InterfaceDeclaration | ts.ClassDeclaration): boolean {
return node.kind === ts.SyntaxKind.InterfaceDeclaration || node.kind === ts.SyntaxKind.ClassDeclaration;
}
public createType(
node: ts.InterfaceDeclaration | ts.ClassDeclaration,
context: Context,
reference?: ReferenceType,
): BaseType {
if (node.typeParameters?.length) {
node.typeParameters.forEach((typeParam) => {
const nameSymbol = this.typeChecker.getSymbolAtLocation(typeParam.name)!;
context.pushParameter(nameSymbol.name);
if (typeParam.default) {
const type = this.childNodeParser.createType(typeParam.default, context);
context.setDefault(nameSymbol.name, type);
}
});
}
const id = this.getTypeId(node, context);
if (reference) {
reference.setId(id);
reference.setName(id);
}
const properties = this.getProperties(node, context);
if (properties === undefined) {
return new NeverType();
}
const additionalProperties = this.getAdditionalProperties(node, context);
// When type only extends Array or ReadonlyArray then create an array type instead of an object type
if (properties.length === 0 && additionalProperties === false) {
const arrayItemType = this.getArrayItemType(node);
if (arrayItemType) {
return new ArrayType(this.childNodeParser.createType(arrayItemType, context));
}
}
return new ObjectType(id, this.getBaseTypes(node, context), properties, additionalProperties);
}
/**
* If specified node extends Array or ReadonlyArray and nothing else then this method returns the
* array item type. In all other cases null is returned to indicate that the node is not a simple array.
*
* @param node - The interface or class to check.
* @return The array item type if node is an array, null otherwise.
*/
protected getArrayItemType(node: ts.InterfaceDeclaration | ts.ClassDeclaration): ts.TypeNode | null {
if (node.heritageClauses && node.heritageClauses.length === 1) {
const clause = node.heritageClauses[0];
if (clause.types.length === 1) {
const type = clause.types[0];
const symbol = this.typeChecker.getSymbolAtLocation(type.expression);
if (symbol && (symbol.name === "Array" || symbol.name === "ReadonlyArray")) {
const typeArguments = type.typeArguments;
if (typeArguments?.length === 1) {
return typeArguments[0];
}
}
}
}
return null;
}
protected getBaseTypes(node: ts.InterfaceDeclaration | ts.ClassDeclaration, context: Context): BaseType[] {
if (!node.heritageClauses) {
return [];
}
return node.heritageClauses.reduce(
(result: BaseType[], baseType) => [
...result,
...baseType.types.map((expression) => this.childNodeParser.createType(expression, context)),
],
[],
);
}
protected getProperties(
node: ts.InterfaceDeclaration | ts.ClassDeclaration,
context: Context,
): ObjectProperty[] | undefined {
let hasRequiredNever = false;
const properties = (node.members as ts.NodeArray<ts.TypeElement | ts.ClassElement>)
.reduce(
(members, member) => {
if (ts.isConstructorDeclaration(member)) {
const params = member.parameters.filter((param) =>
ts.isParameterPropertyDeclaration(param, param.parent),
);
members.push(...params);
} else if (ts.isPropertySignature(member) || ts.isPropertyDeclaration(member)) {
members.push(member);
}
return members;
},
[] as (ts.PropertyDeclaration | ts.PropertySignature | ts.ParameterPropertyDeclaration)[],
)
.filter((member) => isPublic(member) && !isStatic(member) && !isNodeHidden(member))
.reduce((entries, member) => {
let memberType: ts.Node | undefined = member.type;
// Use the type checker if the member has no explicit type
// Ignore members without an initializer. They have no useful type.
if (memberType === undefined && (member as ts.PropertyDeclaration)?.initializer !== undefined) {
const type = this.typeChecker.getTypeAtLocation(member);
memberType = this.typeChecker.typeToTypeNode(type, node, ts.NodeBuilderFlags.NoTruncation);
}
if (memberType !== undefined) {
return [...entries, { member, memberType }];
}
return entries;
}, [])
.map(
({ member, memberType }) =>
new ObjectProperty(
this.getPropertyName(member.name),
this.childNodeParser.createType(memberType, context),
!member.questionToken,
),
)
.filter((prop) => {
const type = prop.getType();
if (prop.isRequired() && type instanceof NeverType) {
hasRequiredNever = true;
}
return !(type instanceof NeverType);
});
if (hasRequiredNever) {
return undefined;
}
return properties;
}
protected getAdditionalProperties(
node: ts.InterfaceDeclaration | ts.ClassDeclaration,
context: Context,
): BaseType | boolean {
const indexSignature = (node.members as ts.NodeArray<ts.NamedDeclaration>).find(ts.isIndexSignatureDeclaration);
if (!indexSignature) {
return this.additionalProperties;
}
return this.childNodeParser.createType(indexSignature.type, context) ?? this.additionalProperties;
}
protected getTypeId(node: ts.Node, context: Context): string {
const nodeType = ts.isInterfaceDeclaration(node) ? "interface" : "class";
return `${nodeType}-${getKey(node, context)}`;
}
protected getPropertyName(propertyName: PropertyName): string {
if (propertyName.kind === ts.SyntaxKind.ComputedPropertyName) {
const symbol = this.typeChecker.getSymbolAtLocation(propertyName);
if (symbol) {
return symbol.getName();
}
}
return propertyName.getText();
}
}