Repository: Aleph-Alpha/ts-rs
Pull request #290 (2024-04-02 18:53:18)

Rework attribute parsing
## Goal

This PR aims to change the way we parse attributes in a few key ways:
- Introducing a couple of traits for the methods all attribute types share
- Change the `merge` method to remove the mutable reference
  - The reasoning for this is mostly that I think a `for_each` that mutates external data is somewhat confusing, so I changed it to use `fold` (think JS `Array.protoype.reduce`) instead.
- Bring all the attribute compatibility validation to the same file where parsing is done

## 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.
- [ ] I have added or updated the tests related to the changes made.
  - This is a refactor, so there isn't really new anything I can test... We could make tests for whether or not our compiler errors are correct, but we still have to figure out how to do that


NyxCode: I'll take a closer look once I get home, but I love what I've seen so far. Great work!

Patch:
diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs
index 1f94bd2..5532a69 100644
--- a/macros/src/attr/enum.rs
+++ b/macros/src/attr/enum.rs
@@ -1,8 +1,8 @@
 use std::collections::HashMap;
 
-use syn::{parse_quote, Attribute, Ident, Path, Result, Type, WherePredicate};
+use syn::{parse_quote, Attribute, Ident, ItemEnum, Path, Result, Type, WherePredicate};
 
-use super::{parse_assign_from_str, parse_bound};
+use super::{parse_assign_from_str, parse_bound, Attr, ContainerAttr, Serde};
 use crate::{
     attr::{parse_assign_inflection, parse_assign_str, parse_concrete, Inflection},
     utils::{parse_attrs, parse_docs},
@@ -26,10 +26,6 @@ pub struct EnumAttr {
     pub content: Option<String>,
 }
 
-#[cfg(feature = "serde-compat")]
-#[derive(Default)]
-pub struct SerdeEnumAttr(EnumAttr);
-
 #[derive(Copy, Clone)]
 pub enum Tagged<'a> {
     Externally,
@@ -52,14 +48,17 @@ impl EnumAttr {
     }
 
     pub fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
-        let mut result = Self::default();
-        parse_attrs(attrs)?.for_each(|a| result.merge(a));
+        let mut result = parse_attrs::<Self>(attrs)?;
+
+        #[cfg(feature = "serde-compat")]
+        {
+            let serde_attr = crate::utils::parse_serde_attrs::<EnumAttr>(attrs);
+            result = result.merge(serde_attr.0);
+        }
 
         let docs = parse_docs(attrs)?;
         result.docs = docs;
 
-        #[cfg(feature = "serde-compat")]
-        crate::utils::parse_serde_attrs::<SerdeEnumAttr>(attrs).for_each(|a| result.merge(a.0));
         Ok(result)
     }
 
@@ -68,48 +67,141 @@ impl EnumAttr {
             .clone()
             .unwrap_or_else(|| parse_quote!(::ts_rs))
     }
+}
+
+impl Attr for EnumAttr {
+    type Item = ItemEnum;
+
+    fn merge(self, other: Self) -> Self {
+        Self {
+            crate_rename: self.crate_rename.or(other.crate_rename),
+            type_as: self.type_as.or(other.type_as),
+            type_override: self.type_override.or(other.type_override),
+            rename: self.rename.or(other.rename),
+            rename_all: self.rename_all.or(other.rename_all),
+            rename_all_fields: self.rename_all_fields.or(other.rename_all_fields),
+            tag: self.tag.or(other.tag),
+            untagged: self.untagged || other.untagged,
+            content: self.content.or(other.content),
+            export: self.export || other.export,
+            export_to: self.export_to.or(other.export_to),
+            docs: other.docs,
+            concrete: self.concrete.into_iter().chain(other.concrete).collect(),
+            bound: match (self.bound, other.bound) {
+                (Some(a), Some(b)) => Some(a.into_iter().chain(b).collect()),
+                (Some(bound), None) | (None, Some(bound)) => Some(bound),
+                (None, None) => None,
+            },
+        }
+    }
+
+    fn assert_validity(&self, item: &Self::Item) -> Result<()> {
+        if self.type_override.is_some() {
+            if self.type_as.is_some() {
+                syn_err_spanned!(
+                    item;
+                    "`as` is not compatible with `type`"
+                );
+            }
+
+            if self.rename_all.is_some() {
+                syn_err_spanned!(
+                    item;
+                    "`rename_all` is not compatible with `type`"
+                );
+            }
+
+            if self.rename_all_fields.is_some() {
+                syn_err_spanned!(
+                    item;
+                    "`rename_all_fields` is not compatible with `type`"
+                );
+            }
+
+            if self.tag.is_some() {
+                syn_err_spanned!(
+                    item;
+                    "`tag` is not compatible with `type`"
+                );
+            }
+
+            if self.content.is_some() {
+                syn_err_spanned!(
+                    item;
+                    "`content` is not compatible with `type`"
+                );
+            }
 
-    fn merge(
-        &mut self,
-        EnumAttr {
-            crate_rename,
-            type_as,
-            type_override,
-            rename_all,
-            rename_all_fields,
-            rename,
-            tag,
-            content,
-            untagged,
-            export_to,
-            export,
-            docs,
-            concrete,
-            bound,
-        }: 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);
-        self.rename_all_fields = self.rename_all_fields.take().or(rename_all_fields);
-        self.tag = self.tag.take().or(tag);
-        self.untagged = self.untagged || untagged;
-        self.content = self.content.take().or(content);
-        self.export = self.export || export;
-        self.export_to = self.export_to.take().or(export_to);
-        self.docs = docs;
-        self.concrete.extend(concrete);
-        self.bound = self
-            .bound
-            .take()
-            .map(|b| {
-                b.into_iter()
-                    .chain(bound.clone().unwrap_or_default())
-                    .collect()
-            })
-            .or(bound);
+            if self.untagged {
+                syn_err_spanned!(
+                    item;
+                    "`untagged` is not compatible with `type`"
+                );
+            }
+        }
+
+        if self.type_as.is_some() {
+            if self.rename_all.is_some() {
+                syn_err_spanned!(
+                    item;
+                    "`rename_all` is not compatible with `as`"
+                );
+            }
+
+            if self.rename_all_fields.is_some() {
+                syn_err_spanned!(
+                    item;
+                    "`rename_all_fields` is not compatible with `as`"
+                );
+            }
+
+            if self.tag.is_some() {
+                syn_err_spanned!(
+                    item;
+                    "`tag` is not compatible with `as`"
+                );
+            }
+
+            if self.content.is_some() {
+                syn_err_spanned!(
+                    item;
+                    "`content` is not compatible with `as`"
+                );
+            }
+
+            if self.untagged {
+                syn_err_spanned!(
+                    item;
+                    "`untagged` is not compatible with `as`"
+                );
+            }
+        }
+
+        match (self.untagged, &self.tag, &self.content) {
+            (true, Some(_), None) => syn_err_spanned!(
+                item;
+                "untagged cannot be used with tag"
+            ),
+            (true, _, Some(_)) => syn_err_spanned!(
+                item;
+                "untagged cannot be used with content"
+            ),
+            (false, None, Some(_)) => syn_err_spanned!(
+                item;
+                "content cannot be used without tag"
+            ),
+            _ => (),
+        };
+
+        Ok(())
+    }
+}
+
+impl ContainerAttr for EnumAttr {
+    fn crate_rename(&self) -> Path {
+        self.crate_rename
+            .clone()
+            .unwrap_or_else(|| parse_quote!(::ts_rs))
     }
 }
 
@@ -133,7 +225,7 @@ impl_parse! {
 
 #[cfg(feature = "serde-compat")]
 impl_parse! {
-    SerdeEnumAttr(input, out) {
+    Serde<EnumAttr>(input, out) {
         "rename" => out.0.rename = Some(parse_assign_str(input)?),
         "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?),
         "rename_all_fields" => out.0.rename_all_fields = Some(parse_assign_inflection(input)?),
diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs
index 7345eb4..a1815f0 100644
--- a/macros/src/attr/field.rs
+++ b/macros/src/attr/field.rs
@@ -1,6 +1,6 @@
-use syn::{Attribute, Ident, Result, Type};
+use syn::{Attribute, Field, Ident, Result, Type};
 
-use super::{parse_assign_from_str, parse_assign_str};
+use super::{parse_assign_from_str, parse_assign_str, Attr, Serde};
 use crate::utils::{parse_attrs, parse_docs};
 
 #[derive(Default)]
@@ -24,47 +24,121 @@ pub struct Optional {
     pub nullable: bool,
 }
 
-#[cfg(feature = "serde-compat")]
-#[derive(Default)]
-pub struct SerdeFieldAttr(FieldAttr);
-
 impl FieldAttr {
     pub fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
-        let mut result = Self::default();
-        parse_attrs(attrs)?.for_each(|a| result.merge(a));
-        result.docs = parse_docs(attrs)?;
+        let mut result = parse_attrs::<Self>(attrs)?;
+
         #[cfg(feature = "serde-compat")]
         if !result.skip {
-            crate::utils::parse_serde_attrs::<SerdeFieldAttr>(attrs)
-                .for_each(|a| result.merge(a.0));
+            let serde_attr = crate::utils::parse_serde_attrs::<FieldAttr>(attrs);
+            result = result.merge(serde_attr.0);
         }
+
+        result.docs = parse_docs(attrs)?;
+
         Ok(result)
     }
+}
+
+impl Attr for FieldAttr {
+    type Item = Field;
 
-    fn merge(
-        &mut self,
-        FieldAttr {
-            type_as,
-            type_override,
-            rename,
-            inline,
-            skip,
-            optional: Optional { optional, nullable },
-            flatten,
-            docs,
-        }: 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;
-        self.optional = Optional {
-            optional: self.optional.optional || optional,
-            nullable: self.optional.nullable || nullable,
-        };
-        self.flatten |= flatten;
-        self.docs.push_str(&docs);
+    fn merge(self, other: Self) -> Self {
+        Self {
+            type_as: self.type_as.or(other.type_as),
+            type_override: self.type_override.or(other.type_override),
+            rename: self.rename.or(other.rename),
+            inline: self.inline || other.inline,
+            skip: self.skip || other.skip,
+            optional: Optional {
+                optional: self.optional.optional || other.optional.optional,
+                nullable: self.optional.nullable || other.optional.nullable,
+            },
+            flatten: self.flatten || other.flatten,
+
+            // We can't emit TSDoc for a flattened field
+            // and we cant make this invalid in assert_validity because
+            // this documentation is totally valid in Rust
+            docs: if self.flatten || other.flatten {
+                String::new()
+            } else {
+                self.docs + &other.docs
+            },
+        }
+    }
+
+    fn assert_validity(&self, field: &Self::Item) -> Result<()> {
+        if self.type_override.is_some() {
+            if self.type_as.is_some() {
+                syn_err_spanned!(field; "`type` is not compatible with `as`")
+            }
+
+            if self.inline {
+                syn_err_spanned!(field; "`type` is not compatible with `inline`")
+            }
+
+            if self.flatten {
+                syn_err_spanned!(
+                    field;
+                    "`type` is not compatible with `flatten`"
+                );
+            }
+        }
+
+        if self.flatten {
+            if self.type_as.is_some() {
+                syn_err_spanned!(
+                    field;
+                    "`as` is not compatible with `flatten`"
+                );
+            }
+
+            if self.rename.is_some() {
+                syn_err_spanned!(
+                    field;
+                    "`rename` is not compatible with `flatten`"
+                );
+            }
+
+            if self.inline {
+                syn_err_spanned!(
+                    field;
+                    "`inline` is not compatible with `flatten`"
+                );
+            }
+
+            if self.optional.optional {
+                syn_err_spanned!(
+                    field;
+                    "`optional` is not compatible with `flatten`"
+                );
+            }
+        }
+
+        if field.ident.is_none() {
+            if self.flatten {
+                syn_err_spanned!(
+                    field;
+                    "`flatten` cannot with tuple struct fields"
+                );
+            }
+
+            if self.rename.is_some() {
+                syn_err_spanned!(
+                    field;
+                    "`flatten` cannot with tuple struct fields"
+                );
+            }
+
+            if self.optional.optional {
+                syn_err_spanned!(
+                    field;
+                    "`optional` cannot with tuple struct fields"
+                );
+            }
+        }
+
+        Ok(())
     }
 }
 
@@ -98,7 +172,7 @@ impl_parse! {
 
 #[cfg(feature = "serde-compat")]
 impl_parse! {
-    SerdeFieldAttr(input, out) {
+    Serde<FieldAttr>(input, out) {
         "rename" => out.0.rename = Some(parse_assign_str(input)?),
         "skip" => out.0.skip = true,
         "flatten" => out.0.flatten = true,
@@ -106,7 +180,6 @@ impl_parse! {
         "default" => {
             use syn::Token;
             if input.peek(Token![=]) {
-                input.parse::<Token![=]>()?;
                 parse_assign_str(input)?;
             }
         },
diff --git a/macros/src/attr/mod.rs b/macros/src/attr/mod.rs
index b80ea7b..2e0931f 100644
--- a/macros/src/attr/mod.rs
+++ b/macros/src/attr/mod.rs
@@ -6,7 +6,7 @@ pub use r#struct::*;
 use syn::{
     parse::{Parse, ParseStream},
     punctuated::Punctuated,
-    Error, Lit, Result, Token, WherePredicate,
+    Error, Lit, Path, Result, Token, WherePredicate,
 };
 pub use variant::*;
 
@@ -26,6 +26,33 @@ pub enum Inflection {
     Kebab,
 }
 
+pub(super) trait Attr: Default {
+    type Item;
+
+    fn merge(self, other: Self) -> Self;
+    fn assert_validity(&self, item: &Self::Item) -> Result<()>;
+}
+
+pub(super) trait ContainerAttr: Attr {
+    fn crate_rename(&self) -> Path;
+}
+
+#[cfg(feature = "serde-compat")]
+#[derive(Default)]
+pub(super) struct Serde<T>(pub T)
+where
+    T: Attr;
+
+#[cfg(feature = "serde-compat")]
+impl<T> Serde<T>
+where
+    T: Attr,
+{
+    pub fn merge(self, other: Self) -> Self {
+        Self(self.0.merge(other.0))
+    }
+}
+
 impl Inflection {
     pub fn apply(self, string: &str) -> String {
         use inflector::Inflector;
diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs
index e094834..bb8d186 100644
--- a/macros/src/attr/struct.rs
+++ b/macros/src/attr/struct.rs
@@ -1,8 +1,8 @@
 use std::collections::HashMap;
 
-use syn::{parse_quote, Attribute, Ident, Path, Result, Type, WherePredicate};
+use syn::{parse_quote, Attribute, Fields, Ident, Path, Result, Type, WherePredicate};
 
-use super::{parse_assign_from_str, parse_bound, parse_concrete};
+use super::{parse_assign_from_str, parse_bound, parse_concrete, Attr, ContainerAttr, Serde};
 use crate::{
     attr::{parse_assign_str, EnumAttr, Inflection, VariantAttr},
     utils::{parse_attrs, parse_docs},
@@ -23,75 +23,108 @@ pub struct StructAttr {
     pub bound: Option<Vec<WherePredicate>>,
 }
 
-#[cfg(feature = "serde-compat")]
-#[derive(Default)]
-pub struct SerdeStructAttr(StructAttr);
-
 impl StructAttr {
     pub fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
-        let mut result = Self::default();
-        parse_attrs(attrs)?.for_each(|a| result.merge(a));
+        let mut result = parse_attrs::<Self>(attrs)?;
+
+        #[cfg(feature = "serde-compat")]
+        {
+            let serde_attr = crate::utils::parse_serde_attrs::<StructAttr>(attrs);
+            result = result.merge(serde_attr.0);
+        }
 
         let docs = parse_docs(attrs)?;
         result.docs = docs;
-
-        #[cfg(feature = "serde-compat")]
-        crate::utils::parse_serde_attrs::<SerdeStructAttr>(attrs).for_each(|a| result.merge(a.0));
+        
         Ok(result)
     }
 
-    pub fn from_variant(enum_attr: &EnumAttr, variant_attr: &VariantAttr) -> Self {
+    pub fn from_variant(
+        enum_attr: &EnumAttr,
+        variant_attr: &VariantAttr,
+        variant_fields: &Fields,
+    ) -> Self {
         Self {
             crate_rename: Some(enum_attr.crate_rename()),
             rename: variant_attr.rename.clone(),
-            rename_all: variant_attr.rename_all,
+            rename_all: variant_attr.rename_all.or(match variant_fields {
+                Fields::Named(_) => enum_attr.rename_all_fields,
+                Fields::Unnamed(_) | Fields::Unit => None,
+            }),
             // inline and skip are not supported on StructAttr
             ..Self::default()
         }
     }
+}
+
+impl Attr for StructAttr {
+    type Item = Fields;
+
+    fn merge(self, other: Self) -> Self {
+        Self {
+            crate_rename: self.crate_rename.or(other.crate_rename),
+            type_as: self.type_as.or(other.type_as),
+            type_override: self.type_override.or(other.type_override),
+            rename: self.rename.or(other.rename),
+            rename_all: self.rename_all.or(other.rename_all),
+            export_to: self.export_to.or(other.export_to),
+            export: self.export || other.export,
+            tag: self.tag.or(other.tag),
+            docs: other.docs,
+            concrete: self.concrete.into_iter().chain(other.concrete).collect(),
+            bound: match (self.bound, other.bound) {
+                (Some(a), Some(b)) => Some(a.into_iter().chain(b).collect()),
+                (Some(bound), None) | (None, Some(bound)) => Some(bound),
+                (None, None) => None,
+            },
+        }
+    }
 
-    pub fn crate_rename(&self) -> Path {
+    fn assert_validity(&self, item: &Self::Item) -> Result<()> {
+        if self.type_override.is_some() {
+            if self.type_as.is_some() {
+                syn_err!("`as` is not compatible with `type`");
+            }
+
+            if self.rename_all.is_some() {
+                syn_err!("`rename_all` is not compatible with `type`");
+            }
+
+            if self.tag.is_some() {
+                syn_err!("`tag` is not compatible with `type`");
+            }
+        }
+
+        if self.type_as.is_some() {
+            if self.tag.is_some() {
+                syn_err!("`tag` is not compatible with `as`");
+            }
+
+            if self.rename_all.is_some() {
+                syn_err!("`rename_all` is not compatible with `as`");
+            }
+        }
+
+        if !matches!(item, Fields::Named(_)) {
+            if self.tag.is_some() {
+                syn_err!("`tag` cannot be used with unit or tuple structs");
+            }
+
+            if self.rename_all.is_some() {
+                syn_err!("`rename_all` cannot be used with unit or tuple structs");
+            }
+        }
+
+        Ok(())
+    }
+}
+
+impl ContainerAttr for StructAttr {
+    fn crate_rename(&self) -> Path {
         self.crate_rename
             .clone()
             .unwrap_or_else(|| parse_quote!(::ts_rs))
     }
-
-    fn merge(
-        &mut self,
-        StructAttr {
-            crate_rename,
-            type_as,
-            type_override,
-            rename_all,
-            rename,
-            export,
-            export_to,
-            tag,
-            docs,
-            concrete,
-            bound,
-        }: 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);
-        self.export_to = self.export_to.take().or(export_to);
-        self.export = self.export || export;
-        self.tag = self.tag.take().or(tag);
-        self.docs = docs;
-        self.concrete.extend(concrete);
-        self.bound = self
-            .bound
-            .take()
-            .map(|b| {
-                b.into_iter()
-                    .chain(bound.clone().unwrap_or_default())
-                    .collect()
-            })
-            .or(bound);
-    }
 }
 
 impl_parse! {
@@ -111,7 +144,7 @@ impl_parse! {
 
 #[cfg(feature = "serde-compat")]
 impl_parse! {
-    SerdeStructAttr(input, out) {
+    Serde<StructAttr>(input, out) {
         "rename" => out.0.rename = Some(parse_assign_str(input)?),
         "rename_all" => out.0.rename_all = Some(parse_assign_str(input).and_then(Inflection::try_from)?),
         "tag" => out.0.tag = Some(parse_assign_str(input)?),
diff --git a/macros/src/attr/variant.rs b/macros/src/attr/variant.rs
index cca9532..52daeda 100644
--- a/macros/src/attr/variant.rs
+++ b/macros/src/attr/variant.rs
@@ -1,6 +1,6 @@
-use syn::{Attribute, Ident, Result};
+use syn::{Attribute, Fields, Ident, Result, Variant};
 
-use super::EnumAttr;
+use super::{Attr, Serde};
 use crate::{
     attr::{parse_assign_inflection, parse_assign_str, Inflection},
     utils::parse_attrs,
@@ -15,43 +15,40 @@ pub struct VariantAttr {
     pub untagged: bool,
 }
 
-#[cfg(feature = "serde-compat")]
-#[derive(Default)]
-pub struct SerdeVariantAttr(VariantAttr);
-
 impl VariantAttr {
-    pub fn new(attrs: &[Attribute], enum_attr: &EnumAttr) -> Result<Self> {
-        let mut result = Self::from_attrs(attrs)?;
-        result.rename_all = result.rename_all.or(enum_attr.rename_all_fields);
-        Ok(result)
-    }
-
     pub fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
-        let mut result = Self::default();
-        parse_attrs(attrs)?.for_each(|a| result.merge(a));
+        let mut result = parse_attrs::<Self>(attrs)?;
         #[cfg(feature = "serde-compat")]
         if !result.skip {
-            crate::utils::parse_serde_attrs::<SerdeVariantAttr>(attrs)
-                .for_each(|a| result.merge(a.0));
+            let serde_attr = crate::utils::parse_serde_attrs::<VariantAttr>(attrs);
+            result = result.merge(serde_attr.0);
         }
         Ok(result)
     }
+}
+
+impl Attr for VariantAttr {
+    type Item = Variant;
+
+    fn merge(self, other: Self) -> Self {
+        Self {
+            rename: self.rename.or(other.rename),
+            rename_all: self.rename_all.or(other.rename_all),
+            inline: self.inline || other.inline,
+            skip: self.skip || other.skip,
+            untagged: self.untagged || other.untagged,
+        }
+    }
+
+    fn assert_validity(&self, item: &Self::Item) -> Result<()> {
+        if !matches!(item.fields, Fields::Named(_)) && self.rename_all.is_some() {
+            syn_err_spanned!(
+                item;
+                "`rename_all` is not applicable to unit or tuple variants"
+            )
+        }
 
-    fn merge(
-        &mut self,
-        VariantAttr {
-            rename,
-            rename_all,
-            inline,
-            skip,
-            untagged,
-        }: VariantAttr,
-    ) {
-        self.rename = self.rename.take().or(rename);
-        self.rename_all = self.rename_all.take().or(rename_all);
-        self.inline = self.inline || inline;
-        self.skip = self.skip || skip;
-        self.untagged = self.untagged || untagged;
+        Ok(())
     }
 }
 
@@ -67,7 +64,7 @@ impl_parse! {
 
 #[cfg(feature = "serde-compat")]
 impl_parse! {
-    SerdeVariantAttr(input, out) {
+    Serde<VariantAttr>(input, out) {
         "rename" => out.0.rename = Some(parse_assign_str(input)?),
         "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?),
         "skip" => out.0.skip = true,
diff --git a/macros/src/deps.rs b/macros/src/deps.rs
index b99c49f..6730539 100644
--- a/macros/src/deps.rs
+++ b/macros/src/deps.rs
@@ -35,7 +35,9 @@ impl Dependencies {
     }
 
     pub fn append(&mut self, mut other: Dependencies) {
-        self.dependencies.push(quote![.extend(#other)]);
+        if !other.dependencies.is_empty() {
+            self.dependencies.push(quote![.extend(#other)]);
+        }
         self.types.append(&mut other.types);
     }
 }
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index 655500d..6dcc0f4 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -3,7 +3,7 @@ use quote::{format_ident, quote};
 use syn::{Fields, ItemEnum, Variant};
 
 use crate::{
-    attr::{EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr},
+    attr::{Attr, EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr},
     deps::Dependencies,
     types::{self, type_as, type_override},
     DerivedTS,
@@ -12,6 +12,8 @@ use crate::{
 pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
     let enum_attr: EnumAttr = EnumAttr::from_attrs(&s.attrs)?;
 
+    enum_attr.assert_validity(s)?;
+
     let crate_rename = enum_attr.crate_rename();
 
     let name = match &enum_attr.rename {
@@ -83,10 +85,9 @@ fn format_variant(
     // If `variant.fields` is not a `Fields::Named(_)` the `rename_all_fields`
     // attribute must be ignored to prevent a `rename_all` from getting to
     // the newtype, tuple or unit formatting, which would cause an error
-    let variant_attr = match variant.fields {
-        Fields::Unit | Fields::Unnamed(_) => VariantAttr::from_attrs(&variant.attrs)?,
-        Fields::Named(_) => VariantAttr::new(&variant.attrs, enum_attr)?,
-    };
+    let variant_attr = VariantAttr::from_attrs(&variant.attrs)?;
+
+    variant_attr.assert_validity(variant)?;
 
     if variant_attr.skip {
         return Ok(());
@@ -99,7 +100,7 @@ fn format_variant(
         (None, Some(rn)) => rn.apply(&variant.ident.to_string()),
     };
 
-    let struct_attr = StructAttr::from_variant(enum_attr, &variant_attr);
+    let struct_attr = StructAttr::from_variant(enum_attr, &variant_attr, &variant.fields);
     let variant_type = types::type_def(
         &struct_attr,
         // since we are generating the variant as a struct, it doesn't have a name
@@ -114,8 +115,12 @@ fn format_variant(
         (false, Tagged::Externally) => match &variant.fields {
             Fields::Unit => quote!(format!("\"{}\"", #name)),
             Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
-                let FieldAttr { skip, .. } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?;
-                if skip {
+                let field = &unnamed.unnamed[0];
+                let field_attr = FieldAttr::from_attrs(&field.attrs)?;
+
+                field_attr.assert_validity(field)?;
+
+                if field_attr.skip {
                     quote!(format!("\"{}\"", #name))
                 } else {
                     quote!(format!("{{ \"{}\": {} }}", #name, #inline_type))
@@ -125,19 +130,24 @@ fn format_variant(
         },
         (false, Tagged::Adjacently { tag, content }) => match &variant.fields {
             Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
+                let field = &unnamed.unnamed[0];
+                let field_attr = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?;
+
+                field_attr.assert_validity(field)?;
+
                 let FieldAttr {
                     type_as,
                     type_override,
                     skip,
                     ..
-                } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?;
+                } = field_attr;
 
                 if skip {
                     quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
                 } else {
                     let ty = match (type_override, type_as) {
                         (Some(_), Some(_)) => {
-                            syn_err_spanned!(variant; "`type` is not compatible with `as`")
+                            unreachable!("This has been handled by assert_validity")
                         }
                         (Some(type_override), None) => quote! { #type_override },
                         (None, Some(type_as)) => {
@@ -176,19 +186,24 @@ fn format_variant(
             },
             None => match &variant.fields {
                 Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
+                    let field = &unnamed.unnamed[0];
+                    let field_attr = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?;
+
+                    field_attr.assert_validity(field)?;
+
                     let FieldAttr {
                         type_as,
                         skip,
                         type_override,
                         ..
-                    } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?;
+                    } = field_attr;
 
                     if skip {
                         quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
                     } else {
                         let ty = match (type_override, type_as) {
                             (Some(_), Some(_)) => {
-                                syn_err_spanned!(variant; "`type` is not compatible with `as`")
+                                unreachable!("This has been handled by assert_validity")
                             }
                             (Some(type_override), None) => quote! { #type_override },
                             (None, Some(type_as)) => {
diff --git a/macros/src/types/mod.rs b/macros/src/types/mod.rs
index 4cb551d..caebefc 100644
--- a/macros/src/types/mod.rs
+++ b/macros/src/types/mod.rs
@@ -1,6 +1,10 @@
 use syn::{Fields, Ident, ItemStruct, Result};
 
-use crate::{attr::StructAttr, utils::to_ts_ident, DerivedTS};
+use crate::{
+    attr::{Attr, StructAttr},
+    utils::to_ts_ident,
+    DerivedTS,
+};
 
 mod r#enum;
 mod named;
@@ -19,6 +23,8 @@ pub(crate) fn struct_def(s: &ItemStruct) -> Result<DerivedTS> {
 }
 
 fn type_def(attr: &StructAttr, ident: &Ident, fields: &Fields) -> Result<DerivedTS> {
+    attr.assert_validity(fields)?;
+
     let name = attr.rename.clone().unwrap_or_else(|| to_ts_ident(ident));
     if let Some(attr_type_override) = &attr.type_override {
         return type_override::type_override_struct(attr, &name, attr_type_override);
diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs
index c0ddb43..a5db899 100644
--- a/macros/src/types/named.rs
+++ b/macros/src/types/named.rs
@@ -5,7 +5,7 @@ use syn::{
 };
 
 use crate::{
-    attr::{FieldAttr, Inflection, Optional, StructAttr},
+    attr::{Attr, ContainerAttr, FieldAttr, Inflection, Optional, StructAttr},
     deps::Dependencies,
     utils::{raw_name_to_ts_field, to_ts_ident},
     DerivedTS,
@@ -88,6 +88,10 @@ fn format_field(
     field: &Field,
     rename_all: &Option<Inflection>,
 ) -> Result<()> {
+    let field_attr = FieldAttr::from_attrs(&field.attrs)?;
+
+    field_attr.assert_validity(field)?;
+
     let FieldAttr {
         type_as,
         type_override,
@@ -97,16 +101,12 @@ fn format_field(
         optional,
         flatten,
         docs,
-    } = FieldAttr::from_attrs(&field.attrs)?;
+    } = field_attr;
 
     if skip {
         return Ok(());
     }
 
-    if type_as.is_some() && type_override.is_some() {
-        syn_err_spanned!(field; "`type` is not compatible with `as`")
-    }
-
     let parsed_ty = type_as.as_ref().unwrap_or(&field.ty).clone();
 
     let (ty, optional_annotation) = match optional {
@@ -126,18 +126,6 @@ fn format_field(
     };
 
     if flatten {
-        match (&type_as, &type_override, &rename, inline) {
-            (Some(_), _, _, _) => syn_err_spanned!(field; "`as` is not compatible with `flatten`"),
-            (_, Some(_), _, _) => {
-                syn_err_spanned!(field; "`type` is not compatible with `flatten`")
-            }
-            (_, _, Some(_), _) => {
-                syn_err_spanned!(field; "`rename` is not compatible with `flatten`")
-            }
-            (_, _, _, true) => syn_err_spanned!(field; "`inline` is not compatible with `flatten`"),
-            _ => {}
-        }
-
         flattened_fields.push(quote!(<#ty as #crate_rename::TS>::inline_flattened()));
         dependencies.append_from(ty);
         return Ok(());
@@ -152,6 +140,7 @@ fn format_field(
             quote!(<#ty as #crate_rename::TS>::name())
         }
     });
+
     let field_name = to_ts_ident(field.ident.as_ref().unwrap());
     let name = match (rename, rename_all) {
         (Some(rn), _) => rn,
diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs
index df198da..9589d05 100644
--- a/macros/src/types/newtype.rs
+++ b/macros/src/types/newtype.rs
@@ -2,56 +2,39 @@ use quote::quote;
 use syn::{FieldsUnnamed, Result};
 
 use crate::{
-    attr::{FieldAttr, StructAttr},
+    attr::{Attr, ContainerAttr, FieldAttr, StructAttr},
     deps::Dependencies,
     DerivedTS,
 };
 
 pub(crate) fn newtype(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) -> Result<DerivedTS> {
-    if attr.rename_all.is_some() {
-        syn_err!("`rename_all` is not applicable to newtype structs");
-    }
-    if attr.tag.is_some() {
-        syn_err!("`tag` is not applicable to newtype structs");
-    }
     let inner = fields.unnamed.first().unwrap();
+
+    let field_attr = FieldAttr::from_attrs(&inner.attrs)?;
+    field_attr.assert_validity(inner)?;
+
     let FieldAttr {
         type_as,
         type_override,
-        rename: rename_inner,
         inline,
         skip,
-        optional,
-        flatten,
-        docs: _,
-    } = FieldAttr::from_attrs(&inner.attrs)?;
+        ..
+    } = field_attr;
 
     let crate_rename = attr.crate_rename();
 
-    match (&rename_inner, skip, optional.optional, flatten) {
-        (Some(_), ..) => syn_err_spanned!(fields; "`rename` is not applicable to newtype fields"),
-        (_, true, ..) => return super::unit::null(attr, name),
-        (_, _, true, ..) => {
-            syn_err_spanned!(fields; "`optional` is not applicable to newtype fields")
-        }
-        (_, _, _, true) => {
-            syn_err_spanned!(fields; "`flatten` is not applicable to newtype fields")
-        }
-        _ => {}
-    };
-
-    if type_as.is_some() && type_override.is_some() {
-        syn_err_spanned!(fields; "`type` is not compatible with `as`")
+    if skip {
+        return super::unit::null(attr, name);
     }
 
     let inner_ty = type_as.as_ref().unwrap_or(&inner.ty).clone();
 
     let mut dependencies = Dependencies::new(crate_rename.clone());
 
-    match (type_override.is_none(), inline) {
-        (false, _) => (),
-        (true, true) => dependencies.append_from(&inner_ty),
-        (true, false) => dependencies.push(&inner_ty),
+    match (&type_override, inline) {
+        (Some(_), _) => (),
+        (None, true) => dependencies.append_from(&inner_ty),
+        (None, false) => dependencies.push(&inner_ty),
     };
 
     let inline_def = match type_override {
diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs
index e85ca1a..2b3f44a 100644
--- a/macros/src/types/tuple.rs
+++ b/macros/src/types/tuple.rs
@@ -3,19 +3,12 @@ use quote::quote;
 use syn::{Field, FieldsUnnamed, Path, Result};
 
 use crate::{
-    attr::{FieldAttr, StructAttr},
+    attr::{Attr, ContainerAttr, FieldAttr, StructAttr},
     deps::Dependencies,
     DerivedTS,
 };
 
 pub(crate) fn tuple(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) -> Result<DerivedTS> {
-    if attr.rename_all.is_some() {
-        syn_err!("`rename_all` is not applicable to tuple structs");
-    }
-    if attr.tag.is_some() {
-        syn_err!("`tag` is not applicable to tuple structs");
-    }
-
     let crate_rename = attr.crate_rename();
     let mut formatted_fields = Vec::new();
     let mut dependencies = Dependencies::new(crate_rename.clone());
@@ -53,16 +46,19 @@ fn format_field(
     dependencies: &mut Dependencies,
     field: &Field,
 ) -> Result<()> {
+    let field_attr = FieldAttr::from_attrs(&field.attrs)?;
+    field_attr.assert_validity(field)?;
+
     let FieldAttr {
         type_as,
         type_override,
-        rename,
+        rename: _,
         inline,
         skip,
-        optional,
-        flatten,
+        optional: _,
+        flatten: _,
         docs: _,
-    } = FieldAttr::from_attrs(&field.attrs)?;
+    } = field_attr;
 
     if skip {
         return Ok(());
@@ -70,22 +66,6 @@ fn format_field(
 
     let ty = type_as.as_ref().unwrap_or(&field.ty).clone();
 
-    if type_as.is_some() && type_override.is_some() {
-        syn_err_spanned!(field; "`type` is not compatible with `as`")
-    }
-
-    if rename.is_some() {
-        syn_err_spanned!(field; "`rename` is not applicable to tuple structs")
-    }
-
-    if optional.optional {
-        syn_err_spanned!(field; "`optional` is not applicable to tuple fields")
-    }
-
-    if flatten {
-        syn_err_spanned!(field; "`flatten` is not applicable to tuple fields")
-    }
-
     formatted_fields.push(match type_override {
         Some(ref o) => quote!(#o.to_owned()),
         None if inline => quote!(<#ty as #crate_rename::TS>::inline()),
diff --git a/macros/src/types/type_as.rs b/macros/src/types/type_as.rs
index e964af1..9a76eac 100644
--- a/macros/src/types/type_as.rs
+++ b/macros/src/types/type_as.rs
@@ -2,19 +2,12 @@ use quote::quote;
 use syn::{Result, Type};
 
 use crate::{
-    attr::{EnumAttr, StructAttr},
+    attr::{ContainerAttr, 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 {
@@ -32,25 +25,6 @@ pub(crate) fn type_as_struct(attr: &StructAttr, name: &str, type_as: &Type) -> R
 }
 
 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 {
diff --git a/macros/src/types/type_override.rs b/macros/src/types/type_override.rs
index 8ebc911..1f9c06f 100644
--- a/macros/src/types/type_override.rs
+++ b/macros/src/types/type_override.rs
@@ -2,7 +2,7 @@ use quote::quote;
 use syn::Result;
 
 use crate::{
-    attr::{EnumAttr, StructAttr},
+    attr::{ContainerAttr, EnumAttr, StructAttr},
     deps::Dependencies,
     DerivedTS,
 };
@@ -12,13 +12,6 @@ pub(crate) fn type_override_struct(
     name: &str,
     type_override: &str,
 ) -> Result<DerivedTS> {
-    if attr.rename_all.is_some() {
-        syn_err!("`rename_all` is not compatible with `type`");
-    }
-    if attr.tag.is_some() {
-        syn_err!("`tag` is not compatible with `type`");
-    }
-
     let crate_rename = attr.crate_rename();
 
     Ok(DerivedTS {
@@ -40,25 +33,6 @@ pub(crate) fn type_override_enum(
     name: &str,
     type_override: &str,
 ) -> Result<DerivedTS> {
-    if attr.rename_all.is_some() {
-        syn_err!("`rename_all` is not compatible with `type`");
-    }
-    if attr.rename_all_fields.is_some() {
-        syn_err!("`rename_all_fields` is not compatible with `type`");
-    }
-    if attr.tag.is_some() {
-        syn_err!("`tag` is not compatible with `type`");
-    }
-    if attr.content.is_some() {
-        syn_err!("`content` is not compatible with `type`");
-    }
-    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();
 
     Ok(DerivedTS {
diff --git a/macros/src/types/unit.rs b/macros/src/types/unit.rs
index 6a3c0c3..1a37954 100644
--- a/macros/src/types/unit.rs
+++ b/macros/src/types/unit.rs
@@ -1,7 +1,11 @@
 use quote::quote;
 use syn::Result;
 
-use crate::{attr::StructAttr, deps::Dependencies, DerivedTS};
+use crate::{
+    attr::{ContainerAttr, StructAttr},
+    deps::Dependencies,
+    DerivedTS,
+};
 
 pub(crate) fn empty_object(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
     check_attributes(attr)?;
diff --git a/macros/src/utils.rs b/macros/src/utils.rs
index 01a9953..51511d3 100644
--- a/macros/src/utils.rs
+++ b/macros/src/utils.rs
@@ -7,6 +7,7 @@ use syn::{
     Result, Type,
 };
 
+use super::attr::{Attr, Serde};
 use crate::deps::Dependencies;
 
 macro_rules! syn_err {
@@ -25,16 +26,16 @@ macro_rules! syn_err_spanned {
 }
 
 macro_rules! impl_parse {
-    ($i:ident ($input:ident, $out:ident) { $($k:pat => $e:expr),* $(,)? }) => {
-        impl std::convert::TryFrom<&syn::Attribute> for $i {
+    ($i:ident $(<$inner: ident>)? ($input:ident, $out:ident) { $($k:pat => $e:expr),* $(,)? }) => {
+        impl std::convert::TryFrom<&syn::Attribute> for $i $(<$inner>)? {
             type Error = syn::Error;
 
             fn try_from(attr: &syn::Attribute) -> syn::Result<Self> { attr.parse_args() }
         }
 
-        impl syn::parse::Parse for $i {
+        impl syn::parse::Parse for $i $(<$inner>)? {
             fn parse($input: syn::parse::ParseStream) -> syn::Result<Self> {
-                let mut $out = $i::default();
+                let mut $out = Self::default();
                 loop {
                     let span = $input.span();
                     let key: Ident = $input.call(syn::ext::IdentExt::parse_any)?;
@@ -96,28 +97,33 @@ pub fn raw_name_to_ts_field(value: String) -> String {
 }
 
 /// Parse all `#[ts(..)]` attributes from the given slice.
-pub fn parse_attrs<'a, A>(attrs: &'a [Attribute]) -> Result<impl Iterator<Item = A>>
+pub fn parse_attrs<'a, A>(attrs: &'a [Attribute]) -> Result<A>
 where
-    A: TryFrom<&'a Attribute, Error = Error>,
+    A: TryFrom<&'a Attribute, Error = Error> + Attr,
 {
     Ok(attrs
         .iter()
         .filter(|a| a.path().is_ident("ts"))
         .map(A::try_from)
         .collect::<Result<Vec<A>>>()?
-        .into_iter())
+        .into_iter()
+        .fold(A::default(), |acc, cur| acc.merge(cur)))
 }
 
 /// Parse all `#[serde(..)]` attributes from the given slice.
 #[cfg(feature = "serde-compat")]
 #[allow(unused)]
-pub fn parse_serde_attrs<'a, A: TryFrom<&'a Attribute, Error = Error>>(
-    attrs: &'a [Attribute],
-) -> impl Iterator<Item = A> {
+pub fn parse_serde_attrs<'a, A>(attrs: &'a [Attribute]) -> Serde<A>
+where
+    A: Attr,
+    Serde<A>: TryFrom<&'a Attribute, Error = Error>,
+{
+    use crate::attr::Serde;
+
     attrs
         .iter()
         .filter(|a| a.path().is_ident("serde"))
-        .flat_map(|attr| match A::try_from(attr) {
+        .flat_map(|attr| match Serde::<A>::try_from(attr) {
             Ok(attr) => Some(attr),
             Err(_) => {
                 #[cfg(not(feature = "no-serde-warnings"))]
@@ -133,8 +139,7 @@ pub fn parse_serde_attrs<'a, A: TryFrom<&'a Attribute, Error = Error>>(
                 None
             }
         })
-        .collect::<Vec<_>>()
-        .into_iter()
+        .fold(Serde::<A>::default(), |acc, cur| acc.merge(cur))
 }
 
 /// Return doc comments parsed and formatted as JSDoc.


Test patch:
diff --git a/ts-rs/tests/recursion_limit.rs b/ts-rs/tests/recursion_limit.rs
new file mode 100644
index 0000000..f859023
--- /dev/null
+++ b/ts-rs/tests/recursion_limit.rs
@@ -0,0 +1,170 @@
+#[derive(Debug, ts_rs::TS)]
+#[ts(export)]
+pub enum Iso4217CurrencyCode {
+    AED,
+    AFN,
+    ALL,
+    AMD,
+    ANG,
+    AOA,
+    ARS,
+    AUD,
+    AWG,
+    AZN,
+    BAM,
+    BBD,
+    BDT,
+    BGN,
+    BHD,
+    BIF,
+    BMD,
+    BND,
+    BOB,
+    BRL,
+    BSD,
+    BTN,
+    BWP,
+    BYN,
+    BZD,
+    CAD,
+    CDF,
+    CHF,
+    CLP,
+    CNY,
+    COP,
+    CRC,
+    CUC,
+    CUP,
+    CVE,
+    CZK,
+    DJF,
+    DKK,
+    DOP,
+    DZD,
+    EGP,
+    ERN,
+    ETB,
+    EUR,
+    FJD,
+    FKP,
+    GBP,
+    GEL,
+    GGP,
+    GHS,
+    GIP,
+    GMD,
+    GNF,
+    GTQ,
+    GYD,
+    HKD,
+    HNL,
+    HRK,
+    HTG,
+    HUF,
+    IDR,
+    ILS,
+    IMP,
+    INR,
+    IQD,
+    IRR,
+    ISK,
+    JEP,
+    JMD,
+    JOD,
+    JPY,
+    KES,
+    KGS,
+    KHR,
+    KMF,
+    KPW,
+    KRW,
+    KWD,
+    KYD,
+    KZT,
+    LAK,
+    LBP,
+    LKR,
+    LRD,
+    LSL,
+    LYD,
+    MAD,
+    MDL,
+    MGA,
+    MKD,
+    MMK,
+    MNT,
+    MOP,
+    MRU,
+    MUR,
+    MVR,
+    MWK,
+    MXN,
+    MYR,
+    MZN,
+    NAD,
+    NGN,
+    NIO,
+    NOK,
+    NPR,
+    NZD,
+    OMR,
+    PAB,
+    PEN,
+    PGK,
+    PHP,
+    PKR,
+    PLN,
+    PYG,
+    QAR,
+    RON,
+    RSD,
+    RUB,
+    RWF,
+    SAR,
+    SBD,
+    SCR,
+    SDG,
+    SEK,
+    SGD,
+    SHP,
+    SLL,
+    SOS,
+    SPL,
+    SRD,
+    STN,
+    SVC,
+    SYP,
+    SZL,
+    THB,
+    TJS,
+    TMT,
+    TND,
+    TOP,
+    TRY,
+    TTD,
+    TVD,
+    TWD,
+    TZS,
+    UAH,
+    UGX,
+    USD,
+    UYU,
+    UZS,
+    VEF,
+    VND,
+    VUV,
+    WST,
+    XAF,
+    XCD,
+    XDR,
+    XOF,
+    XPF,
+    YER,
+    ZAR,
+    ZMW,
+    ZWD,
+}
+
+fn main() {
+    println!("{:?}", Iso4217CurrencyCode::USD)
+}
diff --git a/ts-rs/tests/serde-skip-with-default.rs b/ts-rs/tests/serde-skip-with-default.rs
index fc11b9b..b67e2e5 100644
--- a/ts-rs/tests/serde-skip-with-default.rs
+++ b/ts-rs/tests/serde-skip-with-default.rs
@@ -12,7 +12,7 @@ fn default_http_version() -> String {
 #[derive(Debug, Clone, Deserialize, Serialize, TS)]
 #[ts(export, export_to = "serde_skip_with_default/")]
 pub struct Foobar {
-    #[ts(skip)]
+    // #[ts(skip)]
     #[serde(skip, default = "default_http_version")]
     pub http_version: String,
     pub something_else: i32,