Skip to content

Commit fc5d3f3

Browse files
authored
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...
1 parent 95bf8f2 commit fc5d3f3

File tree

1 file changed

+139
-42
lines changed

1 file changed

+139
-42
lines changed

src-transpiler/expandType.js

Lines changed: 139 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,19 @@ import ts from 'typescript';
2323
* expandType('typeof Number '); // Outputs:
2424
* @param {string} type - The type string to be expanded into a structured representation.
2525
* @todo Share type with expandTypeBabelTS and expandTypeDepFree
26-
* @returns {string | {type: string, [key: string]: any} | undefined} The structured type
26+
* @returns {string | number | boolean | {type: string, [key: string]: any} | undefined} The structured type
2727
* representation obtained from parsing and converting the provided type string.
2828
*/
2929
function expandType(type) {
3030
const ast = parseType(type);
31+
if (!ast) {
32+
return 'never';
33+
}
3134
return toSourceTS(ast);
3235
}
33-
/**
34-
* @todo I want to use for example: import('typescript').Node
35-
* But the TS types make no sense to me so far ... need to investigate more.
36-
* @typedef TypeScriptType
37-
* @property {object[]|undefined} typeArguments - The type arguments.
38-
* @property {import('typescript').Node} typeName - The type name.
39-
* @property {number} kind - The kind for `ts.SyntaxKind[kind]`.
40-
*/
4136
/**
4237
* @param {string} str - The type string.
43-
* @returns {TypeScriptType} - The node containing all the information about the input type string.
38+
* @returns {ts.TypeNode|undefined} - The node containing all the information about the input type string.
4439
*/
4540
function parseType(str) {
4641
// TS doesn't like ... notation in this context
@@ -51,7 +46,12 @@ function parseType(str) {
5146
// type tmp = (...string) => 123; to have a function context
5247
str = `type tmp = ${str};`;
5348
const ast = ts.createSourceFile('repl.ts', str, ts.ScriptTarget.Latest, true /*setParentNodes*/);
54-
return ast.statements[0].type;
49+
const firstStatement = ast.statements[0];
50+
if (!ts.isTypeAliasDeclaration(firstStatement)) {
51+
console.warn('parseType> Expected type alias declaration, got', firstStatement, 'instead.');
52+
return;
53+
}
54+
return firstStatement.type;
5555
}
5656
/** @type {Record<string, 'missing'|'found'>} */
5757
export const requiredTypeofs = {};
@@ -61,63 +61,116 @@ export const requiredTypeofs = {};
6161
* This function handles various TypeScript AST node types and converts them into a string
6262
* or an object representing the type.
6363
*
64-
* @param {TypeScriptType} node - The TypeScript AST node to convert.
64+
* @param {ts.TypeNode} node - The TypeScript AST node to convert.
6565
* @returns {string | number | boolean | {type: string, [key: string]: any} | undefined} The source string/number,
6666
* or an object with type information based on the node, or `undefined` if the node kind is not handled.
6767
*/
6868
function toSourceTS(node) {
6969
const {typeArguments, typeName} = node;
7070
const kind_ = ts.SyntaxKind[node.kind];
7171
const {
72-
AnyKeyword, ArrayType, BooleanKeyword, FunctionType, Identifier, IntersectionType,
73-
JSDocAllType, LastTypeNode, LiteralType, NullKeyword, NumberKeyword, NumericLiteral,
74-
ObjectKeyword, Parameter, ParenthesizedType, PropertySignature, StringKeyword,
75-
StringLiteral, ThisType, TupleType, TypeLiteral, TypeReference, UndefinedKeyword,
76-
UnionType, JSDocNullableType, TrueKeyword, FalseKeyword, VoidKeyword, UnknownKeyword,
77-
NeverKeyword, BigIntKeyword, BigIntLiteral, ConditionalType, IndexedAccessType, RestType,
78-
TypeQuery, // parseType('typeof Number')
79-
TypeOperator, // parseType('keyof typeof obj')
80-
KeyOfKeyword, // "operator" key in TypeOperator node
81-
ConstructorType, // parseType('new (...args: any[]) => any');
82-
NamedTupleMember,
83-
MappedType, // parseType('{[K in TaskType]: InstanceType<typeof SUPPORTED_TASKS[K]["pipeline"]>}')
84-
TypeParameter, // Basically K and TaskType of MappedType
72+
AnyKeyword, // parseType('any' ).kind === ts.SyntaxKind.AnyKeyword && toSourceTS(parseType('any')) === 'any'
73+
ArrayType, // parseType('number[]' ).kind === ts.SyntaxKind.ArrayType // todo toSourceTS(parseType('number[]')) === {type: 'array etc.
74+
BooleanKeyword, // parseType("boolean" ).kind === ts.SyntaxKind.BooleanKeyword
75+
FunctionType, // parseType("() => void" ).kind === ts.SyntaxKind.FunctionType
76+
Identifier, // parseType("{a: 1, b: 2}" ).members[0].name.kind === ts.SyntaxKind.Identifier
77+
IntersectionType, // parseType("1 & 2" ).kind === ts.SyntaxKind.IntersectionType
78+
JSDocAllType, // parseType("*" ).kind === ts.SyntaxKind.JSDocAllType
79+
ImportType, // parseType('import("test").Test' ).kind === ts.SyntaxKind.ImportType
80+
LiteralType, // parseType("123" ).kind === ts.SyntaxKind.LiteralType
81+
NullKeyword, // parseType("null" ).literal.kind === ts.SyntaxKind.NullKeyword
82+
NumberKeyword, // parseType("number" ).kind === ts.SyntaxKind.NumberKeyword
83+
NumericLiteral, // parseType("123" ).literal.kind === ts.SyntaxKind.NumericLiteral
84+
ObjectKeyword, // parseType("object" ).kind === ts.SyntaxKind.ObjectKeyword
85+
Parameter, // parseType("(a) => void" ).parameters[0].kind === ts.SyntaxKind.Parameter
86+
ParenthesizedType, // parseType("(SomeType)" ).kind === ts.SyntaxKind.ParenthesizedType
87+
PropertySignature, // parseType("{a: 1, b: 2}" ).members[0].kind === ts.SyntaxKind.PropertySignature
88+
StringKeyword, // parseType("string" ).kind === ts.SyntaxKind.StringKeyword
89+
StringLiteral, // parseType("'test'" ).literal.kind === ts.SyntaxKind.StringLiteral
90+
ThisType, // parseType("this" ).kind === ts.SyntaxKind.ThisType
91+
TupleType, // parseType("[1, 2, 3]" ).kind === ts.SyntaxKind.TupleType
92+
TypeLiteral, // parseType("{a: 1, b: 2}" ).kind === ts.SyntaxKind.TypeLiteral
93+
TypeReference, // parseType("SomeOtherType" ).kind === ts.SyntaxKind.TypeReference
94+
UndefinedKeyword, // parseType("undefined" ).kind === ts.SyntaxKind.UndefinedKeyword
95+
UnionType, // parseType("1|2" ).kind === ts.SyntaxKind.UnionType
96+
JSDocNullableType, // parseType("?lol?" ).kind === ts.SyntaxKind.JSDocNullableType
97+
TrueKeyword, // parseType("true" ).literal.kind === ts.SyntaxKind.TrueKeyword
98+
FalseKeyword, // parseType("false" ).literal.kind === ts.SyntaxKind.FalseKeyword
99+
VoidKeyword, // parseType("void" ).kind === ts.SyntaxKind.VoidKeyword
100+
UnknownKeyword, // parseType("unknown" ).kind === ts.SyntaxKind.UnknownKeyword
101+
NeverKeyword, // parseType("never" ).kind === ts.SyntaxKind.NeverKeyword
102+
BigIntKeyword, // parseType("bigint" ).kind === ts.SyntaxKind.BigIntKeyword
103+
BigIntLiteral, // parseType("123n" ).literal.kind === ts.SyntaxKind.BigIntLiteral
104+
ConditionalType, // parseType("1 extends number ? true : false").kind === ts.SyntaxKind.ConditionalType
105+
IndexedAccessType, // parseType('Test[123]' ).kind === ts.SyntaxKind.IndexedAccessType
106+
RestType, // parseType("[...number]" ).elements[0].kind === ts.SyntaxKind.RestType
107+
TypeQuery, // parseType('typeof Number' ).kind === ts.SyntaxKind.TypeQuery
108+
TypeOperator, // parseType('keyof typeof obj' ).kind === ts.SyntaxKind.TypeOperator
109+
KeyOfKeyword, // parseType('keyof typeof obj' ).operator === ts.SyntaxKind.KeyOfKeyword
110+
ConstructorType, // parseType('new (...args: any[]) => any' ).kind === ts.SyntaxKind.ConstructorType
111+
NamedTupleMember, // parseType('[a: 1]' ).elements[0].kind === ts.SyntaxKind.NamedTupleMember
112+
MappedType, // parseType('{[K in TaskType]: 123}' ).kind === ts.SyntaxKind.MappedType
113+
TypeParameter, // parseType('{[K in TaskType]: 123}' ).typeParameter.kind === ts.SyntaxKind.TypeParameter
85114
} = ts.SyntaxKind;
86115
// console.log({typeArguments, typeName, kind_, node});
87116
switch (node.kind) {
88117
case BigIntKeyword:
89118
return {type: 'bigint'};
90119
case BigIntLiteral:
120+
if (!ts.isBigIntLiteral(node)) {
121+
throw Error("Impossible");
122+
}
91123
const literal = node.text.slice(0, -1); // Remove the "n"
92124
return {type: 'bigint', literal};
93125
case ConditionalType:
126+
if (!ts.isConditionalTypeNode(node)) {
127+
throw Error("Impossible");
128+
}
94129
// Keys on node:
95130
// ['pos', 'end', 'flags', 'modifierFlagsCache', 'transformFlags', 'parent', 'kind', 'checkType',
96131
// 'extendsType', 'trueType', 'falseType', 'locals', 'nextContainer']
97-
const checkType = toSourceTS(node.checkType);
132+
const checkType = toSourceTS(node.checkType );
98133
const extendsType = toSourceTS(node.extendsType);
99-
const trueType = toSourceTS(node.trueType);
100-
const falseType = toSourceTS(node.falseType);
134+
const trueType = toSourceTS(node.trueType );
135+
const falseType = toSourceTS(node.falseType );
101136
return {type: 'condition', checkType, extendsType, trueType, falseType};
102137
case ConstructorType: {
138+
if (!ts.isConstructorTypeNode(node)) {
139+
throw Error("Impossible");
140+
}
103141
const parameters = node.parameters.map(toSourceTS);
104142
const ret = toSourceTS(node.type);
105143
return {type: 'new', parameters, ret};
106144
}
107145
case FunctionType:
146+
if (!ts.isFunctionTypeNode(node)) {
147+
throw Error("Impossible");
148+
}
108149
const parameters = node.parameters.map(toSourceTS);
109150
return {type: 'function', parameters};
110151
case IndexedAccessType:
111-
const index = toSourceTS(node.indexType);
152+
if (!ts.isIndexedAccessTypeNode(node)) {
153+
throw Error("Impossible");
154+
}
155+
const index = toSourceTS(node.indexType);
112156
const object = toSourceTS(node.objectType);
113157
return {type: 'indexedAccess', index, object};
114158
case RestType:
159+
if (!ts.isRestTypeNode(node)) {
160+
throw Error("Impossible");
161+
}
115162
const annotation = toSourceTS(node.type);
116163
return {type: 'rest', annotation};
117164
case JSDocNullableType:
165+
if (!ts.isJSDocNullableType(node)) {
166+
throw Error("Impossible");
167+
}
118168
const t = toSourceTS(node.type);
119169
return {type: 'union', members: [t, 'null']};
120170
case MappedType: {
171+
if (!ts.isMappedTypeNode(node)) {
172+
throw Error("Impossible");
173+
}
121174
const result = toSourceTS(node.type);
122175
const parameter = node.typeParameter;
123176
if (parameter.kind === TypeParameter) {
@@ -132,6 +185,9 @@ function toSourceTS(node) {
132185
// todo work out more: const jsdoc = `(...a: ...number) => 123
133186
// TS even thinks it's two parameters... just go for array/[]
134187
case Parameter:
188+
if (!ts.isParameter(node)) {
189+
throw Error("Impossible");
190+
}
135191
const type = node.type ? toSourceTS(node.type) : 'any';
136192
const name = toSourceTS(node.name);
137193
const ret = {type, name};
@@ -140,19 +196,28 @@ function toSourceTS(node) {
140196
}
141197
return ret;
142198
case TypeQuery:
199+
if (!ts.isTypeQueryNode(node)) {
200+
throw Error("Impossible");
201+
}
143202
const argument = toSourceTS(node.exprName);
144203
// Notify Asserter class that we have to register variables with this name
145204
if (!requiredTypeofs[argument]) {
146205
requiredTypeofs[argument] = 'missing';
147206
}
148207
return {type: 'typeof', argument};
149208
case TypeOperator:
209+
if (!ts.isTypeOperatorNode(node)) {
210+
throw Error("Impossible");
211+
}
150212
if (node.operator === KeyOfKeyword) {
151213
const argument = toSourceTS(node.type);
152214
return {type: 'keyof', argument};
153215
}
154216
console.warn("unimplemented TypeOperator", node);
155217
case TypeReference: {
218+
if (!ts.isTypeReferenceNode(node)) {
219+
throw Error("Impossible");
220+
}
156221
if ((typeName.text === 'Object' || typeName.text === 'Record') && typeArguments?.length === 2) {
157222
return {
158223
type: 'record',
@@ -185,23 +250,34 @@ function toSourceTS(node) {
185250
const args = typeArguments.map(toSourceTS);
186251
return {type: 'reference', name, args};
187252
}
188-
case StringKeyword:
189-
return node.getText();
190-
case NumberKeyword:
191-
return node.getText();
192253
case NamedTupleMember:
254+
if (!ts.isNamedTupleMember(node)) {
255+
throw Error("Impossible");
256+
}
193257
return toSourceTS(node.type);
194258
case IntersectionType: {
259+
if (!ts.isIntersectionTypeNode(node)) {
260+
throw Error("Impossible");
261+
}
195262
const members = node.types.map(toSourceTS);
196263
return {type: 'intersection', members};
197264
}
198265
case TupleType:
266+
if (!ts.isTupleTypeNode(node)) {
267+
throw Error("Impossible");
268+
}
199269
const elements = node.elements.map(toSourceTS);
200270
return {type: 'tuple', elements};
201271
case UnionType:
272+
if (!ts.isUnionTypeNode(node)) {
273+
throw Error("Impossible");
274+
}
202275
const members = node.types.map(toSourceTS);
203276
return {type: 'union', members};
204277
case TypeLiteral:
278+
if (!ts.isTypeLiteralNode(node)) {
279+
throw Error("Impossible");
280+
}
205281
const properties = {};
206282
node.members.forEach(member => {
207283
const name = toSourceTS(member.name);
@@ -210,27 +286,41 @@ function toSourceTS(node) {
210286
});
211287
return {type: 'object', properties};
212288
case PropertySignature:
289+
if (!ts.isPropertySignature(node)) {
290+
throw Error("Impossible");
291+
}
213292
console.warn('toSourceTS> should not happen, handled by TypeLiteral directly');
214293
return `${toSourceTS(node.name)}: ${toSourceTS(node.type)}`;
215294
case Identifier:
295+
if (!ts.isIdentifier(node)) {
296+
throw Error("Impossible");
297+
}
216298
return node.text;
217299
case ArrayType: {
300+
if (!ts.isArrayTypeNode(node)) {
301+
throw Error("Impossible");
302+
}
218303
const elementType = toSourceTS(node.elementType);
219304
return {type: 'array', elementType};
220305
}
221306
case LiteralType:
307+
if (!ts.isLiteralTypeNode(node)) {
308+
throw Error("Impossible");
309+
}
222310
return toSourceTS(node.literal);
223-
case AnyKeyword:
224-
case BooleanKeyword:
311+
case AnyKeyword:
312+
case BooleanKeyword:
313+
case StringKeyword:
314+
case NeverKeyword:
315+
case NullKeyword:
316+
case NumberKeyword:
317+
case UndefinedKeyword:
318+
case UnknownKeyword:
319+
case VoidKeyword:
225320
// ts.SyntaxKind[parseType("*").kind] === 'JSDocAllType'
226321
case JSDocAllType:
227-
case NullKeyword:
322+
case ThisType:
228323
case StringLiteral:
229-
case ThisType:
230-
case UndefinedKeyword:
231-
case VoidKeyword:
232-
case UnknownKeyword:
233-
case NeverKeyword:
234324
return node.getText();
235325
case TrueKeyword:
236326
return true;
@@ -244,9 +334,16 @@ function toSourceTS(node) {
244334
properties: {}
245335
};
246336
case ParenthesizedType:
337+
if (!ts.isParenthesizedTypeNode(node)) {
338+
throw Error("Impossible");
339+
}
247340
// fall-through for parentheses
248341
return toSourceTS(node.type);
249-
case LastTypeNode:
342+
case ImportType:
343+
if (!ts.isImportTypeNode(node)) {
344+
throw Error("Impossible");
345+
}
346+
/** @todo Handle case without any qualifier like `import('test')` */
250347
return toSourceTS(node.qualifier);
251348
default:
252349
// const test = {};

0 commit comments

Comments
 (0)