@@ -336,6 +336,208 @@ function getCaptureName(expr: ts.Expression): string | undefined {
336336 return undefined ;
337337}
338338
339+ /**
340+ * Build a TypeNode for the callback parameter and register property TypeNodes in typeRegistry.
341+ * Returns a TypeLiteral representing { elem: T, index?: number, params: {...} }
342+ */
343+ function buildCallbackParamTypeNode (
344+ mapCall : ts . CallExpression ,
345+ elemParam : ts . ParameterDeclaration | undefined ,
346+ indexParam : ts . ParameterDeclaration | undefined ,
347+ capturedVarNames : Set < string > ,
348+ captures : Map < string , ts . Expression > ,
349+ context : TransformationContext ,
350+ ) : ts . TypeNode {
351+ const { factory, checker } = context ;
352+ const typeRegistry = context . options . typeRegistry ;
353+
354+ // 1. Build elem type property
355+ let elemTypeNode : ts . TypeNode ;
356+ let elemType : ts . Type | undefined ;
357+
358+ // Check if we have an explicit type annotation that's not 'any'
359+ if ( elemParam ?. type ) {
360+ const annotationType = checker . getTypeFromTypeNode ( elemParam . type ) ;
361+ if ( ! ( annotationType . flags & ts . TypeFlags . Any ) ) {
362+ // Use the explicit annotation
363+ elemTypeNode = elemParam . type ;
364+ elemType = annotationType ;
365+ } else {
366+ // Annotation is 'any', try to infer from array
367+ const inferred = inferElementType ( mapCall , context ) ;
368+ elemTypeNode = inferred . typeNode ;
369+ elemType = inferred . type ;
370+ }
371+ } else {
372+ // No annotation, infer from array
373+ const inferred = inferElementType ( mapCall , context ) ;
374+ elemTypeNode = inferred . typeNode ;
375+ elemType = inferred . type ;
376+ }
377+
378+ // Register elem TypeNode if we have a Type
379+ if ( typeRegistry && elemType ) {
380+ typeRegistry . set ( elemTypeNode , elemType ) ;
381+ }
382+
383+ const callbackParamProperties : ts . TypeElement [ ] = [
384+ factory . createPropertySignature (
385+ undefined ,
386+ factory . createIdentifier ( "elem" ) ,
387+ undefined ,
388+ elemTypeNode ,
389+ ) ,
390+ ] ;
391+
392+ // 2. Add index property if present
393+ if ( indexParam ) {
394+ callbackParamProperties . push (
395+ factory . createPropertySignature (
396+ undefined ,
397+ factory . createIdentifier ( "index" ) ,
398+ factory . createToken ( ts . SyntaxKind . QuestionToken ) ,
399+ factory . createKeywordTypeNode ( ts . SyntaxKind . NumberKeyword ) ,
400+ ) ,
401+ ) ;
402+ }
403+
404+ // 3. Build params object type with captured variables
405+ const paramsProperties : ts . TypeElement [ ] = [ ] ;
406+ for ( const varName of capturedVarNames ) {
407+ const expr = captures . get ( varName ) ;
408+ if ( ! expr ) continue ;
409+
410+ // Get the Type of the captured expression
411+ const exprType = checker . getTypeAtLocation ( expr ) ;
412+
413+ // Convert Type to TypeNode
414+ const typeNode = checker . typeToTypeNode (
415+ exprType ,
416+ context . sourceFile ,
417+ ts . NodeBuilderFlags . NoTruncation | ts . NodeBuilderFlags . UseStructuralFallback ,
418+ ) ?? factory . createKeywordTypeNode ( ts . SyntaxKind . UnknownKeyword ) ;
419+
420+ // Register this property's TypeNode with its Type
421+ if ( typeRegistry ) {
422+ typeRegistry . set ( typeNode , exprType ) ;
423+ }
424+
425+ paramsProperties . push (
426+ factory . createPropertySignature (
427+ undefined ,
428+ factory . createIdentifier ( varName ) ,
429+ undefined ,
430+ typeNode ,
431+ ) ,
432+ ) ;
433+ }
434+
435+ // Add params property
436+ callbackParamProperties . push (
437+ factory . createPropertySignature (
438+ undefined ,
439+ factory . createIdentifier ( "params" ) ,
440+ undefined ,
441+ factory . createTypeLiteralNode ( paramsProperties ) ,
442+ ) ,
443+ ) ;
444+
445+ return factory . createTypeLiteralNode ( callbackParamProperties ) ;
446+ }
447+
448+ /**
449+ * Infer the element type from an OpaqueRef<T[]> or Array<T> being mapped.
450+ */
451+ function inferElementType (
452+ mapCall : ts . CallExpression ,
453+ context : TransformationContext ,
454+ ) : { typeNode : ts . TypeNode ; type ?: ts . Type } {
455+ const { factory, checker } = context ;
456+
457+ if ( ! ts . isPropertyAccessExpression ( mapCall . expression ) ) {
458+ return {
459+ typeNode : factory . createKeywordTypeNode ( ts . SyntaxKind . UnknownKeyword ) ,
460+ } ;
461+ }
462+
463+ const arrayExpr = mapCall . expression . expression ;
464+ const arrayType = checker . getTypeAtLocation ( arrayExpr ) ;
465+
466+ // Handle OpaqueRef<T[]> which is an intersection type
467+ let actualArrayType = arrayType ;
468+ if ( arrayType . flags & ts . TypeFlags . Intersection ) {
469+ const intersectionType = arrayType as ts . IntersectionType ;
470+ // Look for the Reference type member (e.g., OpaqueRefMethods<T[]>)
471+ for ( const type of intersectionType . types ) {
472+ if ( type . flags & ts . TypeFlags . Object ) {
473+ const objType = type as ts . ObjectType ;
474+ if ( objType . objectFlags & ts . ObjectFlags . Reference ) {
475+ actualArrayType = type ;
476+ break ;
477+ }
478+ }
479+ }
480+ }
481+
482+ // Get type arguments from the reference type
483+ let typeArgs : readonly ts . Type [ ] | undefined ;
484+ if ( actualArrayType . flags & ts . TypeFlags . Object ) {
485+ const objectType = actualArrayType as ts . ObjectType ;
486+ if ( objectType . objectFlags & ts . ObjectFlags . Reference ) {
487+ typeArgs = checker . getTypeArguments ( objectType as ts . TypeReference ) ;
488+ }
489+ }
490+
491+ if ( typeArgs && typeArgs . length > 0 ) {
492+ const innerType = typeArgs [ 0 ] ;
493+ if ( innerType ) {
494+ // innerType is either T[] or T depending on the structure
495+ let elementType : ts . Type ;
496+ if ( checker . isArrayType ( innerType ) ) {
497+ // It's T[], extract T
498+ const extracted = checker . getIndexTypeOfType ( innerType , ts . IndexKind . Number ) ;
499+ if ( extracted ) {
500+ elementType = extracted ;
501+ } else {
502+ return {
503+ typeNode : factory . createKeywordTypeNode ( ts . SyntaxKind . UnknownKeyword ) ,
504+ } ;
505+ }
506+ } else {
507+ // It's already T
508+ elementType = innerType ;
509+ }
510+
511+ // Convert Type to TypeNode
512+ const typeNode = checker . typeToTypeNode (
513+ elementType ,
514+ context . sourceFile ,
515+ ts . NodeBuilderFlags . NoTruncation | ts . NodeBuilderFlags . UseStructuralFallback ,
516+ ) ?? factory . createKeywordTypeNode ( ts . SyntaxKind . UnknownKeyword ) ;
517+
518+ return { typeNode, type : elementType } ;
519+ }
520+ }
521+
522+ // Fallback for plain Array<T>
523+ if ( checker . isArrayType ( arrayType ) ) {
524+ const elementType = checker . getIndexTypeOfType ( arrayType , ts . IndexKind . Number ) ;
525+ if ( elementType ) {
526+ const typeNode = checker . typeToTypeNode (
527+ elementType ,
528+ context . sourceFile ,
529+ ts . NodeBuilderFlags . NoTruncation | ts . NodeBuilderFlags . UseStructuralFallback ,
530+ ) ?? factory . createKeywordTypeNode ( ts . SyntaxKind . UnknownKeyword ) ;
531+
532+ return { typeNode, type : elementType } ;
533+ }
534+ }
535+
536+ return {
537+ typeNode : factory . createKeywordTypeNode ( ts . SyntaxKind . UnknownKeyword ) ,
538+ } ;
539+ }
540+
339541/**
340542 * Transform a map callback that captures variables.
341543 */
@@ -562,19 +764,28 @@ function transformMapCallback(
562764 transformedBody ,
563765 ) ;
564766
565- // Wrap in recipe() using the proper imported identifier
767+ // Build a TypeNode for the callback parameter to pass as a type argument to recipe<T>()
768+ // The callback signature is: ({ elem, index?, params: { captured1, captured2, ... } }) => ...
769+ // Also register individual property TypeNodes in typeRegistry so SchemaGeneratorTransformer can resolve them
770+ const callbackParamTypeNode = buildCallbackParamTypeNode (
771+ mapCall ,
772+ elemParam ,
773+ indexParam ,
774+ capturedVarNames ,
775+ captures ,
776+ context ,
777+ ) ;
778+
779+ // Wrap in recipe<T>() using type argument (SchemaInjectionTransformer will convert to toSchema<T>)
566780 const recipeIdentifier = getHelperIdentifier (
567781 factory ,
568782 context . sourceFile ,
569783 "recipe" ,
570784 ) ;
571785 const recipeCall = factory . createCallExpression (
572786 recipeIdentifier ,
573- undefined ,
574- [
575- factory . createStringLiteral ( "map with pattern including captures" ) ,
576- newCallback ,
577- ] ,
787+ [ callbackParamTypeNode ] , // Type argument
788+ [ newCallback ] ,
578789 ) ;
579790
580791 // Create the params object
0 commit comments