import * as ts from 'typescript' const intrinsicTypeKeywords = new Set([ ts.SyntaxKind.AnyKeyword, ts.SyntaxKind.BooleanKeyword, ts.SyntaxKind.NeverKeyword, ts.SyntaxKind.NumberKeyword, ts.SyntaxKind.StringKeyword, ts.SyntaxKind.UndefinedKeyword, ts.SyntaxKind.UnknownKeyword, ts.SyntaxKind.VoidKeyword ]) const typeOperatorKeywords = new Map([ [ts.SyntaxKind.KeyofKeyword, '_keyof'], [ts.SyntaxKind.ReadonlyKeyword, '_readonly'], [ts.SyntaxKind.UniqueKeyword, '_unique'], ]) 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 parameterTypeStructure(paramNode, checker) { let typeStruc = typeStructure(paramNode.type, checker) if (paramNode.questionToken) typeStruc = {_optional: typeStruc} return typeStruc } function typeStructure(typeNode, checker) { switch (typeNode.kind) { case ts.SyntaxKind.UnionType: return { _union: typeNode.types.map(t => typeStructure(t, checker)) } case ts.SyntaxKind.IntersectionType: return { _intersection: typeNode.types.map(t => typeStructure(t, checker)) } case ts.SyntaxKind.ImportType: { const typeStruc = { _importedFrom: typeNode.argument.literal.text, _name: typeNode.qualifier.text } if (typeNode.typeArguments) { typeStruc._typeArguments = typeNode.typeArguments.map( p => typeStructure(p, checker)) } return typeStruc } case ts.SyntaxKind.ArrayType: return {_array: typeStructure(typeNode.elementType, checker)} case ts.SyntaxKind.TypeLiteral: // Seems to be plain object types return Object.fromEntries(typeNode.members.map( mem => [mem.name.text, typeStructure(mem.type, checker)])) case ts.SyntaxKind.FunctionType: { const typeStruc = { _parameters: typeNode.parameters.map( p => parameterTypeStructure(p, checker)), _returns: typeStructure(typeNode.type, checker) } if (typeNode.typeParameters) { typeStruc._typeParameters = typeNode.typeParameters.map( p => p.name.text) } return typeStruc } case ts.SyntaxKind.IndexedAccessType: return { _ofType: typeStructure(typeNode.objectType, checker), _index: typeStructure(typeNode.indexType, checker) } case ts.SyntaxKind.TypeOperator: const key = typeOperatorKeywords.get( typeNode.operator, '_unidentified_operator' ) return { [key]: typeStructure(typeNode.type, checker) } case ts.SyntaxKind.ConditionalType: return { _subtype: typeStructure(typeNode.checkType, checker), _basetype: typeStructure(typeNode.extendsType, checker), _truetype: typeStructure(typeNode.trueType, checker), _falsetype: typeStructure(typeNode.falseType, checker), } case ts.SyntaxKind.TypeReference: { const typeStruc = {_typeParameter: typeNode.typeName.text} if (typeNode.typeArguments) { typeStruc._typeArguments = typeNode.typeArguments.map( arg => typeStructure(arg, checker)) } return typeStruc } case ts.SyntaxKind.TypePredicate: return {_is: typeStructure(typeNode.type, checker)} case ts.SyntaxKind.LiteralType: return checker.typeToString(checker.getTypeFromTypeNode(typeNode)) default: if (intrinsicTypeKeywords.has(typeNode.kind)) { return checker.getTypeFromTypeNode(typeNode).intrinsicName } } throw new Error(`Unhandled type node ${ts.SyntaxKind[typeNode.kind]}`) } const visit = (parent, checker) => node => { switch (node.kind) { // Currently, we are ignoring the following sorts of statements // that may appear in .d.ts files. We may need to revisit these, // especially the InterfaceDeclaration and TypeAliasDeclaration, // if we want to generate runtime information on pure type // declarations. I think this may be necessary for example to compute // the "RealType" of a type at runtime. case ts.SyntaxKind.EndOfFileToken: case ts.SyntaxKind.ExportDeclaration: case ts.SyntaxKind.ImportDeclaration: case ts.SyntaxKind.InterfaceDeclaration: case ts.SyntaxKind.TypeAliasDeclaration: break case ts.SyntaxKind.VariableStatement: node.declarationList.declarations.forEach(visit(parent, checker)) break case ts.SyntaxKind.VariableDeclaration: { const typeStruc = typeStructure(node.type, checker) parent.addChild(node.name.text, typeStruc) break } case ts.SyntaxKind.FunctionDeclaration: { const typeStruc = { _parameters: node.parameters.map( p => parameterTypeStructure(p, checker)), _returns: typeStructure(node.type, checker) } if (node.typeParameters) { typeStruc._typeParameters = node.typeParameters.map( p => p.name.text) } parent.addChild(node.name.text, typeStruc) break } case ts.SyntaxKind.ModuleDeclaration: let moduleName = node.name.text visit(parent.addChild(moduleName), checker)(node.body) break case ts.SyntaxKind.ModuleBlock: ts.forEachChild(node, visit(parent, 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 { if (intrinsicTypeKeywords.has(propertyType.kind)) { parent.addChild( realPropertyName, checker.getTypeFromTypeNode(propertyType).intrinsicName) } } 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() const filenameUnix = filename.replaceAll('\\', '/') let sourceFile = sourceFiles.find(file => file.fileName === filenameUnix) ts.forEachChild(sourceFile, visit(node, checker)) return node.getObject()[ROOT_NAME] }