@@ -580,11 +580,13 @@ defmodule AshTypescript.Rpc.Codegen do
580580 type InferResult<
581581 T extends TypedSchema,
582582 SelectedFields extends UnifiedFieldSelection<T>[],
583- > = UnionToIntersection<
584- {
585- [K in keyof SelectedFields]: InferFieldValue<T, SelectedFields[K]>;
586- }[number]
587- >;
583+ > = SelectedFields extends []
584+ ? {}
585+ : UnionToIntersection<
586+ {
587+ [K in keyof SelectedFields]: InferFieldValue<T, SelectedFields[K]>;
588+ }[number]
589+ >;
588590
589591 // Pagination conditional types
590592 // Checks if a page configuration object has any pagination parameters
@@ -2110,11 +2112,10 @@ defmodule AshTypescript.Rpc.Codegen do
21102112 config_fields
21112113 end
21122114
2113- defp build_payload_fields ( _resource , _action , rpc_action_name , context , opts ) do
2115+ defp build_payload_fields ( rpc_action_name , context , opts ) do
21142116 include_fields = Keyword . get ( opts , :include_fields , false )
21152117 include_filtering_pagination = Keyword . get ( opts , :include_filtering_pagination , true )
21162118 include_metadata_fields = Keyword . get ( opts , :include_metadata_fields , false )
2117-
21182119 payload_fields = [ "action: \" #{ rpc_action_name } \" " ]
21192120
21202121 payload_fields =
@@ -2145,7 +2146,7 @@ defmodule AshTypescript.Rpc.Codegen do
21452146 if include_fields do
21462147 payload_fields ++
21472148 [
2148- "...(config.#{ formatted_fields_field ( ) } && { #{ formatted_fields_field ( ) } : config.#{ formatted_fields_field ( ) } })"
2149+ "...(config.#{ formatted_fields_field ( ) } !== undefined && { #{ formatted_fields_field ( ) } : config.#{ formatted_fields_field ( ) } })"
21492150 ]
21502151 else
21512152 payload_fields
@@ -2544,7 +2545,7 @@ defmodule AshTypescript.Rpc.Codegen do
25442545 generic_part = if generic_param != "" , do: "<#{ generic_param } >" , else: ""
25452546
25462547 payload_fields =
2547- build_payload_fields ( resource , action , rpc_action_name , context ,
2548+ build_payload_fields ( rpc_action_name , context ,
25482549 include_fields: has_fields ,
25492550 include_metadata_fields: has_metadata
25502551 )
@@ -2553,10 +2554,49 @@ defmodule AshTypescript.Rpc.Codegen do
25532554
25542555 before_request_hook = generate_before_request_hook_call ( hook_config , rpc_action_name , false )
25552556
2557+ overloads =
2558+ if action . type in [ :create , :update ] and has_fields do
2559+ config_type_without_fields =
2560+ config_fields
2561+ |> Enum . reject ( & String . contains? ( & 1 , "#{ formatted_fields_field ( ) } " ) )
2562+ |> then ( fn fields -> "{\n #{ Enum . join ( fields , "\n " ) } \n }" end )
2563+
2564+ if has_metadata do
2565+ metadata_param =
2566+ "MetadataFields extends ReadonlyArray<keyof #{ rpc_action_name_pascal } Metadata> = []"
2567+
2568+ """
2569+ export async function #{ function_name } <#{ metadata_param } >(
2570+ config: #{ config_type_without_fields }
2571+ ): Promise<#{ rpc_action_name_pascal } Result<[], MetadataFields>>;
2572+ export async function #{ function_name } <#{ metadata_param } >(
2573+ config: #{ config_type_without_fields } & { #{ formatted_fields_field ( ) } : [] }
2574+ ): Promise<#{ rpc_action_name_pascal } Result<[], MetadataFields>>;
2575+ export async function #{ function_name } <Fields extends #{ rpc_action_name_pascal } Fields, #{ metadata_param } >(
2576+ config: #{ config_type_without_fields } & { #{ formatted_fields_field ( ) } : Fields }
2577+ ): Promise<#{ rpc_action_name_pascal } Result<Fields, MetadataFields>>;
2578+ """
2579+ else
2580+ """
2581+ export async function #{ function_name } (
2582+ config: #{ config_type_without_fields }
2583+ ): Promise<#{ rpc_action_name_pascal } Result<[]>>;
2584+ export async function #{ function_name } (
2585+ config: #{ config_type_without_fields } & { #{ formatted_fields_field ( ) } : never[] }
2586+ ): Promise<#{ rpc_action_name_pascal } Result<[]>>;
2587+ export async function #{ function_name } <Fields extends #{ rpc_action_name_pascal } Fields>(
2588+ config: #{ config_type_without_fields } & { #{ formatted_fields_field ( ) } : Fields }
2589+ ): Promise<#{ rpc_action_name_pascal } Result<Fields>>;
2590+ """
2591+ end
2592+ else
2593+ ""
2594+ end
2595+
25562596 """
25572597 #{ config_type_export } #{ result_type_def }
25582598
2559- export async function #{ function_name } #{ generic_part } (
2599+ #{ overloads } export async function #{ function_name } #{ generic_part } (
25602600 #{ function_signature }
25612601 ): Promise<#{ return_type_def } > {
25622602 #{ before_request_hook }
@@ -2650,7 +2690,7 @@ defmodule AshTypescript.Rpc.Codegen do
26502690
26512691 # Build the validation payload using helper (no fields or filtering/pagination for validation)
26522692 validation_payload_fields =
2653- build_payload_fields ( resource , action , rpc_action_name , context ,
2693+ build_payload_fields ( rpc_action_name , context ,
26542694 include_fields: false ,
26552695 include_filtering_pagination: false
26562696 )
@@ -2739,7 +2779,7 @@ defmodule AshTypescript.Rpc.Codegen do
27392779
27402780 # Build the payload using helper (no fields or filtering/pagination for validation)
27412781 payload_fields =
2742- build_payload_fields ( resource , action , rpc_action_name , context ,
2782+ build_payload_fields ( rpc_action_name , context ,
27432783 include_fields: false ,
27442784 include_filtering_pagination: false
27452785 )
@@ -2981,7 +3021,7 @@ defmodule AshTypescript.Rpc.Codegen do
29813021
29823022 # Build the payload using helper - include metadata_fields only when metadata is exposed
29833023 payload_fields =
2984- build_payload_fields ( resource , action , rpc_action_name , context ,
3024+ build_payload_fields ( rpc_action_name , context ,
29853025 include_fields: has_fields ,
29863026 include_metadata_fields: has_metadata_for_payload
29873027 )
@@ -3025,8 +3065,93 @@ defmodule AshTypescript.Rpc.Codegen do
30253065 end
30263066 end
30273067
3068+ # Unlike RPC functions which return Promise<Result>, channel functions use resultHandler callbacks.
3069+ # Generate overloads to enable proper type inference of the callback parameter based on fields presence.
3070+ overloads =
3071+ if action . type in [ :create , :update ] and has_fields do
3072+ base_config_fields =
3073+ config_fields
3074+ |> Enum . reject (
3075+ & ( String . contains? ( & 1 , "#{ formatted_fields_field ( ) } ?" ) or
3076+ String . contains? ( & 1 , "resultHandler:" ) )
3077+ )
3078+
3079+ if has_metadata do
3080+ metadata_param =
3081+ "MetadataFields extends ReadonlyArray<keyof #{ rpc_action_name_pascal } Metadata> = []"
3082+
3083+ no_fields_config =
3084+ base_config_fields ++
3085+ [
3086+ " resultHandler: (result: #{ rpc_action_name_pascal } Result<[], MetadataFields>) => void;"
3087+ ]
3088+
3089+ no_fields_config_str = "{\n #{ Enum . join ( no_fields_config , "\n " ) } \n }"
3090+
3091+ empty_fields_config =
3092+ base_config_fields ++
3093+ [
3094+ " resultHandler: (result: #{ rpc_action_name_pascal } Result<[], MetadataFields>) => void;"
3095+ ]
3096+
3097+ empty_fields_config_str = "{\n #{ Enum . join ( empty_fields_config , "\n " ) } \n }"
3098+
3099+ with_fields_config =
3100+ base_config_fields ++
3101+ [
3102+ " resultHandler: (result: #{ rpc_action_name_pascal } Result<Fields, MetadataFields>) => void;"
3103+ ]
3104+
3105+ with_fields_config_str = "{\n #{ Enum . join ( with_fields_config , "\n " ) } \n }"
3106+
3107+ """
3108+ export async function #{ function_name } <Fields extends #{ rpc_action_name_pascal } Fields, #{ metadata_param } >(
3109+ config: #{ with_fields_config_str } & { #{ formatted_fields_field ( ) } : Fields }
3110+ ): Promise<void>;
3111+ export async function #{ function_name } <#{ metadata_param } >(
3112+ config: #{ empty_fields_config_str } & { #{ formatted_fields_field ( ) } : [] }
3113+ ): Promise<void>;
3114+ export async function #{ function_name } <#{ metadata_param } >(
3115+ config: #{ no_fields_config_str }
3116+ ): Promise<void>;
3117+ """
3118+ else
3119+ no_fields_config =
3120+ base_config_fields ++
3121+ [ " resultHandler: (result: #{ rpc_action_name_pascal } Result<[]>) => void;" ]
3122+
3123+ no_fields_config_str = "{\n #{ Enum . join ( no_fields_config , "\n " ) } \n }"
3124+
3125+ empty_fields_config =
3126+ base_config_fields ++
3127+ [ " resultHandler: (result: #{ rpc_action_name_pascal } Result<[]>) => void;" ]
3128+
3129+ empty_fields_config_str = "{\n #{ Enum . join ( empty_fields_config , "\n " ) } \n }"
3130+
3131+ with_fields_config =
3132+ base_config_fields ++
3133+ [ " resultHandler: (result: #{ rpc_action_name_pascal } Result<Fields>) => void;" ]
3134+
3135+ with_fields_config_str = "{\n #{ Enum . join ( with_fields_config , "\n " ) } \n }"
3136+
3137+ """
3138+ export async function #{ function_name } <Fields extends #{ rpc_action_name_pascal } Fields>(
3139+ config: #{ with_fields_config_str } & { #{ formatted_fields_field ( ) } : Fields }
3140+ ): Promise<void>;
3141+ export async function #{ function_name } (
3142+ config: #{ empty_fields_config_str } & { #{ formatted_fields_field ( ) } : [] }
3143+ ): Promise<void>;
3144+ export async function #{ function_name } (
3145+ config: #{ no_fields_config_str }
3146+ ): Promise<void>;
3147+ """
3148+ end
3149+ else
3150+ ""
3151+ end
3152+
30283153 """
3029- export async function #{ function_name } #{ generic_part } (config: #{ config_type_def } ) {
3154+ #{ overloads } export async function #{ function_name } #{ generic_part } (config: #{ config_type_def } ) {
30303155 #{ before_channel_push_hook }
30313156
30323157 const timeout = config.timeout ?? processedConfig.timeout;
0 commit comments