Repository: Aleph-Alpha/ts-rs
Pull request #299 (2024-04-08 12:10:50)

Infered types (`_`) in `#[ts(as = "...")]`
## Goal

This PR aims to allow the use of infered types (`_`) inside the `#[ts(as = "...")]` attribute. The `_` type will be interpreted as the field's original type. E.g.:

```rs
#[derive(TS)]
struct Foo {
    #[ts(as = "Option<_>"]
    bar: i32,
}
```

is the same as:
```rs
#[derive(TS)]
struct Foo {
    #[ts(as = "Option<i32>"]
    bar: i32,
}
```
but without having to repeat the original type.

This allows us to somewhat support using `#[ts(optional)]` with any type, by using `#[ts(as = "Option<_>", optional)]` without requiring us to change the semantics of `#[ts(optional)]`

## Changes

Added a recursive function to traverse the provided type and replace `Type::Infer(_)` with the field's original type

## Checklist

- [x] I have followed the steps listed in the [Contributing guide](https://github.com/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md).
- [x] If necessary, I have added documentation related to the changes made.
- [x] I have added or updated the tests related to the changes made.


NyxCode: Interesting! So `_` refers to the original type, right?
NyxCode: I do think this is pretty neat, though I think it might be a bit unintuitive for anyone who doesn't know what `_` does in that context.  
Have you considered a different syntax, e.g `#[ts(as = "Option<type>")]`. I feel like we might be able to come up with something more intuitive, but I'm not sure if the extra effort/complexity involved in parsing this would be worth it.
gustavo-shigueo: > Interesting! So `_` refers to the original type, right?

Yes, this is mostly a way to resolve the discussion in #175
abhay-agarwal: I think the understore is super intuitive, as it's already widely used throughout rust syntax. So the code is instantly readable. The other nice thing is that you could, for example, turn a type into a vector, such as `as = Vec<_>`, or `as = Result<_,Something>`, etc.
escritorio-gustavo: There is also precedent for this behavior in `serde_with` https://github.com/jonasbb/serde_with?tab=readme-ov-file#examples
NyxCode: Oh, alright! Maybe it's just me then.  
I feel like the semantics of `_` are very different here, but I think you two convinced me!
NyxCode: Also, you were spot on with your take in #175. This solution keeps `#[ts(optional)]` as it was. Specifically, `#[ts(optional)]` still doesn't alter the type, just how it's represented. This seems super clean to me!
NyxCode: Interestingly, this kinda makes `#[ts(optional = nullable)]` obsolete. It'd be equivalent to 
```rs
#[ts(as = "Option<_>", optional)]
field: Option<i32>,
```

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d1d01ea..8fc39e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,10 +11,12 @@
 - Add support for `#[ts(type = "..")]` directly on structs and enums ([#286](https://github.com/Aleph-Alpha/ts-rs/pull/286))
 - Add support for `#[ts(as = "..")]` directly on structs and enums ([#288](https://github.com/Aleph-Alpha/ts-rs/pull/288))
 - Add support for `#[ts(rename_all = "SCREAMING-KEBAB-CASE")]` ([#298](https://github.com/Aleph-Alpha/ts-rs/pull/298))
+- Support `_` in `#[ts(type = "..")]` to refer to the type of the field ([#299](https://github.com/Aleph-Alpha/ts-rs/pull/299))
 
 ### Fixes
 
 - Fix `#[ts(rename_all_fields = "...")]` on enums containing tuple or unit variants ([#287](https://github.com/Aleph-Alpha/ts-rs/pull/287))
+- Fix "overflow evaluating the requirement" and "reached the recursion limit" errors in some cases ([#293](https://github.com/Aleph-Alpha/ts-rs/pull/293))
 
 # 8.1.0
 
diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs
index 4dc4d2a..1417777 100644
--- a/macros/src/attr/field.rs
+++ b/macros/src/attr/field.rs
@@ -1,11 +1,15 @@
-use syn::{Attribute, Field, Ident, Result, Type};
+use syn::{
+    AngleBracketedGenericArguments, Attribute, Field, GenericArgument, Ident, PathArguments,
+    Result, ReturnType, Type, TypeArray, TypeGroup, TypeParen, TypePath, TypePtr, TypeReference,
+    TypeSlice, TypeTuple,
+};
 
 use super::{parse_assign_from_str, parse_assign_str, Attr, Serde};
 use crate::utils::{parse_attrs, parse_docs};
 
 #[derive(Default)]
 pub struct FieldAttr {
-    pub type_as: Option<Type>,
+    type_as: Option<Type>,
     pub type_override: Option<String>,
     pub rename: Option<String>,
     pub inline: bool,
@@ -41,6 +45,15 @@ impl FieldAttr {
 
         Ok(result)
     }
+
+    pub fn type_as(&self, original_type: &Type) -> Type {
+        if let Some(mut ty) = self.type_as.clone() {
+            replace_underscore(&mut ty, original_type);
+            ty
+        } else {
+            original_type.clone()
+        }
+    }
 }
 
 impl Attr for FieldAttr {
@@ -202,3 +215,58 @@ impl_parse! {
         },
     }
 }
+
+fn replace_underscore(ty: &mut Type, with: &Type) {
+    match ty {
+        Type::Infer(_) => *ty = with.clone(),
+        Type::Array(TypeArray { elem, .. })
+        | Type::Group(TypeGroup { elem, .. })
+        | Type::Paren(TypeParen { elem, .. })
+        | Type::Ptr(TypePtr { elem, .. })
+        | Type::Reference(TypeReference { elem, .. })
+        | Type::Slice(TypeSlice { elem, .. }) => {
+            replace_underscore(elem, with);
+        }
+        Type::Tuple(TypeTuple { elems, .. }) => {
+            for elem in elems {
+                replace_underscore(elem, with);
+            }
+        }
+        Type::Path(TypePath { path, qself: None }) => {
+            for segment in &mut path.segments {
+                match &mut segment.arguments {
+                    PathArguments::None => (),
+                    PathArguments::AngleBracketed(a) => {
+                        replace_underscore_in_angle_bracketed(a, with);
+                    }
+                    PathArguments::Parenthesized(p) => {
+                        for input in &mut p.inputs {
+                            replace_underscore(input, with);
+                        }
+                        if let ReturnType::Type(_, output) = &mut p.output {
+                            replace_underscore(output, with);
+                        }
+                    }
+                }
+            }
+        }
+        _ => (),
+    }
+}
+
+fn replace_underscore_in_angle_bracketed(args: &mut AngleBracketedGenericArguments, with: &Type) {
+    for arg in &mut args.args {
+        match arg {
+            GenericArgument::Type(ty) => {
+                replace_underscore(ty, with);
+            }
+            GenericArgument::AssocType(assoc_ty) => {
+                replace_underscore(&mut assoc_ty.ty, with);
+                for g in &mut assoc_ty.generics {
+                    replace_underscore_in_angle_bracketed(g, with);
+                }
+            }
+            _ => (),
+        }
+    }
+}
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index 6dcc0f4..3e85a92 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -135,30 +135,16 @@ fn format_variant(
 
                 field_attr.assert_validity(field)?;
 
-                let FieldAttr {
-                    type_as,
-                    type_override,
-                    skip,
-                    ..
-                } = field_attr;
-
-                if skip {
+                if field_attr.skip {
                     quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
                 } else {
-                    let ty = match (type_override, type_as) {
-                        (Some(_), Some(_)) => {
-                            unreachable!("This has been handled by assert_validity")
-                        }
-                        (Some(type_override), None) => quote! { #type_override },
-                        (None, Some(type_as)) => {
-                            quote!(<#type_as as #crate_rename::TS>::name())
-                        }
-                        (None, None) => {
-                            let ty = &unnamed.unnamed[0].ty;
+                    let ty = match field_attr.type_override {
+                        Some(type_override) => quote!(#type_override),
+                        None => {
+                            let ty = field_attr.type_as(&field.ty);
                             quote!(<#ty as #crate_rename::TS>::name())
                         }
                     };
-
                     quote!(format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #ty))
                 }
             }
@@ -191,26 +177,13 @@ fn format_variant(
 
                     field_attr.assert_validity(field)?;
 
-                    let FieldAttr {
-                        type_as,
-                        skip,
-                        type_override,
-                        ..
-                    } = field_attr;
-
-                    if skip {
+                    if field_attr.skip {
                         quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
                     } else {
-                        let ty = match (type_override, type_as) {
-                            (Some(_), Some(_)) => {
-                                unreachable!("This has been handled by assert_validity")
-                            }
-                            (Some(type_override), None) => quote! { #type_override },
-                            (None, Some(type_as)) => {
-                                quote!(<#type_as as #crate_rename::TS>::name())
-                            }
-                            (None, None) => {
-                                let ty = &unnamed.unnamed[0].ty;
+                        let ty = match field_attr.type_override {
+                            Some(type_override) => quote! { #type_override },
+                            None => {
+                                let ty = field_attr.type_as(&field.ty);
                                 quote!(<#ty as #crate_rename::TS>::name())
                             }
                         };
diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs
index 3573c5b..3457b7a 100644
--- a/macros/src/types/named.rs
+++ b/macros/src/types/named.rs
@@ -92,27 +92,13 @@ fn format_field(
 
     field_attr.assert_validity(field)?;
 
-    let FieldAttr {
-        type_as,
-        type_override,
-        rename,
-        inline,
-        skip,
-        optional,
-        flatten,
-        docs,
-
-        #[cfg(feature = "serde-compat")]
-            using_serde_with: _,
-    } = field_attr;
-
-    if skip {
+    if field_attr.skip {
         return Ok(());
     }
 
-    let parsed_ty = type_as.as_ref().unwrap_or(&field.ty).clone();
+    let parsed_ty = field_attr.type_as(&field.ty);
 
-    let (ty, optional_annotation) = match optional {
+    let (ty, optional_annotation) = match field_attr.optional {
         Optional {
             optional: true,
             nullable,
@@ -128,24 +114,27 @@ fn format_field(
         } => (&parsed_ty, ""),
     };
 
-    if flatten {
+    if field_attr.flatten {
         flattened_fields.push(quote!(<#ty as #crate_rename::TS>::inline_flattened()));
         dependencies.append_from(ty);
         return Ok(());
     }
 
-    let formatted_ty = type_override.map(|t| quote!(#t)).unwrap_or_else(|| {
-        if inline {
-            dependencies.append_from(ty);
-            quote!(<#ty as #crate_rename::TS>::inline())
-        } else {
-            dependencies.push(ty);
-            quote!(<#ty as #crate_rename::TS>::name())
-        }
-    });
+    let formatted_ty = field_attr
+        .type_override
+        .map(|t| quote!(#t))
+        .unwrap_or_else(|| {
+            if field_attr.inline {
+                dependencies.append_from(ty);
+                quote!(<#ty as #crate_rename::TS>::inline())
+            } else {
+                dependencies.push(ty);
+                quote!(<#ty as #crate_rename::TS>::name())
+            }
+        });
 
     let field_name = to_ts_ident(field.ident.as_ref().unwrap());
-    let name = match (rename, rename_all) {
+    let name = match (field_attr.rename, rename_all) {
         (Some(rn), _) => rn,
         (None, Some(rn)) => rn.apply(&field_name),
         (None, None) => field_name,
@@ -153,9 +142,9 @@ fn format_field(
     let valid_name = raw_name_to_ts_field(name);
 
     // Start every doc string with a newline, because when other characters are in front, it is not "understood" by VSCode
-    let docs = match docs.is_empty() {
+    let docs = match field_attr.docs.is_empty() {
         true => "".to_string(),
-        false => format!("\n{}", &docs),
+        false => format!("\n{}", &field_attr.docs),
     };
 
     formatted_fields.push(quote! {
diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs
index e993386..c48b955 100644
--- a/macros/src/types/newtype.rs
+++ b/macros/src/types/newtype.rs
@@ -13,34 +13,25 @@ pub(crate) fn newtype(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) ->
     let field_attr = FieldAttr::from_attrs(&inner.attrs)?;
     field_attr.assert_validity(inner)?;
 
-    let FieldAttr {
-        type_as,
-        type_override,
-        inline,
-        skip,
-        docs: _,
-        ..
-    } = field_attr;
-
     let crate_rename = attr.crate_rename();
 
-    if skip {
+    if field_attr.skip {
         return super::unit::null(attr, name);
     }
 
-    let inner_ty = type_as.as_ref().unwrap_or(&inner.ty).clone();
+    let inner_ty = field_attr.type_as(&inner.ty);
 
     let mut dependencies = Dependencies::new(crate_rename.clone());
 
-    match (&type_override, inline) {
+    match (&field_attr.type_override, field_attr.inline) {
         (Some(_), _) => (),
         (None, true) => dependencies.append_from(&inner_ty),
         (None, false) => dependencies.push(&inner_ty),
     };
 
-    let inline_def = match type_override {
+    let inline_def = match field_attr.type_override {
         Some(ref o) => quote!(#o.to_owned()),
-        None if inline => quote!(<#inner_ty as #crate_rename::TS>::inline()),
+        None if field_attr.inline => quote!(<#inner_ty as #crate_rename::TS>::inline()),
         None => quote!(<#inner_ty as #crate_rename::TS>::name()),
     };
 
diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs
index 9b41b81..0b8571a 100644
--- a/macros/src/types/tuple.rs
+++ b/macros/src/types/tuple.rs
@@ -49,33 +49,19 @@ fn format_field(
     let field_attr = FieldAttr::from_attrs(&field.attrs)?;
     field_attr.assert_validity(field)?;
 
-    let FieldAttr {
-        type_as,
-        type_override,
-        rename: _,
-        inline,
-        skip,
-        optional: _,
-        flatten: _,
-        docs: _,
-
-        #[cfg(feature = "serde-compat")]
-            using_serde_with: _,
-    } = field_attr;
-
-    if skip {
+    if field_attr.skip {
         return Ok(());
     }
 
-    let ty = type_as.as_ref().unwrap_or(&field.ty).clone();
+    let ty = field_attr.type_as(&field.ty);
 
-    formatted_fields.push(match type_override {
+    formatted_fields.push(match field_attr.type_override {
         Some(ref o) => quote!(#o.to_owned()),
-        None if inline => quote!(<#ty as #crate_rename::TS>::inline()),
+        None if field_attr.inline => quote!(<#ty as #crate_rename::TS>::inline()),
         None => quote!(<#ty as #crate_rename::TS>::name()),
     });
 
-    match (inline, type_override) {
+    match (field_attr.inline, field_attr.type_override) {
         (_, Some(_)) => (),
         (false, _) => dependencies.push(&ty),
         (true, _) => dependencies.append_from(&ty),
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index 0954e69..5eb7d0e 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -287,7 +287,8 @@ pub mod typelist;
 ///
 /// - **`#[ts(as = "..")]`**  
 ///   Overrides the type of the annotated field, using the provided Rust type instead.
-///   This is useful when there's a type for which you cannot derive `TS`.
+///   This is useful when there's a type for which you cannot derive `TS`.  
+///   `_` may be used to refer to the type of the field, e.g `#[ts(as = "Option<_>")]`.
 ///   <br/><br/>
 ///
 /// - **`#[ts(rename = "..")]`**  


Test patch:
diff --git a/ts-rs/tests/infer_as.rs b/ts-rs/tests/infer_as.rs
new file mode 100644
index 0000000..459aaac
--- /dev/null
+++ b/ts-rs/tests/infer_as.rs
@@ -0,0 +1,15 @@
+#![allow(dead_code)]
+
+use ts_rs::TS;
+
+#[derive(TS)]
+#[ts(export)]
+struct Foo {
+    #[ts(optional, as = "Option<_>")]
+    my_optional_bool: bool,
+}
+
+#[test]
+fn test() {
+    assert_eq!(Foo::inline(), "{ my_optional_bool?: boolean, }");
+}