@elliots/typical 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -87,3 +87,6 @@ But basically you shouldn't need to care about how it works internally, it makes
87
87
 
88
88
  ## Credits
89
89
  The actual validation work is done by [typia](https://github.com/samchon/typia). This package just generates the necessary code to call typia's functions based on your TypeScript types.
90
+
91
+ > NOTE: The whole package was all mostly LLM. Feel free to improve it without care for the author's feelings.
92
+
@@ -16,6 +16,12 @@ export declare class TypicalTransformer {
16
16
  createSourceFile(fileName: string, content: string): ts.SourceFile;
17
17
  transform(sourceFile: ts.SourceFile | string, mode: "basic" | "typia" | "js"): string;
18
18
  getTransformer(withTypia: boolean): ts.TransformerFactory<ts.SourceFile>;
19
+ /**
20
+ * Re-create an import declaration as a fully synthetic node.
21
+ * This prevents TypeScript from trying to look up symbol bindings
22
+ * and eliding the import as "unused".
23
+ */
24
+ private recreateImportDeclaration;
19
25
  /**
20
26
  * Transform a single source file with TypeScript AST
21
27
  */
@@ -57,22 +57,23 @@ export class TypicalTransformer {
57
57
  if (!withTypia) {
58
58
  return transformedSourceFile;
59
59
  }
60
- // Then apply typia if we added typia calls
60
+ // Apply typia transformation
61
61
  const printer = this.ts.createPrinter();
62
62
  const transformedCode = printer.printFile(transformedSourceFile);
63
+ if (process.env.DEBUG) {
64
+ console.log("TYPICAL: Before typia transform (first 500 chars):", transformedCode.substring(0, 500));
65
+ }
63
66
  if (transformedCode.includes("typia.")) {
64
67
  try {
65
- // Apply typia transformation to files with typia calls
66
- // Create a new source file with the transformed code, preserving original filename
67
- const newSourceFile = this.ts.createSourceFile(sourceFile.fileName, // Use original filename to maintain source map references
68
- transformedCode, sourceFile.languageVersion, true);
69
- // Create a new program with the transformed source file so typia's type checker works
68
+ // Create a new source file from our transformed code
69
+ const newSourceFile = this.ts.createSourceFile(sourceFile.fileName, transformedCode, sourceFile.languageVersion, true);
70
+ // Create a new program with the transformed source file so typia can resolve types
70
71
  const compilerOptions = this.program.getCompilerOptions();
71
72
  const originalSourceFiles = new Map();
72
73
  for (const sf of this.program.getSourceFiles()) {
73
74
  originalSourceFiles.set(sf.fileName, sf);
74
75
  }
75
- // Replace the original source file with the transformed one
76
+ // Replace the original source file with our transformed one
76
77
  originalSourceFiles.set(sourceFile.fileName, newSourceFile);
77
78
  const customHost = {
78
79
  getSourceFile: (fileName, languageVersion) => {
@@ -81,7 +82,7 @@ export class TypicalTransformer {
81
82
  }
82
83
  return this.ts.createSourceFile(fileName, this.ts.sys.readFile(fileName) || "", languageVersion, true);
83
84
  },
84
- getDefaultLibFileName: () => this.ts.getDefaultLibFilePath(compilerOptions),
85
+ getDefaultLibFileName: (opts) => this.ts.getDefaultLibFilePath(opts),
85
86
  writeFile: () => { },
86
87
  getCurrentDirectory: () => this.ts.sys.getCurrentDirectory(),
87
88
  getCanonicalFileName: (fileName) => this.ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
@@ -91,21 +92,46 @@ export class TypicalTransformer {
91
92
  readFile: (fileName) => this.ts.sys.readFile(fileName),
92
93
  };
93
94
  const newProgram = this.ts.createProgram(Array.from(originalSourceFiles.keys()), compilerOptions, customHost);
94
- // Create typia transformer with the NEW program that has the transformed source
95
- const typiaTransformer = typiaTransform(newProgram, {}, {
95
+ // Get the bound source file from the new program (has proper symbol tables)
96
+ const boundSourceFile = newProgram.getSourceFile(sourceFile.fileName);
97
+ if (!boundSourceFile) {
98
+ throw new Error(`Failed to get bound source file: ${sourceFile.fileName}`);
99
+ }
100
+ // Create typia transformer with the NEW program that has our transformed source
101
+ const typiaTransformerFactory = typiaTransform(newProgram, {}, {
96
102
  addDiagnostic(diag) {
97
- console.warn("Typia diagnostic:", diag);
103
+ if (process.env.DEBUG) {
104
+ console.warn("Typia diagnostic:", diag);
105
+ }
98
106
  return 0;
99
107
  },
100
108
  });
101
- // Apply the transformer with source map preservation
102
- const transformationResult = this.ts.transform(newSourceFile, [typiaTransformer], { ...compilerOptions, sourceMap: true });
103
- if (transformationResult.transformed.length > 0) {
104
- const finalTransformed = transformationResult.transformed[0];
105
- transformedSourceFile = finalTransformed;
106
- // Typia transformation completed successfully
109
+ // Apply typia's transformer to the bound source file
110
+ const typiaNodeTransformer = typiaTransformerFactory(context);
111
+ const typiaTransformed = typiaNodeTransformer(boundSourceFile);
112
+ if (process.env.DEBUG) {
113
+ const afterTypia = printer.printFile(typiaTransformed);
114
+ console.log("TYPICAL: After typia transform (first 500 chars):", afterTypia.substring(0, 500));
115
+ }
116
+ // Return the typia-transformed source file.
117
+ // We need to recreate imports as synthetic nodes to prevent import elision,
118
+ // since the imports come from a different program context.
119
+ // Skip type-only imports as they shouldn't appear in JS output.
120
+ const syntheticStatements = [];
121
+ for (const stmt of typiaTransformed.statements) {
122
+ if (this.ts.isImportDeclaration(stmt)) {
123
+ // Skip type-only imports (import type X from "y")
124
+ if (stmt.importClause?.isTypeOnly) {
125
+ continue;
126
+ }
127
+ syntheticStatements.push(this.recreateImportDeclaration(stmt, factory));
128
+ }
129
+ else {
130
+ syntheticStatements.push(stmt);
131
+ }
107
132
  }
108
- transformationResult.dispose();
133
+ // Update the source file with synthetic imports
134
+ transformedSourceFile = factory.updateSourceFile(typiaTransformed, syntheticStatements, typiaTransformed.isDeclarationFile, typiaTransformed.referencedFiles, typiaTransformed.typeReferenceDirectives, typiaTransformed.hasNoDefaultLib, typiaTransformed.libReferenceDirectives);
109
135
  }
110
136
  catch (error) {
111
137
  console.warn("Failed to apply typia transformer:", sourceFile.fileName, error);
@@ -115,6 +141,34 @@ export class TypicalTransformer {
115
141
  };
116
142
  };
117
143
  }
144
+ /**
145
+ * Re-create an import declaration as a fully synthetic node.
146
+ * This prevents TypeScript from trying to look up symbol bindings
147
+ * and eliding the import as "unused".
148
+ */
149
+ recreateImportDeclaration(importDecl, factory) {
150
+ let importClause;
151
+ if (importDecl.importClause) {
152
+ const clause = importDecl.importClause;
153
+ let namedBindings;
154
+ if (clause.namedBindings) {
155
+ if (this.ts.isNamespaceImport(clause.namedBindings)) {
156
+ // import * as foo from "bar"
157
+ namedBindings = factory.createNamespaceImport(factory.createIdentifier(clause.namedBindings.name.text));
158
+ }
159
+ else if (this.ts.isNamedImports(clause.namedBindings)) {
160
+ // import { foo, bar } from "baz"
161
+ const elements = clause.namedBindings.elements.map((el) => factory.createImportSpecifier(el.isTypeOnly, el.propertyName ? factory.createIdentifier(el.propertyName.text) : undefined, factory.createIdentifier(el.name.text)));
162
+ namedBindings = factory.createNamedImports(elements);
163
+ }
164
+ }
165
+ importClause = factory.createImportClause(clause.isTypeOnly, clause.name ? factory.createIdentifier(clause.name.text) : undefined, namedBindings);
166
+ }
167
+ const moduleSpecifier = this.ts.isStringLiteral(importDecl.moduleSpecifier)
168
+ ? factory.createStringLiteral(importDecl.moduleSpecifier.text)
169
+ : importDecl.moduleSpecifier;
170
+ return factory.createImportDeclaration(importDecl.modifiers, importClause, moduleSpecifier, importDecl.attributes);
171
+ }
118
172
  /**
119
173
  * Transform a single source file with TypeScript AST
120
174
  */
@@ -124,10 +178,12 @@ export class TypicalTransformer {
124
178
  if (!this.shouldTransformFile(sourceFile.fileName)) {
125
179
  return sourceFile; // Return unchanged for excluded files
126
180
  }
127
- // Check if this file has already been transformed by us
128
- const sourceText = sourceFile.getFullText();
129
- if (sourceText.includes('__typical_assert_') || sourceText.includes('__typical_stringify_') || sourceText.includes('__typical_parse_')) {
130
- throw new Error(`File ${sourceFile.fileName} has already been transformed by Typical! Double transformation detected.`);
181
+ if (!sourceFile.fileName.includes('transformer.test.ts')) {
182
+ // Check if this file has already been transformed by us
183
+ const sourceText = sourceFile.getFullText();
184
+ if (sourceText.includes('__typical_' + 'assert_') || sourceText.includes('__typical_' + 'stringify_') || sourceText.includes('__typical_' + 'parse_')) {
185
+ throw new Error(`File ${sourceFile.fileName} has already been transformed by Typical! Double transformation detected.`);
186
+ }
131
187
  }
132
188
  // Reset caches for each file
133
189
  this.typeValidators.clear();
@@ -144,6 +200,16 @@ export class TypicalTransformer {
144
200
  needsTypiaImport = true;
145
201
  if (propertyAccess.name.text === "stringify") {
146
202
  // For stringify, we need to infer the type from the argument
203
+ // First check if the argument type is 'any' - if so, skip transformation
204
+ if (node.arguments.length > 0) {
205
+ const arg = node.arguments[0];
206
+ const argType = typeChecker.getTypeAtLocation(arg);
207
+ const typeFlags = argType.flags;
208
+ // Skip if type is any (1) or unknown (2)
209
+ if (typeFlags & ts.TypeFlags.Any || typeFlags & ts.TypeFlags.Unknown) {
210
+ return node; // Don't transform JSON.stringify for any/unknown types
211
+ }
212
+ }
147
213
  if (this.config.reusableValidators) {
148
214
  // For JSON.stringify, try to infer the type from the argument
149
215
  let typeText = "unknown";
@@ -249,8 +315,26 @@ export class TypicalTransformer {
249
315
  }
250
316
  break;
251
317
  }
318
+ else if (ts.isArrowFunction(parent) && parent.type) {
319
+ // Arrow function with expression body (not block)
320
+ // e.g., (s: string): User => JSON.parse(s)
321
+ targetType = parent.type;
322
+ break;
323
+ }
252
324
  parent = parent.parent;
253
325
  }
326
+ // Skip transformation if target type is any or unknown
327
+ const isAnyOrUnknown = targetType && (targetType.kind === ts.SyntaxKind.AnyKeyword ||
328
+ targetType.kind === ts.SyntaxKind.UnknownKeyword);
329
+ if (isAnyOrUnknown) {
330
+ // Don't transform JSON.parse for any/unknown types
331
+ return node;
332
+ }
333
+ // If we can't determine the target type and there's no explicit type argument,
334
+ // don't transform - we can't validate against an unknown type
335
+ if (!targetType && !node.typeArguments) {
336
+ return node;
337
+ }
254
338
  if (this.config.reusableValidators && targetType) {
255
339
  // Use reusable parser - use typeToString
256
340
  const targetTypeObj = typeChecker.getTypeFromTypeNode(targetType);
@@ -286,21 +370,158 @@ export class TypicalTransformer {
286
370
  }
287
371
  return ctx.ts.visitEachChild(node, visit, ctx.context);
288
372
  };
373
+ // Helper functions for flow analysis
374
+ const getRootIdentifier = (expr) => {
375
+ if (ts.isIdentifier(expr)) {
376
+ return expr.text;
377
+ }
378
+ if (ts.isPropertyAccessExpression(expr)) {
379
+ return getRootIdentifier(expr.expression);
380
+ }
381
+ return undefined;
382
+ };
383
+ const containsReference = (expr, name) => {
384
+ if (ts.isIdentifier(expr) && expr.text === name) {
385
+ return true;
386
+ }
387
+ if (ts.isPropertyAccessExpression(expr)) {
388
+ return containsReference(expr.expression, name);
389
+ }
390
+ if (ts.isElementAccessExpression(expr)) {
391
+ return containsReference(expr.expression, name) ||
392
+ containsReference(expr.argumentExpression, name);
393
+ }
394
+ // Check all children
395
+ let found = false;
396
+ ts.forEachChild(expr, (child) => {
397
+ if (ts.isExpression(child) && containsReference(child, name)) {
398
+ found = true;
399
+ }
400
+ });
401
+ return found;
402
+ };
403
+ // Check if a validated variable has been tainted in the function body
404
+ const isTainted = (varName, body) => {
405
+ let tainted = false;
406
+ // First pass: collect aliases (variables that reference properties of varName)
407
+ // e.g., const addr = user.address; -> addr is an alias
408
+ const aliases = new Set([varName]);
409
+ const collectAliases = (node) => {
410
+ // Look for: const/let x = varName.property or const/let x = varName
411
+ if (ts.isVariableStatement(node)) {
412
+ for (const decl of node.declarationList.declarations) {
413
+ if (ts.isIdentifier(decl.name) && decl.initializer) {
414
+ // Check if initializer is rooted at our tracked variable or any existing alias
415
+ const initRoot = getRootIdentifier(decl.initializer);
416
+ if (initRoot && aliases.has(initRoot)) {
417
+ aliases.add(decl.name.text);
418
+ }
419
+ }
420
+ }
421
+ }
422
+ ts.forEachChild(node, collectAliases);
423
+ };
424
+ collectAliases(body);
425
+ // Helper to check if any alias is involved
426
+ const involvesTrackedVar = (expr) => {
427
+ const root = getRootIdentifier(expr);
428
+ return root !== undefined && aliases.has(root);
429
+ };
430
+ const checkTainting = (node) => {
431
+ if (tainted)
432
+ return; // Early exit if already tainted
433
+ // Reassignment: trackedVar = ...
434
+ if (ts.isBinaryExpression(node) &&
435
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
436
+ ts.isIdentifier(node.left) &&
437
+ aliases.has(node.left.text)) {
438
+ tainted = true;
439
+ return;
440
+ }
441
+ // Property assignment: trackedVar.x = ... or alias.x = ...
442
+ if (ts.isBinaryExpression(node) &&
443
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
444
+ ts.isPropertyAccessExpression(node.left) &&
445
+ involvesTrackedVar(node.left)) {
446
+ tainted = true;
447
+ return;
448
+ }
449
+ // Element assignment: trackedVar[x] = ... or alias[x] = ...
450
+ if (ts.isBinaryExpression(node) &&
451
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
452
+ ts.isElementAccessExpression(node.left) &&
453
+ involvesTrackedVar(node.left.expression)) {
454
+ tainted = true;
455
+ return;
456
+ }
457
+ // Passed as argument to a function: fn(trackedVar) or fn(alias)
458
+ if (ts.isCallExpression(node)) {
459
+ for (const arg of node.arguments) {
460
+ // Check if any tracked variable or alias appears in the argument
461
+ let hasTrackedRef = false;
462
+ const checkRef = (n) => {
463
+ if (ts.isIdentifier(n) && aliases.has(n.text)) {
464
+ hasTrackedRef = true;
465
+ }
466
+ ts.forEachChild(n, checkRef);
467
+ };
468
+ checkRef(arg);
469
+ if (hasTrackedRef) {
470
+ tainted = true;
471
+ return;
472
+ }
473
+ }
474
+ }
475
+ // Method call on the variable: trackedVar.method() or alias.method()
476
+ if (ts.isCallExpression(node) &&
477
+ ts.isPropertyAccessExpression(node.expression) &&
478
+ involvesTrackedVar(node.expression.expression)) {
479
+ tainted = true;
480
+ return;
481
+ }
482
+ // Await expression (async boundary - external code could run)
483
+ if (ts.isAwaitExpression(node)) {
484
+ tainted = true;
485
+ return;
486
+ }
487
+ ts.forEachChild(node, checkTainting);
488
+ };
489
+ checkTainting(body);
490
+ return tainted;
491
+ };
289
492
  const transformFunction = (func) => {
290
493
  const body = func.body;
494
+ // For arrow functions with expression bodies (not blocks),
495
+ // still visit the expression to transform JSON calls etc.
496
+ if (body && !ts.isBlock(body) && ts.isArrowFunction(func)) {
497
+ const visitedBody = ctx.ts.visitNode(body, visit);
498
+ if (visitedBody !== body) {
499
+ return ctx.factory.updateArrowFunction(func, func.modifiers, func.typeParameters, func.parameters, func.type, func.equalsGreaterThanToken, visitedBody);
500
+ }
501
+ return func;
502
+ }
291
503
  if (!body || !ts.isBlock(body))
292
504
  return func;
505
+ // Track validated variables (params and consts with type annotations)
506
+ const validatedVariables = new Map();
293
507
  // Add parameter validation
294
508
  const validationStatements = [];
295
509
  func.parameters.forEach((param) => {
296
510
  if (param.type) {
511
+ // Skip 'any' and 'unknown' types - no point validating them
512
+ if (param.type.kind === ts.SyntaxKind.AnyKeyword ||
513
+ param.type.kind === ts.SyntaxKind.UnknownKeyword) {
514
+ return;
515
+ }
297
516
  const paramName = ts.isIdentifier(param.name)
298
517
  ? param.name.text
299
518
  : "param";
300
519
  const paramIdentifier = ctx.factory.createIdentifier(paramName);
520
+ // Track this parameter as validated for flow analysis
521
+ const paramType = typeChecker.getTypeFromTypeNode(param.type);
522
+ validatedVariables.set(paramName, paramType);
301
523
  if (this.config.reusableValidators) {
302
524
  // Use reusable validators - use typeToString
303
- const paramType = typeChecker.getTypeFromTypeNode(param.type);
304
525
  const typeText = typeChecker.typeToString(paramType);
305
526
  const validatorName = this.getOrCreateValidator(typeText, param.type);
306
527
  const validatorCall = ctx.factory.createCallExpression(ctx.factory.createIdentifier(validatorName), undefined, [paramIdentifier]);
@@ -320,31 +541,121 @@ export class TypicalTransformer {
320
541
  });
321
542
  // First visit all child nodes (including JSON calls) before adding validation
322
543
  const visitedBody = ctx.ts.visitNode(body, visit);
544
+ // Also track const declarations with type annotations as validated
545
+ // (the assignment will be validated, and const can't be reassigned)
546
+ const collectConstDeclarations = (node) => {
547
+ if (ts.isVariableStatement(node)) {
548
+ const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
549
+ if (isConst) {
550
+ for (const decl of node.declarationList.declarations) {
551
+ if (decl.type && ts.isIdentifier(decl.name)) {
552
+ // Skip any/unknown types
553
+ if (decl.type.kind !== ts.SyntaxKind.AnyKeyword &&
554
+ decl.type.kind !== ts.SyntaxKind.UnknownKeyword) {
555
+ const constType = typeChecker.getTypeFromTypeNode(decl.type);
556
+ validatedVariables.set(decl.name.text, constType);
557
+ }
558
+ }
559
+ }
560
+ }
561
+ }
562
+ ts.forEachChild(node, collectConstDeclarations);
563
+ };
564
+ collectConstDeclarations(visitedBody);
323
565
  // Transform return statements - use explicit type or infer from type checker
324
566
  let transformedStatements = visitedBody.statements;
325
567
  let returnType = func.type;
568
+ // Check if this is an async function
569
+ const isAsync = func.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.AsyncKeyword);
326
570
  // If no explicit return type, try to infer it from the type checker
327
571
  let returnTypeForString;
328
572
  if (!returnType) {
329
- const signature = typeChecker.getSignatureFromDeclaration(func);
330
- if (signature) {
331
- const inferredReturnType = typeChecker.getReturnTypeOfSignature(signature);
332
- returnType = typeChecker.typeToTypeNode(inferredReturnType, func, ts.NodeBuilderFlags.InTypeAlias);
333
- returnTypeForString = inferredReturnType;
573
+ try {
574
+ const signature = typeChecker.getSignatureFromDeclaration(func);
575
+ if (signature) {
576
+ const inferredReturnType = typeChecker.getReturnTypeOfSignature(signature);
577
+ returnType = typeChecker.typeToTypeNode(inferredReturnType, func, ts.NodeBuilderFlags.InTypeAlias);
578
+ returnTypeForString = inferredReturnType;
579
+ }
580
+ }
581
+ catch {
582
+ // Could not infer signature (e.g., untyped arrow function callback)
583
+ // Skip return type validation for this function
334
584
  }
335
585
  }
336
586
  else {
337
587
  // For explicit return types, get the Type from the TypeNode
338
588
  returnTypeForString = typeChecker.getTypeFromTypeNode(returnType);
339
589
  }
340
- if (returnType && returnTypeForString) {
590
+ // For async functions, unwrap Promise<T> to get T
591
+ // The return statement in an async function returns T, not Promise<T>
592
+ if (isAsync && returnType && returnTypeForString) {
593
+ const promiseSymbol = returnTypeForString.getSymbol();
594
+ if (promiseSymbol && promiseSymbol.getName() === "Promise") {
595
+ // Get the type argument of Promise<T>
596
+ const typeArgs = returnTypeForString.typeArguments;
597
+ if (typeArgs && typeArgs.length > 0) {
598
+ returnTypeForString = typeArgs[0];
599
+ // Also update the TypeNode to match
600
+ if (ts.isTypeReferenceNode(returnType) && returnType.typeArguments && returnType.typeArguments.length > 0) {
601
+ returnType = returnType.typeArguments[0];
602
+ }
603
+ else {
604
+ // Create a new type node from the unwrapped type
605
+ returnType = typeChecker.typeToTypeNode(returnTypeForString, func, ts.NodeBuilderFlags.InTypeAlias);
606
+ }
607
+ }
608
+ }
609
+ }
610
+ // Skip 'any' and 'unknown' return types - no point validating them
611
+ const isAnyOrUnknownReturn = returnType && (returnType.kind === ts.SyntaxKind.AnyKeyword ||
612
+ returnType.kind === ts.SyntaxKind.UnknownKeyword);
613
+ if (returnType && returnTypeForString && !isAnyOrUnknownReturn) {
341
614
  const returnTransformer = (node) => {
342
615
  if (ts.isReturnStatement(node) && node.expression) {
616
+ // Skip return validation if the expression already contains a __typical _parse_* call
617
+ // since typia.assertParse already validates the parsed data
618
+ const containsTypicalParse = (n) => {
619
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
620
+ const name = n.expression.text;
621
+ if (name.startsWith("__typical" + "_parse_")) {
622
+ return true;
623
+ }
624
+ }
625
+ return ts.forEachChild(n, containsTypicalParse) || false;
626
+ };
627
+ if (containsTypicalParse(node.expression)) {
628
+ return node; // Already validated by parse, skip return validation
629
+ }
630
+ // Flow analysis: Skip return validation if returning a validated variable
631
+ // (or property of one) that hasn't been tainted
632
+ const rootVar = getRootIdentifier(node.expression);
633
+ if (rootVar && validatedVariables.has(rootVar)) {
634
+ // Check if the variable has been tainted (mutated, passed to function, etc.)
635
+ if (!isTainted(rootVar, visitedBody)) {
636
+ // Return expression is rooted at a validated, untainted variable
637
+ // For direct returns (identifier) or property access, we can skip validation
638
+ if (ts.isIdentifier(node.expression) || ts.isPropertyAccessExpression(node.expression)) {
639
+ return node; // Skip validation - already validated and untainted
640
+ }
641
+ }
642
+ }
643
+ // For async functions, we need to await the expression before validating
644
+ // because the return expression might be a Promise
645
+ let expressionToValidate = node.expression;
646
+ if (isAsync) {
647
+ // Check if the expression is already an await expression
648
+ const isAlreadyAwaited = ts.isAwaitExpression(node.expression);
649
+ if (!isAlreadyAwaited) {
650
+ // Wrap in await: return validate(await expr)
651
+ expressionToValidate = ctx.factory.createAwaitExpression(node.expression);
652
+ }
653
+ }
343
654
  if (this.config.reusableValidators) {
344
655
  // Use reusable validators - always use typeToString
345
656
  const returnTypeText = typeChecker.typeToString(returnTypeForString);
346
657
  const validatorName = this.getOrCreateValidator(returnTypeText, returnType);
347
- const validatorCall = ctx.factory.createCallExpression(ctx.factory.createIdentifier(validatorName), undefined, [node.expression]);
658
+ const validatorCall = ctx.factory.createCallExpression(ctx.factory.createIdentifier(validatorName), undefined, [expressionToValidate]);
348
659
  return ctx.factory.updateReturnStatement(node, validatorCall);
349
660
  }
350
661
  else {
@@ -352,7 +663,7 @@ export class TypicalTransformer {
352
663
  const typiaIdentifier = ctx.factory.createIdentifier("typia");
353
664
  const assertIdentifier = ctx.factory.createIdentifier("assert");
354
665
  const propertyAccess = ctx.factory.createPropertyAccessExpression(typiaIdentifier, assertIdentifier);
355
- const callExpression = ctx.factory.createCallExpression(propertyAccess, [returnType], [node.expression]);
666
+ const callExpression = ctx.factory.createCallExpression(propertyAccess, [returnType], [expressionToValidate]);
356
667
  return ctx.factory.updateReturnStatement(node, callExpression);
357
668
  }
358
669
  }
@@ -418,7 +729,7 @@ export class TypicalTransformer {
418
729
  if (this.typeValidators.has(typeText)) {
419
730
  return this.typeValidators.get(typeText).name;
420
731
  }
421
- const validatorName = `__typical_assert_${this.typeValidators.size}`;
732
+ const validatorName = `__typical_` + `assert_${this.typeValidators.size}`;
422
733
  this.typeValidators.set(typeText, { name: validatorName, typeNode });
423
734
  return validatorName;
424
735
  }
@@ -426,7 +737,7 @@ export class TypicalTransformer {
426
737
  if (this.typeStringifiers.has(typeText)) {
427
738
  return this.typeStringifiers.get(typeText).name;
428
739
  }
429
- const stringifierName = `__typical_stringify_${this.typeStringifiers.size}`;
740
+ const stringifierName = `__typical_` + `stringify_${this.typeStringifiers.size}`;
430
741
  this.typeStringifiers.set(typeText, { name: stringifierName, typeNode });
431
742
  return stringifierName;
432
743
  }
@@ -434,7 +745,7 @@ export class TypicalTransformer {
434
745
  if (this.typeParsers.has(typeText)) {
435
746
  return this.typeParsers.get(typeText).name;
436
747
  }
437
- const parserName = `__typical_parse_${this.typeParsers.size}`;
748
+ const parserName = `__typical_` + `parse_${this.typeParsers.size}`;
438
749
  this.typeParsers.set(typeText, { name: parserName, typeNode });
439
750
  return parserName;
440
751
  }