import * as ts from 'typescript' const PROPERTY_TYPES = { any: ts.SyntaxKind.AnyKeyword, boolean: ts.SyntaxKind.BooleanKeyword, number: ts.SyntaxKind.NumberKeyword, string: ts.SyntaxKind.StringKeyword, } class TSNode { constructor(name, type) { this.children = [] this.addChild = (name, type) => { let node = new TSNode(name, type) this.children.push(node) return node } this.getType = () => this.type this.getObject = () => { let map = {} map[this.name] = this.children.length ? this.children .map(child => child.getObject()) .reduce((pv, child) => { for (let key in child) { if (pv.hasOwnProperty(key) || key in pv) { if (child[key]) { Object.assign(pv[key], child[key]) } } else { pv[key] = child[key] } } return pv }, {}) : this.type return map }; this.name = name this.type = type } } function typeStructure(typeNode, checker) { switch (typeNode.kind) { case ts.SyntaxKind.IntersectionType: return { _intersection: typeNode.types.map(t => typeStructure(t, checker)) } case ts.SyntaxKind.ImportType: return { _importedFrom: typeNode.argument.literal.text, _name: typeNode.qualifier.text } case ts.SyntaxKind.TypeLiteral: return Object.fromEntries(typeNode.members.map( mem => [mem.name.text, typeStructure(mem.type, checker)])) case ts.SyntaxKind.FunctionType: return { _parameters: typeNode.parameters.map( p => typeStructure(p.type, checker)), _returns: typeStructure(typeNode.type, checker) } case ts.SyntaxKind.LiteralType: return checker.typeToString(checker.getTypeFromTypeNode(typeNode)) default: console.log('I See', ts.SyntaxKind[typeNode.kind]) } return checker.getTypeFromTypeNode(typeNode).intrinsicName } const visit = (parent, checker) => node => { switch (node.kind) { case ts.SyntaxKind.ImportDeclaration: console.log('Skipping import from', node.moduleSpecifier.text) break case ts.SyntaxKind.VariableStatement: console.log('Processing variable', ts.SyntaxKind[node.declarationList.kind]) node.declarationList.declarations.forEach(visit(parent, checker)) console.log('Done with variable') break case ts.SyntaxKind.VariableDeclaration: console.log('Processing var dec', node.name.text, 'in', parent) const typeStruc = typeStructure(node.type, checker) console.log('Type is', typeStructure(node.type, checker)) if ('_intersection' in typeStruc) { console.log('First type is', typeStruc._intersection[0]) } parent.addChild(node.name.text, typeStruc) break case ts.SyntaxKind.ModuleDeclaration: let moduleName = node.name.text console.log('Declaring', moduleName) ts.forEachChild(node, visit(parent.addChild(moduleName), checker)) break case ts.SyntaxKind.ModuleBlock: console.log('Block') ts.forEachChild(node, visit(parent, checker)); break case ts.SyntaxKind.InterfaceDeclaration: let interfaceName = node.name.text console.log('Skipping Interface', interfaceName); break parent[interfaceName] = {} ts.forEachChild(node, visit(parent.addChild(interfaceName), checker)) break case ts.SyntaxKind.PropertySignature: let propertyName = node.name let propertyType = node.type let arrayDeep = 0 let realPropertyName = 'string' !== typeof propertyName && 'text' in propertyName ? propertyName.text : propertyName console.log('Property', realPropertyName) while (propertyType.kind === ts.SyntaxKind.ArrayType) { arrayDeep++ propertyType = propertyType.elementType } if (propertyType.kind === ts.SyntaxKind.TypeReference) { let realPropertyType = propertyType.typeName parent.addChild( realPropertyName, 'Array<'.repeat(arrayDeep) + (realPropertyType.kind === ts.SyntaxKind.QualifiedName ? realPropertyType.getText() : 'text' in realPropertyType ? realPropertyType.text : realPropertyType) + '>'.repeat(arrayDeep) ) } else { for (let type in PROPERTY_TYPES) { if (propertyType.kind === PROPERTY_TYPES[type]) { parent.addChild(realPropertyName, type) break } } } break default: console.warn( 'Unhandled node kind', node.kind, ts.SyntaxKind[node.kind]) } } export default function(filename, options = {}) { const ROOT_NAME = 'root' const node = new TSNode(ROOT_NAME) let program = ts.createProgram([filename], options) let checker = program.getTypeChecker() const sourceFiles = program.getSourceFiles() let sourceFile = sourceFiles.find(file => file.fileName === filename) ts.forEachChild(sourceFile, visit(node, checker)) return node.getObject()[ROOT_NAME] }