Skip to main content

ts_function/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{
4    Error, FnArg, GenericArgument, Ident, Item, ItemImpl, ItemType, PathArguments, ReturnType,
5    Type, parse_macro_input,
6};
7
8#[macro_use]
9mod ts_type;
10mod ts_macro;
11
12use crate::ts_type::ToTsType;
13
14/// Generates TypeScript type aliases and `wasm-bindgen` ABI trait implementations
15/// for Rust items.
16///
17/// This attribute can be applied to:
18/// 1. **Structs**: Generates a TypeScript interface and property bindings.
19/// 2. **Enums**: Automatically applies `#[wasm_bindgen]` (supports C-style enums).
20/// 3. **Type Aliases**: Generates a TypeScript type alias and a typed function wrapper.
21/// 4. **Impl Blocks**: Generates a typed function wrapper for a `call` method.
22///
23/// # Examples
24///
25/// **Struct Usage**
26///
27/// ```rust,ignore
28/// #[ts(rename_all = "camelCase")]
29/// struct MyStruct {
30///     field_name: String,
31/// }
32/// ```
33///
34/// **Enum Usage**
35///
36/// ```rust,ignore
37/// #[ts]
38/// enum Status { Active, Inactive }
39/// ```
40///
41/// **Function Wrapper Usage**
42///
43/// ```rust,ignore
44/// #[ts]
45/// pub type OnReady = fn(msg: String);
46///
47/// #[ts]
48/// struct AppFunctions {
49///     on_ready: OnReady,
50/// }
51/// ```
52#[proc_macro_attribute]
53pub fn ts(attr: TokenStream, input: TokenStream) -> TokenStream {
54    let item = parse_macro_input!(input as Item);
55    ts_internal_dispatcher(attr.into(), item).into()
56}
57
58fn ts_internal_dispatcher(attr: proc_macro2::TokenStream, item: Item) -> proc_macro2::TokenStream {
59    let attr_args = attr.clone();
60
61    match &item {
62        Item::Struct(item_struct) => {
63            let args = match syn::parse2::<ts_macro::TsArgs>(attr_args) {
64                Ok(args) => args,
65                Err(err) => return err.to_compile_error(),
66            };
67            ts_macro::ts_internal(args, item_struct.clone())
68        }
69        Item::Enum(item_enum) => {
70            let enum_name = &item_enum.ident;
71            let variants = item_enum
72                .variants
73                .iter()
74                .map(|variant| {
75                    if !matches!(variant.fields, syn::Fields::Unit) {
76                        return Err(Error::new_spanned(
77                            variant,
78                            "#[ts] enums must only contain unit variants",
79                        ));
80                    }
81                    Ok(&variant.ident)
82                })
83                .collect::<syn::Result<Vec<_>>>();
84            let variants = match variants {
85                Ok(variants) => variants,
86                Err(err) => return err.to_compile_error(),
87            };
88            let variant_constants = variants
89                .iter()
90                .map(|variant| {
91                    let name = format_ident!(
92                        "__TS_FUNCTION_VARIANT_{}",
93                        variant.to_string().to_uppercase()
94                    );
95                    quote! { const #name: u32 = #enum_name::#variant as u32; }
96                })
97                .collect::<Vec<_>>();
98            let conversion_arms = variants
99                .iter()
100                .map(|variant| {
101                    let name = format_ident!(
102                        "__TS_FUNCTION_VARIANT_{}",
103                        variant.to_string().to_uppercase()
104                    );
105                    quote! { #name => ::std::result::Result::Ok(Self::#variant), }
106                })
107                .collect::<Vec<_>>();
108            quote! {
109                #[::wasm_bindgen::prelude::wasm_bindgen]
110                #item_enum
111
112                impl ::std::convert::TryFrom<::wasm_bindgen::JsValue> for #enum_name {
113                    type Error = ::wasm_bindgen::JsValue;
114
115                    #[inline]
116                    fn try_from(value: ::wasm_bindgen::JsValue) -> ::std::result::Result<Self, Self::Error> {
117                        let value = match value.as_f64() {
118                            ::std::option::Option::Some(value) => value,
119                            ::std::option::Option::None => {
120                                return ::std::result::Result::Err(::wasm_bindgen::JsValue::from_str(
121                                    concat!("Expected a number for enum ", stringify!(#enum_name)),
122                                ));
123                            }
124                        };
125                        if !value.is_finite()
126                            || value.fract() != 0.0
127                            || !(0.0..=u32::MAX as f64).contains(&value)
128                        {
129                            return ::std::result::Result::Err(::wasm_bindgen::JsValue::from_str(&format!(
130                                "Invalid {} variant: {}", stringify!(#enum_name), value
131                            )));
132                        }
133                        #(#variant_constants)*
134                        match value as u32 {
135                            #(#conversion_arms)*
136                            _ => ::std::result::Result::Err(::wasm_bindgen::JsValue::from_str(&format!(
137                                "Invalid {} variant: {}", stringify!(#enum_name), value
138                            ))),
139                        }
140                    }
141                }
142            }
143        }
144        Item::Type(item_type) => match parse_item_type(item_type) {
145            Ok(tokens) => tokens,
146            Err(err) => err.to_compile_error(),
147        },
148        Item::Impl(item_impl) => match parse_item_impl(item_impl) {
149            Ok(tokens) => tokens,
150            Err(err) => err.to_compile_error(),
151        },
152        _ => Error::new_spanned(
153            item,
154            "#[ts] can only be applied to a struct, enum, type alias, or impl block",
155        )
156        .to_compile_error(),
157    }
158}
159
160struct ParsedSignature<'a> {
161    struct_ident: &'a Ident,
162    args: Vec<(Ident, &'a Type)>,
163    output: &'a ReturnType,
164}
165
166pub(crate) fn generate_try_convert_support(struct_ident: &syn::Ident) -> proc_macro2::TokenStream {
167    let try_convert_name = format_ident!("try_convert_{}", struct_ident);
168    let trait_name = format_ident!("IntoJsValue_{}", struct_ident);
169    quote! {
170        #[allow(non_camel_case_types)]
171        trait #trait_name {
172            fn into_js_value(self) -> ::wasm_bindgen::JsValue;
173        }
174
175        impl #trait_name for ::wasm_bindgen::JsValue {
176            #[inline]
177            fn into_js_value(self) -> ::wasm_bindgen::JsValue {
178                self
179            }
180        }
181
182        impl #trait_name for ::std::convert::Infallible {
183            #[inline]
184            fn into_js_value(self) -> ::wasm_bindgen::JsValue {
185                match self {}
186            }
187        }
188
189        #[inline]
190        #[allow(non_snake_case)]
191        fn #try_convert_name<T, E>(res: ::wasm_bindgen::JsValue) -> ::std::result::Result<T, ::wasm_bindgen::JsValue>
192        where
193            T: ::std::convert::TryFrom<::wasm_bindgen::JsValue, Error = E>,
194            E: #trait_name,
195        {
196            ::std::convert::TryInto::<T>::try_into(res).map_err(#trait_name::into_js_value)
197        }
198    }
199}
200
201pub(crate) fn generate_return_conversion(
202    struct_ident: &syn::Ident,
203    ty: &Type,
204) -> syn::Result<proc_macro2::TokenStream> {
205    let try_convert_name = format_ident!("try_convert_{}", struct_ident);
206    match ty {
207        Type::Path(type_path) => {
208            let segment = type_path
209                .path
210                .segments
211                .last()
212                .ok_or_else(|| Error::new_spanned(ty, "Expected a type segment"))?;
213            let ident = &segment.ident;
214            let ident_str = ident.to_string();
215
216            if let Some(inner_ty) = get_slice_element_type(ty)
217                && let Some(arr_type) = get_typed_array_ident(inner_ty)
218            {
219                return Ok(quote! {
220                    let arr: ::js_sys::#arr_type = ::wasm_bindgen::JsCast::dyn_into(res)
221                        .map_err(|_| ::wasm_bindgen::JsValue::from_str(concat!("Expected a ", stringify!(#arr_type))))?;
222                    ::std::result::Result::Ok::<_, ::wasm_bindgen::JsValue>(::std::convert::Into::<#ty>::into(arr.to_vec()))
223                });
224            }
225
226            match ident_str.as_str() {
227                "f32" | "f64" | "i8" | "i16" | "i32" | "u8" | "u16" | "u32" => Ok(quote! {
228                    res.as_f64().map(|v| v as #ty).ok_or_else(|| ::wasm_bindgen::JsValue::from_str("Expected a number"))
229                }),
230                "i64" | "u64" => Ok(quote! {
231                    ::std::convert::TryInto::<#ty>::try_into(res).map_err(|_| ::wasm_bindgen::JsValue::from_str("Expected a BigInt"))
232                }),
233                "bool" => Ok(quote! {
234                    res.as_bool().ok_or_else(|| ::wasm_bindgen::JsValue::from_str("Expected a boolean"))
235                }),
236                "String" => Ok(quote! {
237                    res.as_string().ok_or_else(|| ::wasm_bindgen::JsValue::from_str("Expected a string"))
238                }),
239                "JsValue" => Ok(quote! {
240                    ::std::result::Result::Ok::<_, ::wasm_bindgen::JsValue>(res)
241                }),
242                "Option" => {
243                    let PathArguments::AngleBracketed(args) = &segment.arguments else {
244                        return Err(Error::new_spanned(
245                            ty,
246                            "Expected generic argument for Option",
247                        ));
248                    };
249                    let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() else {
250                        return Err(Error::new_spanned(ty, "Expected type argument for Option"));
251                    };
252                    let inner_conversion = generate_return_conversion(struct_ident, inner_ty)?;
253                    Ok(quote! {
254                        if res.is_null() || res.is_undefined() {
255                            ::std::result::Result::Ok::<_, ::wasm_bindgen::JsValue>(None)
256                        } else {
257                            let res = { #inner_conversion };
258                            res.map(Some)
259                        }
260                    })
261                }
262                _ => Ok(quote! {
263                    #try_convert_name::<#ty, _>(res)
264                }),
265            }
266        }
267        _ => Err(Error::new_spanned(
268            ty,
269            "Unsupported return type in type alias pattern. Use the `impl` escape hatch instead.",
270        )),
271    }
272}
273
274fn parse_item_type(item_type: &ItemType) -> syn::Result<proc_macro2::TokenStream> {
275    let Type::BareFn(bare_fn) = &*item_type.ty else {
276        return Err(Error::new_spanned(
277            &item_type.ty,
278            "Expected a function pointer type (e.g., `fn(x: f64)`)",
279        ));
280    };
281
282    let struct_ident = &item_type.ident;
283    let mut args = Vec::new();
284
285    for (i, arg) in bare_fn.inputs.iter().enumerate() {
286        let ident = match &arg.name {
287            Some((ident, _)) => ident.clone(),
288            None => format_ident!("arg{}", i),
289        };
290        args.push((ident, &arg.ty));
291    }
292
293    let parsed = ParsedSignature {
294        struct_ident,
295        args: args.clone(),
296        output: &bare_fn.output,
297    };
298
299    let abi_traits = generate_abi_traits(&parsed)?;
300
301    let mut fn_args = Vec::new();
302    let mut arg_conversions = Vec::new();
303    let mut call_args = Vec::new();
304    for (ident, ty) in &args {
305        fn_args.push(quote! { #ident: #ty });
306        let conversion = generate_conversion(ident, ty)?;
307        arg_conversions.push(conversion);
308        call_args.push(quote! { &#ident });
309    }
310
311    let args_len = call_args.len();
312    if args_len > 9 {
313        return Err(Error::new_spanned(
314            item_type,
315            "Functions with more than 9 arguments are not supported yet",
316        ));
317    }
318    let call_method_name = format_ident!("call{}", args_len);
319    let call_method = quote! { #call_method_name(&::wasm_bindgen::JsValue::NULL, #(#call_args),*) };
320
321    let output = parsed.output;
322    let (ret_type, ret_stmt) = match output {
323        ReturnType::Default => (quote! { () }, quote! { self.0.#call_method.map(|_| ()) }),
324        ReturnType::Type(_, ty) => {
325            let conversion = generate_return_conversion(struct_ident, ty)?;
326            (
327                quote! { #ty },
328                quote! {
329                    let res = self.0.#call_method?;
330                    #conversion
331                },
332            )
333        }
334    };
335
336    Ok(quote! {
337        pub struct #struct_ident(pub ::js_sys::Function);
338
339        const _: () = {
340            #abi_traits
341
342            impl #struct_ident {
343                pub fn call(&self, #(#fn_args),*) -> Result<#ret_type, ::wasm_bindgen::JsValue> {
344                    #(#arg_conversions)*
345                    #ret_stmt
346                }
347            }
348        };
349    })
350}
351
352fn generate_conversion(ident: &Ident, ty: &Type) -> syn::Result<proc_macro2::TokenStream> {
353    if let Type::ImplTrait(type_impl) = ty {
354        for bound in &type_impl.bounds {
355            if let syn::TypeParamBound::Trait(trait_bound) = bound
356                && let Some(segment) = trait_bound.path.segments.last()
357                && let PathArguments::AngleBracketed(args) = &segment.arguments
358                && let Some(GenericArgument::Type(inner_ty)) = args.args.first()
359            {
360                match segment.ident.to_string().as_str() {
361                    "Into" => {
362                        let inner_conversion = generate_conversion(ident, inner_ty)?;
363                        return Ok(quote! {
364                            let #ident = ::std::convert::Into::<#inner_ty>::into(#ident);
365                            #inner_conversion
366                        });
367                    }
368                    "AsRef" => {
369                        if let Type::Slice(slice) = inner_ty {
370                            return Ok(generate_typed_array_conversion(ident, &slice.elem));
371                        }
372                    }
373                    _ => {}
374                }
375            }
376        }
377        return Err(Error::new_spanned(
378            ty,
379            "Unsupported `impl Trait`. Only `impl Into<T>` and `impl AsRef<[T]>` are supported.",
380        ));
381    }
382
383    if let Some(inner_ty) = get_slice_element_type(ty) {
384        Ok(generate_typed_array_conversion(ident, inner_ty))
385    } else {
386        Ok(quote! {
387            let #ident = ::std::convert::Into::<::wasm_bindgen::JsValue>::into(#ident);
388        })
389    }
390}
391
392fn generate_typed_array_conversion(ident: &Ident, inner_ty: &Type) -> proc_macro2::TokenStream {
393    if let Some(arr_type) = get_typed_array_ident(inner_ty) {
394        quote! {
395            let #ident = ::wasm_bindgen::JsValue::from(::js_sys::#arr_type::from(::std::convert::AsRef::<[#inner_ty]>::as_ref(&#ident)));
396        }
397    } else {
398        quote! {
399            let #ident = ::wasm_bindgen::JsValue::from(
400                ::std::convert::AsRef::<[#inner_ty]>::as_ref(&#ident)
401                    .iter()
402                    .map(::wasm_bindgen::JsValue::from)
403                    .collect::<::js_sys::Array>()
404            );
405        }
406    }
407}
408
409fn get_typed_array_ident(inner_ty: &Type) -> Option<proc_macro2::TokenStream> {
410    let inner_str = match inner_ty {
411        Type::Path(p) => p.path.segments.last().map(|s| s.ident.to_string()),
412        _ => None,
413    };
414
415    match inner_str.as_deref() {
416        Some("u8") => Some(quote! { Uint8Array }),
417        Some("i8") => Some(quote! { Int8Array }),
418        Some("u16") => Some(quote! { Uint16Array }),
419        Some("i16") => Some(quote! { Int16Array }),
420        Some("u32") => Some(quote! { Uint32Array }),
421        Some("i32") => Some(quote! { Int32Array }),
422        Some("f32") => Some(quote! { Float32Array }),
423        Some("f64") => Some(quote! { Float64Array }),
424        Some("u64") => Some(quote! { BigUint64Array }),
425        Some("i64") => Some(quote! { BigInt64Array }),
426        _ => None,
427    }
428}
429
430fn get_slice_element_type(ty: &Type) -> Option<&Type> {
431    match ty {
432        Type::Path(type_path) => {
433            let segment = type_path.path.segments.last()?;
434            // Types that implement AsRef<[T]> and we can easily extract T from AST
435            if matches!(
436                segment.ident.to_string().as_str(),
437                "Vec" | "Box" | "Arc" | "Rc"
438            ) && let PathArguments::AngleBracketed(args) = &segment.arguments
439                && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
440            {
441                if let Type::Slice(slice) = inner {
442                    return Some(&*slice.elem);
443                }
444                return Some(inner);
445            }
446        }
447        Type::Reference(type_ref) => {
448            if let Type::Slice(type_slice) = &*type_ref.elem {
449                return Some(&*type_slice.elem);
450            }
451            return get_slice_element_type(&type_ref.elem);
452        }
453        _ => {}
454    }
455    None
456}
457
458fn parse_item_impl(item_impl: &ItemImpl) -> syn::Result<proc_macro2::TokenStream> {
459    if item_impl.trait_.is_some() {
460        return Err(Error::new_spanned(
461            item_impl,
462            "#[ts_function] cannot be applied to trait impls",
463        ));
464    }
465
466    let Type::Path(type_path) = &*item_impl.self_ty else {
467        return Err(Error::new_spanned(
468            &item_impl.self_ty,
469            "Expected a simple path for the struct",
470        ));
471    };
472
473    let struct_ident = type_path.path.get_ident().ok_or_else(|| {
474        Error::new_spanned(
475            &type_path.path,
476            "Expected a single identifier for the struct",
477        )
478    })?;
479
480    let method = item_impl
481        .items
482        .iter()
483        .find_map(|item| {
484            if let syn::ImplItem::Fn(method) = item
485                && method.sig.ident == "call"
486            {
487                return Some(method);
488            }
489            None
490        })
491        .ok_or_else(|| Error::new_spanned(item_impl, "Missing `call` method in impl block"))?;
492
493    let mut args = Vec::new();
494    let mut inputs_iter = method.sig.inputs.iter();
495
496    // Check first argument is `&self` or `&mut self`
497    match inputs_iter.next() {
498        Some(FnArg::Receiver(_)) => {}
499        _ => {
500            return Err(Error::new_spanned(
501                &method.sig,
502                "The `call` method must take `&self` or `&mut self` as its first parameter",
503            ));
504        }
505    }
506
507    for (i, arg) in inputs_iter.enumerate() {
508        let FnArg::Typed(pat_type) = arg else {
509            return Err(Error::new_spanned(arg, "Expected a typed argument"));
510        };
511
512        let ident = if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
513            pat_ident.ident.clone()
514        } else {
515            format_ident!("arg{}", i)
516        };
517
518        args.push((ident, &*pat_type.ty));
519    }
520
521    let parsed = ParsedSignature {
522        struct_ident,
523        args,
524        output: &method.sig.output,
525    };
526
527    let abi_traits = generate_abi_traits(&parsed)?;
528
529    Ok(quote! {
530        #item_impl
531        #abi_traits
532    })
533}
534
535fn generate_abi_traits(parsed: &ParsedSignature) -> syn::Result<proc_macro2::TokenStream> {
536    let struct_ident = parsed.struct_ident;
537    let mut ts_args = Vec::new();
538
539    for (ident, ty) in &parsed.args {
540        let ts_ty = ty
541            .to_ts_type()
542            .map_err(|e| Error::new_spanned(ty, e.message))?
543            .to_string();
544        ts_args.push(format!("{}: {}", ident, ts_ty));
545    }
546
547    let ts_output = match parsed.output {
548        ReturnType::Default => "void".to_string(),
549        ReturnType::Type(_, ty) => ty
550            .to_ts_type()
551            .map_err(|e| Error::new_spanned(ty, e.message))?
552            .to_string(),
553    };
554
555    let ts_string = format!(
556        "type {} = ({}) => {};",
557        struct_ident,
558        ts_args.join(", "),
559        ts_output
560    );
561
562    let try_convert_support = generate_try_convert_support(struct_ident);
563
564    let generated = quote! {
565        #[::wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
566        const _: &'static str = #ts_string;
567
568        #try_convert_support
569
570        impl ::wasm_bindgen::describe::WasmDescribe for #struct_ident {
571            fn describe() {
572                <::js_sys::Function as ::wasm_bindgen::describe::WasmDescribe>::describe()
573            }
574        }
575
576        impl ::wasm_bindgen::convert::FromWasmAbi for #struct_ident {
577            type Abi = <::js_sys::Function as ::wasm_bindgen::convert::FromWasmAbi>::Abi;
578
579            unsafe fn from_abi(js: Self::Abi) -> Self {
580                Self(::js_sys::Function::from_abi(js))
581            }
582        }
583
584        impl ::wasm_bindgen::convert::OptionFromWasmAbi for #struct_ident {
585            fn is_none(abi: &Self::Abi) -> bool {
586                <::js_sys::Function as ::wasm_bindgen::convert::OptionFromWasmAbi>::is_none(abi)
587            }
588        }
589
590        impl From<::js_sys::Function> for #struct_ident {
591            fn from(f: ::js_sys::Function) -> Self {
592                Self(f)
593            }
594        }
595
596        impl ::std::convert::TryFrom<::wasm_bindgen::JsValue> for #struct_ident {
597            type Error = ::wasm_bindgen::JsValue;
598
599            #[inline]
600            fn try_from(value: ::wasm_bindgen::JsValue) -> ::std::result::Result<Self, Self::Error> {
601                use ::wasm_bindgen::JsCast;
602                let f = value.dyn_into::<::js_sys::Function>()?;
603                ::std::result::Result::Ok(Self(f))
604            }
605        }
606
607        impl From<#struct_ident> for ::wasm_bindgen::JsValue {
608            fn from(f: #struct_ident) -> Self {
609                ::wasm_bindgen::JsValue::from(f.0)
610            }
611        }
612    };
613
614    Ok(generated)
615}
616
617#[cfg(test)]
618mod tests {
619    use super::*;
620    use syn::parse_quote;
621
622    #[test]
623    fn test_item_type() {
624        let item_type: ItemType = parse_quote! {
625            pub type OnClick = fn(x: f64, y: impl Into<f64>, arr: js_sys::Float64Array);
626        };
627        let result = parse_item_type(&item_type).unwrap();
628        let result_str = result.to_string();
629
630        assert!(
631            result_str
632                .contains("type OnClick = (x: number, y: number, arr: Float64Array) => void;")
633        );
634        assert!(result_str.contains("pub struct OnClick (pub :: js_sys :: Function) ;"));
635        assert!(result_str.contains(
636            "pub fn call (& self , x : f64 , y : impl Into < f64 > , arr : js_sys :: Float64Array)"
637        ));
638    }
639
640    #[test]
641    fn test_item_impl() {
642        let item_impl: ItemImpl = parse_quote! {
643            impl OnScroll {
644                pub fn call(&self, y: f64) {
645                    // body
646                }
647            }
648        };
649        let result = parse_item_impl(&item_impl).unwrap();
650        let result_str = result.to_string();
651
652        assert!(result_str.contains("type OnScroll = (y: number) => void;"));
653        assert!(
654            result_str.contains("impl :: wasm_bindgen :: describe :: WasmDescribe for OnScroll")
655        );
656    }
657
658    #[test]
659    fn test_dispatcher_item_struct() {
660        let input: Item = parse_quote! {
661            pub struct MyStruct {
662                pub field: f64,
663            }
664        };
665        let attr = quote! {};
666        let result = ts_internal_dispatcher(attr, input);
667        let result_str = result.to_string();
668
669        assert!(result_str.contains("export interface MyStruct"));
670        assert!(result_str.contains("field: number;"));
671    }
672
673    #[test]
674    fn test_dispatcher_item_type() {
675        let input: Item = parse_quote! {
676            pub type OnClick = fn(x: f64);
677        };
678        let attr = quote! {};
679        let result = ts_internal_dispatcher(attr, input);
680        let result_str = result.to_string();
681
682        assert!(result_str.contains("type OnClick = (x: number) => void;"));
683        assert!(result_str.contains("pub struct OnClick (pub :: js_sys :: Function) ;"));
684    }
685
686    #[test]
687    fn test_dispatcher_item_impl() {
688        let input: Item = parse_quote! {
689            impl OnScroll {
690                pub fn call(&self, y: f64) {}
691            }
692        };
693        let attr = quote! {};
694        let result = ts_internal_dispatcher(attr, input);
695        let result_str = result.to_string();
696
697        assert!(result_str.contains("type OnScroll = (y: number) => void;"));
698        assert!(
699            result_str.contains("impl :: wasm_bindgen :: describe :: WasmDescribe for OnScroll")
700        );
701    }
702
703    #[test]
704    fn test_enum_item() {
705        let input: Item = parse_quote! {
706            pub enum Status { Active, Inactive }
707        };
708        let attr = quote! {};
709        let result = ts_internal_dispatcher(attr, input);
710        let result_str = result.to_string();
711
712        assert!(result_str.contains("# [:: wasm_bindgen :: prelude :: wasm_bindgen]"));
713        assert!(result_str.contains("pub enum Status { Active , Inactive }"));
714    }
715
716    #[test]
717    fn test_recursive_generics() {
718        let item_type: ItemType = parse_quote! {
719            pub type ResultFn = fn(res: Result<String, i32>);
720        };
721        let result = parse_item_type(&item_type).unwrap();
722        let result_str = result.to_string();
723
724        assert!(result_str.contains("type ResultFn = (res: Result<string, number>) => void;"));
725
726        let item_type: ItemType = parse_quote! {
727            pub type NestedVecFn = fn(args: Vec<Vec<f64>>);
728        };
729        let result = parse_item_type(&item_type).unwrap();
730        let result_str = result.to_string();
731
732        assert!(result_str.contains("type NestedVecFn = (args: Float64Array[]) => void;"));
733    }
734}