UNPKG

ts-json-schema-generator

Version:

Generate JSON schema from your Typescript sources

191 lines (166 loc) 7.88 kB
import ts from "typescript"; import { ExpectationFailedError } from "../Error/Errors.js"; import type { NodeParser } from "../NodeParser.js"; import { Context } from "../NodeParser.js"; import type { SubNodeParser } from "../SubNodeParser.js"; import { AnnotatedType } from "../Type/AnnotatedType.js"; import { ArrayType } from "../Type/ArrayType.js"; import type { BaseType } from "../Type/BaseType.js"; import { DefinitionType } from "../Type/DefinitionType.js"; import type { EnumValue } from "../Type/EnumType.js"; import { EnumType } from "../Type/EnumType.js"; import { LiteralType } from "../Type/LiteralType.js"; import { NeverType } from "../Type/NeverType.js"; import { NumberType } from "../Type/NumberType.js"; import { ObjectProperty, ObjectType } from "../Type/ObjectType.js"; import { StringType } from "../Type/StringType.js"; import { SymbolType } from "../Type/SymbolType.js"; import { UnionType } from "../Type/UnionType.js"; import { derefAnnotatedType, derefType } from "../Utils/derefType.js"; import { getKey } from "../Utils/nodeKey.js"; import { preserveAnnotation } from "../Utils/preserveAnnotation.js"; import { removeUndefined } from "../Utils/removeUndefined.js"; import { uniqueTypeArray } from "../Utils/uniqueTypeArray.js"; export class MappedTypeNodeParser implements SubNodeParser { public constructor( protected childNodeParser: NodeParser, protected readonly additionalProperties: boolean, ) {} public supportsNode(node: ts.MappedTypeNode): boolean { return node.kind === ts.SyntaxKind.MappedType; } 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)}`; 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), ); } if (keyListType instanceof LiteralType) { // Key type resolves to single known property return new ObjectType(id, [], this.getProperties(node, new UnionType([keyListType]), context), false); } if ( keyListType instanceof StringType || keyListType instanceof NumberType || keyListType instanceof SymbolType ) { if (constraintType?.getId() === "number") { const type = this.childNodeParser.createType( node.type!, this.createSubContext(node, keyListType, context), ); 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 instanceof NeverType ? new NeverType() : new ObjectType(id, [], [], type); const resultType = new ObjectType(id, [], [], type); if (resultType) { let annotations; if (constraintType instanceof AnnotatedType) { annotations = constraintType.getAnnotations(); } else if (constraintType instanceof DefinitionType) { const childType = constraintType.getType(); if (childType instanceof AnnotatedType) { annotations = childType.getAnnotations(); } } if (annotations) { return new AnnotatedType(resultType, { propertyNames: annotations }, false); } } return resultType; } if (keyListType instanceof EnumType) { return new ObjectType(id, [], this.getValues(node, keyListType, context), false); } if (keyListType instanceof NeverType) { return new ObjectType(id, [], [], false); } throw new ExpectationFailedError( `Unexpected key type "${ constraintType ? constraintType.getId() : constraintType }" for this node. (expected "UnionType" or "StringType")`, node, ); } protected mapKey(node: ts.MappedTypeNode, rawKey: LiteralType, context: Context): BaseType { if (!node.nameType) { return rawKey; } return derefType(this.childNodeParser.createType(node.nameType, this.createSubContext(node, rawKey, context))); } protected getProperties(node: ts.MappedTypeNode, keyListType: UnionType, context: Context): ObjectProperty[] { return uniqueTypeArray(keyListType.getFlattenedTypes(derefType)) .filter((type): type is LiteralType => type instanceof LiteralType) .map((type) => [type, this.mapKey(node, type, context)]) .filter((value): value is [LiteralType, LiteralType] => value[1] instanceof LiteralType) .reduce((result: ObjectProperty[], [key, mappedKey]: [LiteralType, LiteralType]) => { const propertyType = this.childNodeParser.createType( node.type!, this.createSubContext(node, key, context), ); 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( mappedKey.getValue().toString(), preserveAnnotation(propertyType, newType), !node.questionToken && !hasUndefined, ); result.push(objectProperty); return result; }, []); } protected 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), ); return new ObjectProperty(value!.toString(), type, !node.questionToken); }); } protected 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 ); } return this.additionalProperties; } protected createSubContext( node: ts.MappedTypeNode, key: LiteralType | StringType | NumberType, parentContext: Context, ): Context { const subContext = new Context(node); for (const parentParameter of parentContext.getParameters()) { subContext.pushParameter(parentParameter); subContext.pushArgument(parentContext.getArgument(parentParameter)); } subContext.pushParameter(node.typeParameter.name.text); subContext.pushArgument(key); return subContext; } }