Skip to content

Commit

Permalink
Use native TS types for src-transpiler/expandType.js (#158)
Browse files Browse the repository at this point in the history
* Use native TS types for src-transpiler/expandType.js

* Give example for *every* ts.SyntaxKind, fix ImportType

* TS fails at namespace importing...
  • Loading branch information
kungfooman authored Jun 10, 2024
1 parent 95bf8f2 commit fc5d3f3
Showing 1 changed file with 139 additions and 42 deletions.
181 changes: 139 additions & 42 deletions src-transpiler/expandType.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string, 'missing'|'found'>} */
export const requiredTypeofs = {};
Expand All @@ -61,63 +61,116 @@ 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.
*/
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<typeof SUPPORTED_TASKS[K]["pipeline"]>}')
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) {
Expand All @@ -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};
Expand All @@ -140,19 +196,28 @@ 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]) {
requiredTypeofs[argument] = 'missing';
}
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',
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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 = {};
Expand Down

0 comments on commit fc5d3f3

Please sign in to comment.