From fc5d3f3db51d2188c37f0cbae1dee7544a150137 Mon Sep 17 00:00:00 2001 From: Hermann Rolfes Date: Mon, 10 Jun 2024 10:52:11 +0200 Subject: [PATCH] Use native TS types for src-transpiler/expandType.js (#158) * Use native TS types for src-transpiler/expandType.js * Give example for *every* ts.SyntaxKind, fix ImportType * TS fails at namespace importing... --- src-transpiler/expandType.js | 181 +++++++++++++++++++++++++++-------- 1 file changed, 139 insertions(+), 42 deletions(-) diff --git a/src-transpiler/expandType.js b/src-transpiler/expandType.js index 5e342a0..3a80a23 100644 --- a/src-transpiler/expandType.js +++ b/src-transpiler/expandType.js @@ -23,24 +23,19 @@ import ts from 'typescript'; * expandType('typeof Number '); // Outputs: * @param {string} type - The type string to be expanded into a structured representation. * @todo Share type with expandTypeBabelTS and expandTypeDepFree - * @returns {string | {type: string, [key: string]: any} | undefined} The structured type + * @returns {string | number | boolean | {type: string, [key: string]: any} | undefined} The structured type * representation obtained from parsing and converting the provided type string. */ function expandType(type) { const ast = parseType(type); + if (!ast) { + return 'never'; + } return toSourceTS(ast); } -/** - * @todo I want to use for example: import('typescript').Node - * But the TS types make no sense to me so far ... need to investigate more. - * @typedef TypeScriptType - * @property {object[]|undefined} typeArguments - The type arguments. - * @property {import('typescript').Node} typeName - The type name. - * @property {number} kind - The kind for `ts.SyntaxKind[kind]`. - */ /** * @param {string} str - The type string. - * @returns {TypeScriptType} - The node containing all the information about the input type string. + * @returns {ts.TypeNode|undefined} - The node containing all the information about the input type string. */ function parseType(str) { // TS doesn't like ... notation in this context @@ -51,7 +46,12 @@ function parseType(str) { // type tmp = (...string) => 123; to have a function context str = `type tmp = ${str};`; const ast = ts.createSourceFile('repl.ts', str, ts.ScriptTarget.Latest, true /*setParentNodes*/); - return ast.statements[0].type; + const firstStatement = ast.statements[0]; + if (!ts.isTypeAliasDeclaration(firstStatement)) { + console.warn('parseType> Expected type alias declaration, got', firstStatement, 'instead.'); + return; + } + return firstStatement.type; } /** @type {Record} */ export const requiredTypeofs = {}; @@ -61,7 +61,7 @@ export const requiredTypeofs = {}; * This function handles various TypeScript AST node types and converts them into a string * or an object representing the type. * - * @param {TypeScriptType} node - The TypeScript AST node to convert. + * @param {ts.TypeNode} node - The TypeScript AST node to convert. * @returns {string | number | boolean | {type: string, [key: string]: any} | undefined} The source string/number, * or an object with type information based on the node, or `undefined` if the node kind is not handled. */ @@ -69,55 +69,108 @@ function toSourceTS(node) { const {typeArguments, typeName} = node; const kind_ = ts.SyntaxKind[node.kind]; const { - AnyKeyword, ArrayType, BooleanKeyword, FunctionType, Identifier, IntersectionType, - JSDocAllType, LastTypeNode, LiteralType, NullKeyword, NumberKeyword, NumericLiteral, - ObjectKeyword, Parameter, ParenthesizedType, PropertySignature, StringKeyword, - StringLiteral, ThisType, TupleType, TypeLiteral, TypeReference, UndefinedKeyword, - UnionType, JSDocNullableType, TrueKeyword, FalseKeyword, VoidKeyword, UnknownKeyword, - NeverKeyword, BigIntKeyword, BigIntLiteral, ConditionalType, IndexedAccessType, RestType, - TypeQuery, // parseType('typeof Number') - TypeOperator, // parseType('keyof typeof obj') - KeyOfKeyword, // "operator" key in TypeOperator node - ConstructorType, // parseType('new (...args: any[]) => any'); - NamedTupleMember, - MappedType, // parseType('{[K in TaskType]: InstanceType}') - TypeParameter, // Basically K and TaskType of MappedType + AnyKeyword, // parseType('any' ).kind === ts.SyntaxKind.AnyKeyword && toSourceTS(parseType('any')) === 'any' + ArrayType, // parseType('number[]' ).kind === ts.SyntaxKind.ArrayType // todo toSourceTS(parseType('number[]')) === {type: 'array etc. + BooleanKeyword, // parseType("boolean" ).kind === ts.SyntaxKind.BooleanKeyword + FunctionType, // parseType("() => void" ).kind === ts.SyntaxKind.FunctionType + Identifier, // parseType("{a: 1, b: 2}" ).members[0].name.kind === ts.SyntaxKind.Identifier + IntersectionType, // parseType("1 & 2" ).kind === ts.SyntaxKind.IntersectionType + JSDocAllType, // parseType("*" ).kind === ts.SyntaxKind.JSDocAllType + ImportType, // parseType('import("test").Test' ).kind === ts.SyntaxKind.ImportType + LiteralType, // parseType("123" ).kind === ts.SyntaxKind.LiteralType + NullKeyword, // parseType("null" ).literal.kind === ts.SyntaxKind.NullKeyword + NumberKeyword, // parseType("number" ).kind === ts.SyntaxKind.NumberKeyword + NumericLiteral, // parseType("123" ).literal.kind === ts.SyntaxKind.NumericLiteral + ObjectKeyword, // parseType("object" ).kind === ts.SyntaxKind.ObjectKeyword + Parameter, // parseType("(a) => void" ).parameters[0].kind === ts.SyntaxKind.Parameter + ParenthesizedType, // parseType("(SomeType)" ).kind === ts.SyntaxKind.ParenthesizedType + PropertySignature, // parseType("{a: 1, b: 2}" ).members[0].kind === ts.SyntaxKind.PropertySignature + StringKeyword, // parseType("string" ).kind === ts.SyntaxKind.StringKeyword + StringLiteral, // parseType("'test'" ).literal.kind === ts.SyntaxKind.StringLiteral + ThisType, // parseType("this" ).kind === ts.SyntaxKind.ThisType + TupleType, // parseType("[1, 2, 3]" ).kind === ts.SyntaxKind.TupleType + TypeLiteral, // parseType("{a: 1, b: 2}" ).kind === ts.SyntaxKind.TypeLiteral + TypeReference, // parseType("SomeOtherType" ).kind === ts.SyntaxKind.TypeReference + UndefinedKeyword, // parseType("undefined" ).kind === ts.SyntaxKind.UndefinedKeyword + UnionType, // parseType("1|2" ).kind === ts.SyntaxKind.UnionType + JSDocNullableType, // parseType("?lol?" ).kind === ts.SyntaxKind.JSDocNullableType + TrueKeyword, // parseType("true" ).literal.kind === ts.SyntaxKind.TrueKeyword + FalseKeyword, // parseType("false" ).literal.kind === ts.SyntaxKind.FalseKeyword + VoidKeyword, // parseType("void" ).kind === ts.SyntaxKind.VoidKeyword + UnknownKeyword, // parseType("unknown" ).kind === ts.SyntaxKind.UnknownKeyword + NeverKeyword, // parseType("never" ).kind === ts.SyntaxKind.NeverKeyword + BigIntKeyword, // parseType("bigint" ).kind === ts.SyntaxKind.BigIntKeyword + BigIntLiteral, // parseType("123n" ).literal.kind === ts.SyntaxKind.BigIntLiteral + ConditionalType, // parseType("1 extends number ? true : false").kind === ts.SyntaxKind.ConditionalType + IndexedAccessType, // parseType('Test[123]' ).kind === ts.SyntaxKind.IndexedAccessType + RestType, // parseType("[...number]" ).elements[0].kind === ts.SyntaxKind.RestType + TypeQuery, // parseType('typeof Number' ).kind === ts.SyntaxKind.TypeQuery + TypeOperator, // parseType('keyof typeof obj' ).kind === ts.SyntaxKind.TypeOperator + KeyOfKeyword, // parseType('keyof typeof obj' ).operator === ts.SyntaxKind.KeyOfKeyword + ConstructorType, // parseType('new (...args: any[]) => any' ).kind === ts.SyntaxKind.ConstructorType + NamedTupleMember, // parseType('[a: 1]' ).elements[0].kind === ts.SyntaxKind.NamedTupleMember + MappedType, // parseType('{[K in TaskType]: 123}' ).kind === ts.SyntaxKind.MappedType + TypeParameter, // parseType('{[K in TaskType]: 123}' ).typeParameter.kind === ts.SyntaxKind.TypeParameter } = ts.SyntaxKind; // console.log({typeArguments, typeName, kind_, node}); switch (node.kind) { case BigIntKeyword: return {type: 'bigint'}; case BigIntLiteral: + if (!ts.isBigIntLiteral(node)) { + throw Error("Impossible"); + } const literal = node.text.slice(0, -1); // Remove the "n" return {type: 'bigint', literal}; case ConditionalType: + if (!ts.isConditionalTypeNode(node)) { + throw Error("Impossible"); + } // Keys on node: // ['pos', 'end', 'flags', 'modifierFlagsCache', 'transformFlags', 'parent', 'kind', 'checkType', // 'extendsType', 'trueType', 'falseType', 'locals', 'nextContainer'] - const checkType = toSourceTS(node.checkType); + const checkType = toSourceTS(node.checkType ); const extendsType = toSourceTS(node.extendsType); - const trueType = toSourceTS(node.trueType); - const falseType = toSourceTS(node.falseType); + const trueType = toSourceTS(node.trueType ); + const falseType = toSourceTS(node.falseType ); return {type: 'condition', checkType, extendsType, trueType, falseType}; case ConstructorType: { + if (!ts.isConstructorTypeNode(node)) { + throw Error("Impossible"); + } const parameters = node.parameters.map(toSourceTS); const ret = toSourceTS(node.type); return {type: 'new', parameters, ret}; } case FunctionType: + if (!ts.isFunctionTypeNode(node)) { + throw Error("Impossible"); + } const parameters = node.parameters.map(toSourceTS); return {type: 'function', parameters}; case IndexedAccessType: - const index = toSourceTS(node.indexType); + if (!ts.isIndexedAccessTypeNode(node)) { + throw Error("Impossible"); + } + const index = toSourceTS(node.indexType); const object = toSourceTS(node.objectType); return {type: 'indexedAccess', index, object}; case RestType: + if (!ts.isRestTypeNode(node)) { + throw Error("Impossible"); + } const annotation = toSourceTS(node.type); return {type: 'rest', annotation}; case JSDocNullableType: + if (!ts.isJSDocNullableType(node)) { + throw Error("Impossible"); + } const t = toSourceTS(node.type); return {type: 'union', members: [t, 'null']}; case MappedType: { + if (!ts.isMappedTypeNode(node)) { + throw Error("Impossible"); + } const result = toSourceTS(node.type); const parameter = node.typeParameter; if (parameter.kind === TypeParameter) { @@ -132,6 +185,9 @@ function toSourceTS(node) { // todo work out more: const jsdoc = `(...a: ...number) => 123 // TS even thinks it's two parameters... just go for array/[] case Parameter: + if (!ts.isParameter(node)) { + throw Error("Impossible"); + } const type = node.type ? toSourceTS(node.type) : 'any'; const name = toSourceTS(node.name); const ret = {type, name}; @@ -140,6 +196,9 @@ function toSourceTS(node) { } return ret; case TypeQuery: + if (!ts.isTypeQueryNode(node)) { + throw Error("Impossible"); + } const argument = toSourceTS(node.exprName); // Notify Asserter class that we have to register variables with this name if (!requiredTypeofs[argument]) { @@ -147,12 +206,18 @@ function toSourceTS(node) { } return {type: 'typeof', argument}; case TypeOperator: + if (!ts.isTypeOperatorNode(node)) { + throw Error("Impossible"); + } if (node.operator === KeyOfKeyword) { const argument = toSourceTS(node.type); return {type: 'keyof', argument}; } console.warn("unimplemented TypeOperator", node); case TypeReference: { + if (!ts.isTypeReferenceNode(node)) { + throw Error("Impossible"); + } if ((typeName.text === 'Object' || typeName.text === 'Record') && typeArguments?.length === 2) { return { type: 'record', @@ -185,23 +250,34 @@ function toSourceTS(node) { const args = typeArguments.map(toSourceTS); return {type: 'reference', name, args}; } - case StringKeyword: - return node.getText(); - case NumberKeyword: - return node.getText(); case NamedTupleMember: + if (!ts.isNamedTupleMember(node)) { + throw Error("Impossible"); + } return toSourceTS(node.type); case IntersectionType: { + if (!ts.isIntersectionTypeNode(node)) { + throw Error("Impossible"); + } const members = node.types.map(toSourceTS); return {type: 'intersection', members}; } case TupleType: + if (!ts.isTupleTypeNode(node)) { + throw Error("Impossible"); + } const elements = node.elements.map(toSourceTS); return {type: 'tuple', elements}; case UnionType: + if (!ts.isUnionTypeNode(node)) { + throw Error("Impossible"); + } const members = node.types.map(toSourceTS); return {type: 'union', members}; case TypeLiteral: + if (!ts.isTypeLiteralNode(node)) { + throw Error("Impossible"); + } const properties = {}; node.members.forEach(member => { const name = toSourceTS(member.name); @@ -210,27 +286,41 @@ function toSourceTS(node) { }); return {type: 'object', properties}; case PropertySignature: + if (!ts.isPropertySignature(node)) { + throw Error("Impossible"); + } console.warn('toSourceTS> should not happen, handled by TypeLiteral directly'); return `${toSourceTS(node.name)}: ${toSourceTS(node.type)}`; case Identifier: + if (!ts.isIdentifier(node)) { + throw Error("Impossible"); + } return node.text; case ArrayType: { + if (!ts.isArrayTypeNode(node)) { + throw Error("Impossible"); + } const elementType = toSourceTS(node.elementType); return {type: 'array', elementType}; } case LiteralType: + if (!ts.isLiteralTypeNode(node)) { + throw Error("Impossible"); + } return toSourceTS(node.literal); - case AnyKeyword: - case BooleanKeyword: + case AnyKeyword: + case BooleanKeyword: + case StringKeyword: + case NeverKeyword: + case NullKeyword: + case NumberKeyword: + case UndefinedKeyword: + case UnknownKeyword: + case VoidKeyword: // ts.SyntaxKind[parseType("*").kind] === 'JSDocAllType' case JSDocAllType: - case NullKeyword: + case ThisType: case StringLiteral: - case ThisType: - case UndefinedKeyword: - case VoidKeyword: - case UnknownKeyword: - case NeverKeyword: return node.getText(); case TrueKeyword: return true; @@ -244,9 +334,16 @@ function toSourceTS(node) { properties: {} }; case ParenthesizedType: + if (!ts.isParenthesizedTypeNode(node)) { + throw Error("Impossible"); + } // fall-through for parentheses return toSourceTS(node.type); - case LastTypeNode: + case ImportType: + if (!ts.isImportTypeNode(node)) { + throw Error("Impossible"); + } + /** @todo Handle case without any qualifier like `import('test')` */ return toSourceTS(node.qualifier); default: // const test = {};