Repository: Aleph-Alpha/ts-rs
Pull request #288 (2024-04-01 17:50:38)

add support for top level `as`
## Goal

This adds support for easily defining a struct or enum's type definition manually at the top level using the `as = ".."` attribute. This is for symmetry with #286.

## Changes

Added type_as to the container attributes, and defined an early-returned implementation for if this attribute is specified

## 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: Hope you guys had a nice easter!

I love it! Seems like a natural extension after supporting `#[ts(type = "..")]` on types. Thanks @dr-bonez for the PR, and thanks @escritorio-gustavo for taking a look!

Patch:
diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs
index d89f13d..1f94bd2 100644
--- a/macros/src/attr/enum.rs
+++ b/macros/src/attr/enum.rs
@@ -11,6 +11,7 @@ use crate::{
 #[derive(Default)]
 pub struct EnumAttr {
     crate_rename: Option<Path>,
+    pub type_as: Option<Type>,
     pub type_override: Option<String>,
     pub rename_all: Option<Inflection>,
     pub rename_all_fields: Option<Inflection>,
@@ -72,6 +73,7 @@ impl EnumAttr {
         &mut self,
         EnumAttr {
             crate_rename,
+            type_as,
             type_override,
             rename_all,
             rename_all_fields,
@@ -87,6 +89,7 @@ impl EnumAttr {
         }: EnumAttr,
     ) {
         self.crate_rename = self.crate_rename.take().or(crate_rename);
+        self.type_as = self.type_as.take().or(type_as);
         self.type_override = self.type_override.take().or(type_override);
         self.rename = self.rename.take().or(rename);
         self.rename_all = self.rename_all.take().or(rename_all);
@@ -113,6 +116,7 @@ impl EnumAttr {
 impl_parse! {
     EnumAttr(input, out) {
         "crate" => out.crate_rename = Some(parse_assign_from_str(input)?),
+        "as" => out.type_as = Some(parse_assign_from_str(input)?),
         "type" => out.type_override = Some(parse_assign_str(input)?),
         "rename" => out.rename = Some(parse_assign_str(input)?),
         "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?),
diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs
index 4464f4c..e094834 100644
--- a/macros/src/attr/struct.rs
+++ b/macros/src/attr/struct.rs
@@ -11,6 +11,7 @@ use crate::{
 #[derive(Default, Clone)]
 pub struct StructAttr {
     crate_rename: Option<Path>,
+    pub type_as: Option<Type>,
     pub type_override: Option<String>,
     pub rename_all: Option<Inflection>,
     pub rename: Option<String>,
@@ -59,6 +60,7 @@ impl StructAttr {
         &mut self,
         StructAttr {
             crate_rename,
+            type_as,
             type_override,
             rename_all,
             rename,
@@ -71,6 +73,7 @@ impl StructAttr {
         }: StructAttr,
     ) {
         self.crate_rename = self.crate_rename.take().or(crate_rename);
+        self.type_as = self.type_as.take().or(type_as);
         self.type_override = self.type_override.take().or(type_override);
         self.rename = self.rename.take().or(rename);
         self.rename_all = self.rename_all.take().or(rename_all);
@@ -94,6 +97,7 @@ impl StructAttr {
 impl_parse! {
     StructAttr(input, out) {
         "crate" => out.crate_rename = Some(parse_assign_from_str(input)?),
+        "as" => out.type_as = Some(parse_assign_from_str(input)?),
         "type" => out.type_override = Some(parse_assign_str(input)?),
         "rename" => out.rename = Some(parse_assign_str(input)?),
         "rename_all" => out.rename_all = Some(parse_assign_str(input).and_then(Inflection::try_from)?),
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index afaa8a1..655500d 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -5,7 +5,7 @@ use syn::{Fields, ItemEnum, Variant};
 use crate::{
     attr::{EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr},
     deps::Dependencies,
-    types::{self, type_override},
+    types::{self, type_as, type_override},
     DerivedTS,
 };
 
@@ -22,6 +22,9 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
     if let Some(attr_type_override) = &enum_attr.type_override {
         return type_override::type_override_enum(&enum_attr, &name, attr_type_override);
     }
+    if let Some(attr_type_as) = &enum_attr.type_as {
+        return type_as::type_as_enum(&enum_attr, &name, attr_type_as);
+    }
 
     if s.variants.is_empty() {
         return Ok(empty_enum(name, enum_attr));
diff --git a/macros/src/types/mod.rs b/macros/src/types/mod.rs
index c0ac8c3..4cb551d 100644
--- a/macros/src/types/mod.rs
+++ b/macros/src/types/mod.rs
@@ -6,6 +6,7 @@ mod r#enum;
 mod named;
 mod newtype;
 mod tuple;
+mod type_as;
 mod type_override;
 mod unit;
 
@@ -22,6 +23,9 @@ fn type_def(attr: &StructAttr, ident: &Ident, fields: &Fields) -> Result<Derived
     if let Some(attr_type_override) = &attr.type_override {
         return type_override::type_override_struct(attr, &name, attr_type_override);
     }
+    if let Some(attr_type_as) = &attr.type_as {
+        return type_as::type_as_struct(attr, &name, attr_type_as);
+    }
 
     match fields {
         Fields::Named(named) => match named.named.len() {
diff --git a/macros/src/types/type_as.rs b/macros/src/types/type_as.rs
new file mode 100644
index 0000000..e964af1
--- /dev/null
+++ b/macros/src/types/type_as.rs
@@ -0,0 +1,68 @@
+use quote::quote;
+use syn::{Result, Type};
+
+use crate::{
+    attr::{EnumAttr, StructAttr},
+    deps::Dependencies,
+    DerivedTS,
+};
+
+pub(crate) fn type_as_struct(attr: &StructAttr, name: &str, type_as: &Type) -> Result<DerivedTS> {
+    if attr.rename_all.is_some() {
+        syn_err!("`rename_all` is not compatible with `as`");
+    }
+    if attr.tag.is_some() {
+        syn_err!("`tag` is not compatible with `as`");
+    }
+
+    let crate_rename = attr.crate_rename();
+
+    Ok(DerivedTS {
+        crate_rename: crate_rename.clone(),
+        inline: quote!(#type_as::inline()),
+        inline_flattened: None,
+        docs: attr.docs.clone(),
+        dependencies: Dependencies::new(crate_rename),
+        export: attr.export,
+        export_to: attr.export_to.clone(),
+        ts_name: name.to_owned(),
+        concrete: attr.concrete.clone(),
+        bound: attr.bound.clone(),
+    })
+}
+
+pub(crate) fn type_as_enum(attr: &EnumAttr, name: &str, type_as: &Type) -> Result<DerivedTS> {
+    if attr.rename_all.is_some() {
+        syn_err!("`rename_all` is not compatible with `as`");
+    }
+    if attr.rename_all_fields.is_some() {
+        syn_err!("`rename_all_fields` is not compatible with `as`");
+    }
+    if attr.tag.is_some() {
+        syn_err!("`tag` is not compatible with `as`");
+    }
+    if attr.content.is_some() {
+        syn_err!("`content` is not compatible with `as`");
+    }
+    if attr.untagged {
+        syn_err!("`untagged` is not compatible with `as`");
+    }
+    if attr.type_override.is_some() {
+        syn_err!("`type` is not compatible with `as`");
+    }
+
+    let crate_rename = attr.crate_rename();
+
+    Ok(DerivedTS {
+        crate_rename: crate_rename.clone(),
+        inline: quote!(#type_as::inline()),
+        inline_flattened: None,
+        docs: attr.docs.clone(),
+        dependencies: Dependencies::new(crate_rename),
+        export: attr.export,
+        export_to: attr.export_to.clone(),
+        ts_name: name.to_owned(),
+        concrete: attr.concrete.clone(),
+        bound: attr.bound.clone(),
+    })
+}
diff --git a/macros/src/types/type_override.rs b/macros/src/types/type_override.rs
index 823619b..8ebc911 100644
--- a/macros/src/types/type_override.rs
+++ b/macros/src/types/type_override.rs
@@ -55,6 +55,9 @@ pub(crate) fn type_override_enum(
     if attr.untagged {
         syn_err!("`untagged` is not compatible with `type`");
     }
+    if attr.type_as.is_some() {
+        syn_err!("`type` is not compatible with `as`");
+    }
 
     let crate_rename = attr.crate_rename();
 
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index fad8eb5..1560ac3 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -204,6 +204,11 @@ pub mod typelist;
 ///   Note that you need to add the `export` attribute as well, in order to generate a test which exports the type.
 ///   <br/><br/>
 ///
+/// - **`#[ts(as = "..")]`**  
+///   Overrides the type used in Typescript, using the provided Rust type instead.
+///   This is useful when you have a custom serializer and deserializer and don't want to implement `TS` manually
+///   <br/><br/>
+///
 /// - **`#[ts(type = "..")]`**  
 ///   Overrides the type used in TypeScript.  
 ///   This is useful when you have a custom serializer and deserializer and don't want to implement `TS` manually


Test patch:
diff --git a/ts-rs/tests/top_level_type_as.rs b/ts-rs/tests/top_level_type_as.rs
new file mode 100644
index 0000000..940c855
--- /dev/null
+++ b/ts-rs/tests/top_level_type_as.rs
@@ -0,0 +1,22 @@
+use ts_rs::TS;
+
+#[derive(TS)]
+#[ts(as = "T")]
+pub enum UntaggedEnum<T: TS> {
+    Left(T),
+    Right(T),
+}
+
+#[test]
+pub fn top_level_type_as_enum() {
+    assert_eq!(UntaggedEnum::<String>::inline(), r#"string"#)
+}
+
+#[derive(TS)]
+#[ts(as = "T")]
+pub struct Wrapper<T: TS>(T);
+
+#[test]
+pub fn top_level_type_as_struct() {
+    assert_eq!(Wrapper::<String>::inline(), r#"string"#)
+}