Repository: Aleph-Alpha/ts-rs
Pull request #174 (2023-10-17 19:14:50)

add "as" derive helper attribute
This implements the feature requested in the following issue: https://github.com/Aleph-Alpha/ts-rs/issues/113

Please take a look at it and let me know if I need to make any other change to have it merged. 


Patch:
diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs
index d45e30f..3625224 100644
--- a/macros/src/attr/field.rs
+++ b/macros/src/attr/field.rs
@@ -7,6 +7,7 @@ use super::parse_assign_str;
 
 #[derive(Default)]
 pub struct FieldAttr {
+    pub type_as: Option<String>,
     pub type_override: Option<String>,
     pub rename: Option<String>,
     pub inline: bool,
@@ -43,6 +44,7 @@ impl FieldAttr {
     fn merge(
         &mut self,
         FieldAttr {
+            type_as,
             type_override,
             rename,
             inline,
@@ -52,6 +54,7 @@ impl FieldAttr {
         }: FieldAttr,
     ) {
         self.rename = self.rename.take().or(rename);
+        self.type_as = self.type_as.take().or(type_as);
         self.type_override = self.type_override.take().or(type_override);
         self.inline = self.inline || inline;
         self.skip = self.skip || skip;
@@ -65,6 +68,7 @@ impl FieldAttr {
 
 impl_parse! {
     FieldAttr(input, out) {
+        "as" => out.type_as = Some(parse_assign_str(input)?),
         "type" => out.type_override = Some(parse_assign_str(input)?),
         "rename" => out.rename = Some(parse_assign_str(input)?),
         "inline" => out.inline = true,
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index 5c0e056..8978fa5 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -1,6 +1,6 @@
 use proc_macro2::TokenStream;
 use quote::{format_ident, quote};
-use syn::{Fields, Generics, ItemEnum, Variant};
+use syn::{Fields, Generics, ItemEnum, Type, Variant};
 
 use crate::{
     attr::{EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr},
@@ -106,18 +106,22 @@ fn format_variant(
         Tagged::Adjacently { tag, content } => match &variant.fields {
             Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
                 let FieldAttr {
+                    type_as,
                     type_override,
                     skip,
                     ..
                 } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?;
+
+                let ty = match (type_override, type_as) {
+                    (Some(_), Some(_)) => syn_err!("`type` is not compatible with `as`"),
+                    (Some(type_override), None) => quote! { #type_override },
+                    (None, Some(type_as)) => format_type(&syn::parse_str::<Type>(&type_as)?, dependencies, generics), 
+                    (None, None) => format_type(&unnamed.unnamed[0].ty, dependencies, generics),
+                };
+
                 if skip {
                     quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
                 } else {
-                    let ty = if let Some(type_override) = type_override {
-                        quote! { #type_override }
-                    } else {
-                        format_type(&unnamed.unnamed[0].ty, dependencies, generics)
-                    };
                     quote!(format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #ty))
                 }
             }
@@ -146,18 +150,22 @@ fn format_variant(
             None => match &variant.fields {
                 Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
                     let FieldAttr {
-                        type_override,
+                        type_as,
                         skip,
+                        type_override,
                         ..
                     } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?;
+
+                    let ty = match (type_override, type_as) {
+                        (Some(_), Some(_)) => syn_err!("`type` is not compatible with `as`"),
+                        (Some(type_override), None) => quote! { #type_override },
+                        (None, Some(type_as)) => format_type(&syn::parse_str::<Type>(&type_as)?, dependencies, generics), 
+                        (None, None) => format_type(&unnamed.unnamed[0].ty, dependencies, generics),
+                    };
+
                     if skip {
                         quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
                     } else {
-                        let ty = if let Some(type_override) = type_override {
-                            quote! { #type_override }
-                        } else {
-                            format_type(&unnamed.unnamed[0].ty, dependencies, generics)
-                        };
                         quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #name, #ty))
                     }
                 }
diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs
index e3141d0..d61267b 100644
--- a/macros/src/types/named.rs
+++ b/macros/src/types/named.rs
@@ -2,6 +2,7 @@ use proc_macro2::TokenStream;
 use quote::quote;
 use syn::{Field, FieldsNamed, GenericArgument, Generics, PathArguments, Result, Type};
 
+use crate::attr::Optional;
 use crate::{
     attr::{FieldAttr, Inflection, StructAttr},
     deps::Dependencies,
@@ -9,7 +10,6 @@ use crate::{
     utils::{raw_name_to_ts_field, to_ts_ident},
     DerivedTS,
 };
-use crate::attr::Optional;
 
 pub(crate) fn named(
     attr: &StructAttr,
@@ -80,6 +80,7 @@ fn format_field(
     generics: &Generics,
 ) -> Result<()> {
     let FieldAttr {
+        type_as,
         type_override,
         rename,
         inline,
@@ -92,22 +93,38 @@ fn format_field(
         return Ok(());
     }
 
+    if type_as.is_some() && type_override.is_some() {
+        syn_err!("`type` is not compatible with `as`")
+    }
+
+    let parsed_ty = if let Some(ref type_as) = type_as {
+        syn::parse_str::<Type>(type_as)?
+    } else {
+        field.ty.clone()
+    };
+
     let (ty, optional_annotation) = match optional {
-        Optional { optional: true, nullable } => {
-            let inner_type = extract_option_argument(&field.ty)?; // inner type of the optional
+        Optional {
+            optional: true,
+            nullable,
+        } => {
+            let inner_type = extract_option_argument(&parsed_ty)?; // inner type of the optional
             match nullable {
-                true => (&field.ty, "?"),  // if it's nullable, we keep the original type
+                true => (&parsed_ty, "?"),  // if it's nullable, we keep the original type
                 false => (inner_type, "?"), // if not, we use the Option's inner type
             }
-        },
-        Optional { optional: false, .. } => (&field.ty, "")
+        }
+        Optional {
+            optional: false, ..
+        } => (&parsed_ty, ""),
     };
 
     if flatten {
-        match (&type_override, &rename, inline) {
-            (Some(_), _, _) => syn_err!("`type` is not compatible with `flatten`"),
-            (_, Some(_), _) => syn_err!("`rename` is not compatible with `flatten`"),
-            (_, _, true) => syn_err!("`inline` is not compatible with `flatten`"),
+        match (&type_as, &type_override, &rename, inline) {
+            (Some(_), _, _, _) => syn_err!("`as` is not compatible with `flatten`"),
+            (_, Some(_), _, _) => syn_err!("`type` is not compatible with `flatten`"),
+            (_, _, Some(_), _) => syn_err!("`rename` is not compatible with `flatten`"),
+            (_, _, _, true) => syn_err!("`inline` is not compatible with `flatten`"),
             _ => {}
         }
 
diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs
index 2473071..f1038b6 100644
--- a/macros/src/types/newtype.rs
+++ b/macros/src/types/newtype.rs
@@ -1,5 +1,5 @@
 use quote::quote;
-use syn::{FieldsUnnamed, Generics, Result};
+use syn::{FieldsUnnamed, Generics, Result, Type};
 
 use crate::{
     attr::{FieldAttr, StructAttr},
@@ -22,6 +22,7 @@ pub(crate) fn newtype(
     }
     let inner = fields.unnamed.first().unwrap();
     let FieldAttr {
+        type_as,
         type_override,
         rename: rename_inner,
         inline,
@@ -38,17 +39,28 @@ pub(crate) fn newtype(
         _ => {}
     };
 
-    let inner_ty = &inner.ty;
+    if type_as.is_some() && type_override.is_some() {
+        syn_err!("`type` is not compatible with `as`")
+    }
+
+    let inner_ty = if let Some(ref type_as) = type_as {
+        syn::parse_str::<Type>(type_as)?
+    } else {
+        inner.ty.clone()
+    };
+
     let mut dependencies = Dependencies::default();
-    match (inline, &type_override) {
-        (_, Some(_)) => (),
-        (true, _) => dependencies.append_from(inner_ty),
-        (false, _) => dependencies.push_or_append_from(inner_ty),
+
+    match (type_override.is_none(), inline) {
+        (false, _) => (),
+        (true, true) => dependencies.append_from(&inner_ty),
+        (true, false) => dependencies.push_or_append_from(&inner_ty),
     };
-    let inline_def = match &type_override {
-        Some(o) => quote!(#o.to_owned()),
+
+    let inline_def = match type_override {
+        Some(ref o) => quote!(#o.to_owned()),
         None if inline => quote!(<#inner_ty as ts_rs::TS>::inline()),
-        None => format_type(inner_ty, &mut dependencies, generics),
+        None => format_type(&inner_ty, &mut dependencies, generics),
     };
 
     let generic_args = format_generics(&mut dependencies, generics);
diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs
index 71ae7f0..bc98005 100644
--- a/macros/src/types/tuple.rs
+++ b/macros/src/types/tuple.rs
@@ -1,6 +1,6 @@
 use proc_macro2::TokenStream;
 use quote::quote;
-use syn::{Field, FieldsUnnamed, Generics, Result};
+use syn::{Field, FieldsUnnamed, Generics, Result, Type};
 
 use crate::{
     attr::{FieldAttr, StructAttr},
@@ -58,8 +58,8 @@ fn format_field(
     field: &Field,
     generics: &Generics,
 ) -> Result<()> {
-    let ty = &field.ty;
     let FieldAttr {
+        type_as,
         type_override,
         rename,
         inline,
@@ -71,29 +71,42 @@ fn format_field(
     if skip {
         return Ok(());
     }
+
+    let ty = if let Some(ref type_as) = type_as {
+        syn::parse_str::<Type>(type_as)?
+    } else {
+        field.ty.clone()
+    };
+
+    if type_as.is_some() && type_override.is_some() {
+        syn_err!("`type` is not compatible with `as`")
+    }
+
     if rename.is_some() {
         syn_err!("`rename` is not applicable to tuple structs")
     }
+
     if optional.optional {
         syn_err!("`optional` is not applicable to tuple fields")
     }
+
     if flatten {
         syn_err!("`flatten` is not applicable to tuple fields")
     }
 
-    formatted_fields.push(match &type_override {
-        Some(o) => quote!(#o.to_owned()),
+    formatted_fields.push(match type_override {
+        Some(ref o) => quote!(#o.to_owned()),
         None if inline => quote!(<#ty as ts_rs::TS>::inline()),
-        None => format_type(ty, dependencies, generics),
+        None => format_type(&ty, dependencies, generics),
     });
 
-    match (inline, &type_override) {
+    match (inline, type_override) {
         (_, Some(_)) => (),
         (false, _) => {
-            dependencies.push_or_append_from(ty);
+            dependencies.push_or_append_from(&ty);
         }
         (true, _) => {
-            dependencies.append_from(ty);
+            dependencies.append_from(&ty);
         }
     };
 


Test patch:
diff --git a/ts-rs/tests/type_as.rs b/ts-rs/tests/type_as.rs
new file mode 100644
index 0000000..f727bc7
--- /dev/null
+++ b/ts-rs/tests/type_as.rs
@@ -0,0 +1,78 @@
+#![allow(dead_code)]
+
+use std::cell::UnsafeCell;
+use std::mem::MaybeUninit;
+use std::ptr::NonNull;
+use std::sync::atomic::AtomicPtr;
+use std::time::Instant;
+
+use ts_rs::TS;
+
+type Unsupported = UnsafeCell<MaybeUninit<NonNull<AtomicPtr<i32>>>>;
+
+#[derive(TS)]
+struct ExternalTypeDef {
+    a: i32,
+    b: i32,
+    c: i32,
+}
+
+#[test]
+fn struct_properties() {
+    #[derive(TS)]
+    struct Override {
+        a: i32,
+        #[ts(as = "ExternalTypeDef")]
+        #[ts(inline)]
+        x: Instant,
+        // here, 'as' just behaves like 'type' (though it adds a dependency!)
+        #[ts(as = "ExternalTypeDef")]
+        y: Unsupported,
+    }
+
+    assert_eq!(
+        Override::inline(),
+        "{ a: number, x: { a: number, b: number, c: number, }, y: ExternalTypeDef, }"
+    );
+    assert!(Override::dependencies()
+        .iter()
+        .any(|d| d.ts_name == "ExternalTypeDef"));
+}
+
+#[test]
+fn enum_variants() {
+    #[derive(TS)]
+    enum OverrideEnum {
+        A(#[ts(as = "ExternalTypeDef")] Instant),
+        B {
+            #[ts(as = "ExternalTypeDef")]
+            x: Unsupported,
+            y: i32,
+            z: i32,
+        },
+    }
+
+    assert_eq!(
+        OverrideEnum::inline(),
+        r#"{ "A": ExternalTypeDef } | { "B": { x: ExternalTypeDef, y: number, z: number, } }"#
+    )
+}
+
+#[test]
+fn complex() {
+    #[derive(TS)]
+    struct Outer {
+        #[ts(as = "Option<ExternalTypeDef>")]
+        #[ts(optional = nullable, inline)]
+        x: Unsupported,
+        #[ts(as = "Option<ExternalTypeDef>")]
+        #[ts(optional = nullable)]
+        y: Unsupported,
+    }
+
+    let external = ExternalTypeDef::inline();
+    assert_eq!(
+        Outer::inline(),
+        format!(r#"{{ x?: {external} | null, y?: ExternalTypeDef | null, }}"#)
+    )
+}