UNPKG

ts-json-schema-generator

Version:

Generate JSON schema from your Typescript sources

152 lines (134 loc) 6.59 kB
import ts from "typescript"; import { LogicError } from "../Error/LogicError"; import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { AnnotatedType } from "../Type/AnnotatedType"; import { ArrayType } from "../Type/ArrayType"; import { BaseType } from "../Type/BaseType"; import { EnumType, EnumValue } from "../Type/EnumType"; import { LiteralType } from "../Type/LiteralType"; import { NumberType } from "../Type/NumberType"; import { ObjectProperty, ObjectType } from "../Type/ObjectType"; import { StringType } from "../Type/StringType"; import { UnionType } from "../Type/UnionType"; import { derefAnnotatedType, derefType } from "../Utils/derefType"; import { getKey } from "../Utils/nodeKey"; import { preserveAnnotation } from "../Utils/preserveAnnotation"; import { removeUndefined } from "../Utils/removeUndefined"; import { notUndefined } from "../Utils/notUndefined"; import { SymbolType } from "../Type/SymbolType"; export class MappedTypeNodeParser implements SubNodeParser { public constructor(private childNodeParser: NodeParser, private readonly additionalProperties: boolean) {} public supportsNode(node: ts.MappedTypeNode): boolean { return node.kind === ts.SyntaxKind.MappedType; } public createType(node: ts.MappedTypeNode, context: Context): BaseType | undefined { const constraintType = this.childNodeParser.createType(node.typeParameter.constraint!, context); const keyListType = derefType(constraintType); const id = `indexed-type-${getKey(node, context)}`; if (keyListType instanceof UnionType) { // Key type resolves to a set of known properties return new ObjectType( id, [], this.getProperties(node, keyListType, context), this.getAdditionalProperties(node, keyListType, context) ); } else if (keyListType instanceof LiteralType) { // Key type resolves to single known property return new ObjectType(id, [], this.getProperties(node, new UnionType([keyListType]), context), false); } else if (keyListType instanceof StringType || keyListType instanceof SymbolType) { // Key type widens to `string` const type = this.childNodeParser.createType(node.type!, context); const resultType = type === undefined ? undefined : new ObjectType(id, [], [], type); if (resultType && constraintType instanceof AnnotatedType) { const annotations = constraintType.getAnnotations(); if (annotations) { return new AnnotatedType(resultType, { propertyNames: annotations }, false); } } return resultType; } else if (keyListType instanceof NumberType) { const type = this.childNodeParser.createType(node.type!, this.createSubContext(node, keyListType, context)); return type === undefined ? undefined : new ArrayType(type); } else if (keyListType instanceof EnumType) { return new ObjectType(id, [], this.getValues(node, keyListType, context), false); } else { throw new LogicError( // eslint-disable-next-line max-len `Unexpected key type "${ constraintType ? constraintType.getId() : constraintType }" for type "${node.getText()}" (expected "UnionType" or "StringType")` ); } } private getProperties(node: ts.MappedTypeNode, keyListType: UnionType, context: Context): ObjectProperty[] { return keyListType .getTypes() .filter((type) => type instanceof LiteralType) .reduce((result: ObjectProperty[], key: LiteralType) => { const propertyType = this.childNodeParser.createType( node.type!, this.createSubContext(node, key, context) ); if (propertyType === undefined) { return result; } let newType = derefAnnotatedType(propertyType); let hasUndefined = false; if (newType instanceof UnionType) { const { newType: newType_, numRemoved } = removeUndefined(newType); hasUndefined = numRemoved > 0; newType = newType_; } const objectProperty = new ObjectProperty( key.getValue().toString(), preserveAnnotation(propertyType, newType), !node.questionToken && !hasUndefined ); result.push(objectProperty); return result; }, []); } private getValues(node: ts.MappedTypeNode, keyListType: EnumType, context: Context): ObjectProperty[] { return keyListType .getValues() .filter((value: EnumValue) => value != null) .map((value: EnumValue) => { const type = this.childNodeParser.createType( node.type!, this.createSubContext(node, new LiteralType(value!), context) ); if (type === undefined) { return undefined; } return new ObjectProperty(value!.toString(), type, !node.questionToken); }) .filter(notUndefined); } private getAdditionalProperties( node: ts.MappedTypeNode, keyListType: UnionType, context: Context ): BaseType | boolean { const key = keyListType.getTypes().filter((type) => !(type instanceof LiteralType))[0]; if (key) { return ( this.childNodeParser.createType(node.type!, this.createSubContext(node, key, context)) ?? this.additionalProperties ); } else { return this.additionalProperties; } } private createSubContext(node: ts.MappedTypeNode, key: LiteralType | StringType, parentContext: Context): Context { const subContext = new Context(node); parentContext.getParameters().forEach((parentParameter) => { subContext.pushParameter(parentParameter); subContext.pushArgument(parentContext.getArgument(parentParameter)); }); subContext.pushParameter(node.typeParameter.name.text); subContext.pushArgument(key); return subContext; } }