Repository: Aleph-Alpha/ts-rs
Pull request #293 (2024-04-02 21:51:12)

dedup dependencies
## Goal

Generate TypeLists containing less duplicates.


Mentions #291 #289 

## Changes

This is an Internal API change, use a HashSet instead of a Vec, and delay formatting until just when it's needed.

## 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: Nice! Pretty much the same as #292, but I think I like this one more.
NyxCode: I explored if generating a custom `TypeList` impl (which would eliminate the recusion problem completely) would a sensible alternative.  

It's definetely possible, but I think it'd require a breaking change to be nice. What I had in mind was
```rs
fn dependency_types() -> impl TypeList {
  #[derive(Copy, Clone)]
  struct Deps;
  impl TypeList for Deps {
    fn for_each(self, v: &mut impl TypeVisitor) {
      v.visit::<TypeOfField1>();
      v.visit::<TypeOfField2>();
      Something::dependency_types().for_each(v);
      SomethingElse::generics().for_each(v);
    }
  }
  Deps
}
```

This breaks with generic parameters though. The naive `for_each` impl would use them, but it can't. to make that work, `Deps` would need to be generic as well. Definetely something we could do right now, but it's kinda messy.

If we changed `fn dependency_types() -> impl TypeList` to `fn dependency_types(v: &mut impl TypeVisitor)`, these problems should go away. That would remove one level of indirection.

So tl;dr: I think we should go ahead with this, and keep this change in mind if someone still runs into the issue or for a breaking 9.0 release some time in the future.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c58c69a..d1d01ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,13 +2,19 @@
 
 ### Breaking
 
+- `#[serde(with = "...")]` requires the use of `#[ts(as = "...")]` or `#[ts(type = "...")]` ([#280](https://github.com/Aleph-Alpha/ts-rs/pull/280))
+- Fix incompatibility with serde for `snake_case`, `kebab-case` and `SCREAMING_SNAKE_CASE` ([#298](https://github.com/Aleph-Alpha/ts-rs/pull/298))
+- `#[ts(rename_all = "...")]` no longer accepts variations in the string's casing, dashes and underscores to make behavior consistent with serde ([#298](https://github.com/Aleph-Alpha/ts-rs/pull/298))
+
 ### Features
 
 - 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))
 
 ### Fixes
 
-Fix `#[ts(rename_all_fields = "...")]` on enums containing tuple or unit variants ([#287](https://github.com/Aleph-Alpha/ts-rs/pull/287))
+- Fix `#[ts(rename_all_fields = "...")]` on enums containing tuple or unit variants ([#287](https://github.com/Aleph-Alpha/ts-rs/pull/287))
 
 # 8.1.0
 
diff --git a/README.md b/README.md
index fa77842..623ca0b 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 ts-rs
 </h1>
 <p align="center">
-generate typescript type declarations from rust types
+Generate typescript type declarations from rust types
 </p>
 
 <div align="center">
@@ -26,20 +26,20 @@ alt="Download" />
 </a>
 </div>
 
-### why?
+### Why?
 When building a web application in rust, data structures have to be shared between backend and frontend.
 Using this library, you can easily generate TypeScript bindings to your rust structs & enums so that you can keep your
 types in one place.
 
 ts-rs might also come in handy when working with webassembly.
 
-### how?
+### How?
 ts-rs exposes a single trait, `TS`. Using a derive macro, you can implement this interface for your types.
 Then, you can use this trait to obtain the TypeScript bindings.
 We recommend doing this in your tests.
 [See the example](https://github.com/Aleph-Alpha/ts-rs/blob/main/example/src/lib.rs) and [the docs](https://docs.rs/ts-rs/latest/ts_rs/).
 
-### get started
+### Get started
 ```toml
 [dependencies]
 ts-rs = "8.1"
@@ -58,7 +58,7 @@ struct User {
 ```
 When running `cargo test`, the TypeScript bindings will be exported to the file `bindings/User.ts`.
 
-### features
+### Features
 - generate type declarations from rust structs
 - generate union declarations from rust enums
 - inline types
@@ -92,7 +92,7 @@ When running `cargo test`, the TypeScript bindings will be exported to the file
 If there's a type you're dealing with which doesn't implement `TS`, use either
 `#[ts(as = "..")]` or `#[ts(type = "..")]`, or open a PR.
 
-### serde compatability
+### `serde` compatability
 With the `serde-compat` feature (enabled by default), serde attributes can be parsed for enums and structs.
 Supported serde attributes:
 - `rename`
@@ -110,17 +110,12 @@ from the generated type, but cannot use `#[serde(skip)]`, use `#[ts(skip)]` inst
 
 When ts-rs encounters an unsupported serde attribute, a warning is emitted, unless the feature `no-serde-warnings` is enabled.
 
-### contributing
+### Contributing
 Contributions are always welcome!
 Feel free to open an issue, discuss using GitHub discussions or open a PR.
 [See CONTRIBUTING.md](https://github.com/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md)
 
-### todo
-- [x] serde compatibility layer
-- [x] documentation
-- [x] use typescript types across files
-- [x] more enum representations
-- [x] generics
-- [x] don't require `'static`
+### MSRV
+The Minimum Supported Rust Version for this crate is 1.75.0
 
 License: MIT
diff --git a/macros/Cargo.toml b/macros/Cargo.toml
index 921ddcc..7373256 100644
--- a/macros/Cargo.toml
+++ b/macros/Cargo.toml
@@ -19,5 +19,4 @@ proc-macro = true
 proc-macro2 = "1"
 quote = "1"
 syn = { version = "2.0.28", features = ["full", "extra-traits"] }
-Inflector = { version = "0.11", default-features = false }
 termcolor = { version = "1", optional = true }
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..4dc4d2a 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)]
@@ -13,6 +13,9 @@ pub struct FieldAttr {
     pub optional: Optional,
     pub flatten: bool,
     pub docs: String,
+
+    #[cfg(feature = "serde-compat")]
+    pub using_serde_with: bool,
 }
 
 /// Indicates whether the field is marked with `#[ts(optional)]`.
@@ -24,47 +27,131 @@ 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(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,
+            #[cfg(feature = "serde-compat")]
+            using_serde_with: self.using_serde_with || other.using_serde_with,
+
+            // 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<()> {
+        #[cfg(feature = "serde-compat")]
+        if self.using_serde_with && !(self.type_as.is_some() || self.type_override.is_some()) {
+            syn_err_spanned!(
+                field;
+                r#"using `#[serde(with = "...")]` requires the use of `#[ts(as = "...")]` or `#[ts(type = "...")]`"#
+            )
+        }
+
+        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"
+                );
+            }
 
-    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);
+            if self.optional.optional {
+                syn_err_spanned!(
+                    field;
+                    "`optional` cannot with tuple struct fields"
+                );
+            }
+        }
+
+        Ok(())
     }
 }
 
@@ -98,7 +185,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,9 +193,12 @@ impl_parse! {
         "default" => {
             use syn::Token;
             if input.peek(Token![=]) {
-                input.parse::<Token![=]>()?;
                 parse_assign_str(input)?;
             }
         },
+        "with" => {
+            parse_assign_str(input)?;
+            out.0.using_serde_with = true;
+        },
     }
 }
diff --git a/macros/src/attr/mod.rs b/macros/src/attr/mod.rs
index b80ea7b..d5a84b6 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::*;
 
@@ -24,12 +24,38 @@ pub enum Inflection {
     Pascal,
     ScreamingSnake,
     Kebab,
+    ScreamingKebab,
+}
+
+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;
-
         match self {
             Inflection::Lower => string.to_lowercase(),
             Inflection::Upper => string.to_uppercase(),
@@ -37,7 +63,18 @@ impl Inflection {
                 let pascal = Inflection::apply(Inflection::Pascal, string);
                 pascal[..1].to_ascii_lowercase() + &pascal[1..]
             }
-            Inflection::Snake => string.to_snake_case(),
+            Inflection::Snake => {
+                let mut s = String::new();
+
+                for (i, ch) in string.char_indices() {
+                    if ch.is_uppercase() && i != 0 {
+                        s.push('_');
+                    }
+                    s.push(ch.to_ascii_lowercase());
+                }
+
+                s
+            }
             Inflection::Pascal => {
                 let mut s = String::with_capacity(string.len());
 
@@ -56,29 +93,13 @@ impl Inflection {
 
                 s
             }
-            Inflection::ScreamingSnake => string.to_screaming_snake_case(),
-            Inflection::Kebab => string.to_kebab_case(),
+            Inflection::ScreamingSnake => Self::Snake.apply(string).to_ascii_uppercase(),
+            Inflection::Kebab => Self::Snake.apply(string).replace('_', "-"),
+            Inflection::ScreamingKebab => Self::Kebab.apply(string).to_ascii_uppercase(),
         }
     }
 }
 
-impl TryFrom<String> for Inflection {
-    type Error = Error;
-
-    fn try_from(value: String) -> Result<Self> {
-        Ok(match &*value.to_lowercase().replace(['_', '-'], "") {
-            "lowercase" => Self::Lower,
-            "uppercase" => Self::Upper,
-            "camelcase" => Self::Camel,
-            "snakecase" => Self::Snake,
-            "pascalcase" => Self::Pascal,
-            "screamingsnakecase" => Self::ScreamingSnake,
-            "kebabcase" => Self::Kebab,
-            _ => syn_err!("invalid inflection: '{}'", value),
-        })
-    }
-}
-
 fn parse_assign_str(input: ParseStream) -> Result<String> {
     input.parse::<Token![=]>()?;
     match Lit::parse(input)? {
@@ -116,7 +137,25 @@ fn parse_concrete(input: ParseStream) -> Result<HashMap<syn::Ident, syn::Type>>
 }
 
 fn parse_assign_inflection(input: ParseStream) -> Result<Inflection> {
-    parse_assign_str(input).and_then(Inflection::try_from)
+    let span = input.span();
+    let str = parse_assign_str(input)?;
+
+    Ok(match &*str {
+        "lowercase" => Inflection::Lower,
+        "UPPERCASE" => Inflection::Upper,
+        "camelCase" => Inflection::Camel,
+        "snake_case" => Inflection::Snake,
+        "PascalCase" => Inflection::Pascal,
+        "SCREAMING_SNAKE_CASE" => Inflection::ScreamingSnake,
+        "kebab-case" => Inflection::Kebab,
+        "SCREAMING-KEBAB-CASE" => Inflection::ScreamingKebab,
+        other => {
+            syn_err!(
+                span;
+                r#"Value "{other}" is not valid for "rename_all". Accepted values are: "lowercase", "UPPERCASE", "camelCase", "snake_case", "PascalCase", "SCREAMING_SNAKE_CASE", "kebab-case" and "SCREAMING-KEBAB-CASE""#
+            )
+        }
+    })
 }
 
 fn parse_assign_from_str<T>(input: ParseStream) -> Result<T>
diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs
index e094834..69a4b06 100644
--- a/macros/src/attr/struct.rs
+++ b/macros/src/attr/struct.rs
@@ -1,8 +1,11 @@
 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_assign_inflection, parse_bound, parse_concrete, Attr,
+    ContainerAttr, Serde,
+};
 use crate::{
     attr::{parse_assign_str, EnumAttr, Inflection, VariantAttr},
     utils::{parse_attrs, parse_docs},
@@ -23,75 +26,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! {
@@ -100,7 +136,7 @@ impl_parse! {
         "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)?),
+        "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?),
         "tag" => out.tag = Some(parse_assign_str(input)?),
         "export" => out.export = true,
         "export_to" => out.export_to = Some(parse_assign_str(input)?),
@@ -111,9 +147,9 @@ 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)?),
+        "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?),
         "tag" => out.0.tag = Some(parse_assign_str(input)?),
         "bound" => out.0.bound = Some(parse_bound(input)?),
         // parse #[serde(default)] to not emit a warning
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 6730539..e38c96c 100644
--- a/macros/src/deps.rs
+++ b/macros/src/deps.rs
@@ -1,54 +1,104 @@
+use std::{collections::HashSet, rc::Rc};
+
 use proc_macro2::TokenStream;
 use quote::{quote, ToTokens};
 use syn::{Path, Type};
 
 pub struct Dependencies {
-    dependencies: Vec<TokenStream>,
-    crate_rename: Path,
-    pub types: Vec<Type>,
+    crate_rename: Rc<Path>,
+    dependencies: HashSet<Dependency>,
+    types: HashSet<Rc<Type>>,
+}
+
+#[derive(Hash, Eq, PartialEq)]
+enum Dependency {
+    // A dependency on all dependencies of `ty`.
+    // This does not include a dependency on `ty` itself - only its dependencies!
+    Transitive {
+        crate_rename: Rc<Path>,
+        ty: Rc<Type>,
+    },
+    // A dependency on all type parameters of `ty`, as returned by `TS::generics()`.
+    // This does not include a dependency on `ty` itself.
+    Generics {
+        crate_rename: Rc<Path>,
+        ty: Rc<Type>,
+    },
+    Type(Rc<Type>),
 }
 
 impl Dependencies {
     pub fn new(crate_rename: Path) -> Self {
         Self {
-            dependencies: Vec::default(),
-            crate_rename,
-            types: Vec::default(),
+            dependencies: HashSet::new(),
+            crate_rename: Rc::new(crate_rename),
+            types: HashSet::new(),
         }
     }
+
+    pub fn used_types(&self) -> impl Iterator<Item = &Type> {
+        self.types.iter().map(Rc::as_ref)
+    }
+
     /// Adds all dependencies from the given type
     pub fn append_from(&mut self, ty: &Type) {
-        let crate_rename = &self.crate_rename;
-        self.dependencies
-            .push(quote![.extend(<#ty as #crate_rename::TS>::dependency_types())]);
-        self.types.push(ty.clone());
+        let ty = self.push_type(ty);
+        self.dependencies.insert(Dependency::Transitive {
+            crate_rename: self.crate_rename.clone(),
+            ty: ty.clone(),
+        });
     }
 
     /// Adds the given type.
     pub fn push(&mut self, ty: &Type) {
-        let crate_rename = &self.crate_rename;
-        self.dependencies.push(quote![.push::<#ty>()]);
-        self.dependencies.push(quote![
-            .extend(<#ty as #crate_rename::TS>::generics())
-        ]);
-        self.types.push(ty.clone());
+        let ty = self.push_type(ty);
+        self.dependencies.insert(Dependency::Type(ty.clone()));
+        self.dependencies.insert(Dependency::Generics {
+            crate_rename: self.crate_rename.clone(),
+            ty: ty.clone(),
+        });
     }
 
-    pub fn append(&mut self, mut other: Dependencies) {
-        if !other.dependencies.is_empty() {
-            self.dependencies.push(quote![.extend(#other)]);
+    pub fn append(&mut self, other: Dependencies) {
+        self.dependencies.extend(other.dependencies);
+        self.types.extend(other.types);
+    }
+
+    fn push_type(&mut self, ty: &Type) -> Rc<Type> {
+        // this can be replaces with `get_or_insert_owned` once #60896 is stabilized
+        match self.types.get(ty) {
+            None => {
+                let ty = Rc::new(ty.clone());
+                self.types.insert(ty.clone());
+                ty
+            }
+            Some(ty) => ty.clone(),
         }
-        self.types.append(&mut other.types);
     }
 }
 
 impl ToTokens for Dependencies {
     fn to_tokens(&self, tokens: &mut TokenStream) {
         let crate_rename = &self.crate_rename;
-        let lines = &self.dependencies;
+        let lines = self.dependencies.iter();
+
         tokens.extend(quote![{
             use #crate_rename::typelist::TypeList;
             ()#(#lines)*
-        }])
+        }]);
+    }
+}
+
+impl ToTokens for Dependency {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(match self {
+            Dependency::Transitive { crate_rename, ty } => {
+                quote![.extend(<#ty as #crate_rename::TS>::dependency_types())]
+            }
+            Dependency::Generics { crate_rename, ty } => {
+                quote![.extend(<#ty as #crate_rename::TS>::generics())]
+            }
+            Dependency::Type(ty) => quote![.push::<#ty>()],
+        });
     }
 }
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index b4895d5..8ec1566 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -368,7 +368,7 @@ fn generate_where_clause(
         let is_type_param = |id: &Ident| generics.type_params().any(|p| &p.ident == id);
 
         let mut used_types = HashSet::new();
-        for ty in &dependencies.types {
+        for ty in dependencies.used_types() {
             used_type_params(&mut used_types, ty, is_type_param);
         }
         used_types.into_iter()
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..10ac25d 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,15 @@ fn format_field(
         optional,
         flatten,
         docs,
-    } = FieldAttr::from_attrs(&field.attrs)?;
+
+        #[cfg(feature = "serde-compat")]
+        using_serde_with: _,
+    } = 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 +129,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 +143,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..e993386 100644
--- a/macros/src/types/newtype.rs
+++ b/macros/src/types/newtype.rs
@@ -2,56 +2,40 @@ 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..889cba5 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,22 @@ 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)?;
+
+        #[cfg(feature = "serde-compat")]
+        using_serde_with: _,
+    } = field_attr;
 
     if skip {
         return Ok(());
@@ -70,22 +69,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.
diff --git a/ts-rs/Cargo.toml b/ts-rs/Cargo.toml
index 9f36e22..a4687ba 100644
--- a/ts-rs/Cargo.toml
+++ b/ts-rs/Cargo.toml
@@ -15,6 +15,7 @@ categories = [
   "web-programming",
 ]
 readme = "../README.md"
+rust-version = "1.75.0"
 
 [features]
 chrono-impl = ["chrono"]
@@ -36,12 +37,13 @@ import-esm = []
 
 [dev-dependencies]
 serde = { version = "1.0", features = ["derive"] }
+serde_json = "1"
 chrono = { version = "0.4", features = ["serde"] }
 
 [dependencies]
 heapless = { version = ">= 0.7, < 0.9", optional = true }
 ts-rs-macros = { version = "=8.1.0", path = "../macros" }
-dprint-plugin-typescript = { version = "0.89", optional = true }
+dprint-plugin-typescript = { version = "0.90", optional = true }
 chrono = { version = "0.4", optional = true }
 bigdecimal = { version = ">= 0.0.13, < 0.5", features = [
   "serde",
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index 1560ac3..0954e69 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -4,7 +4,7 @@
 //! ts-rs
 //! </h1>
 //! <p align="center">
-//! generate typescript type declarations from rust types
+//! Generate typescript type declarations from rust types
 //! </p>
 //!
 //! <div align="center">
@@ -24,20 +24,20 @@
 //! </a>
 //! </div>
 //!
-//! ## why?
+//! ## Why?
 //! When building a web application in rust, data structures have to be shared between backend and frontend.
 //! Using this library, you can easily generate TypeScript bindings to your rust structs & enums so that you can keep your
 //! types in one place.
 //!
 //! ts-rs might also come in handy when working with webassembly.
 //!
-//! ## how?
+//! ## How?
 //! ts-rs exposes a single trait, `TS`. Using a derive macro, you can implement this interface for your types.
 //! Then, you can use this trait to obtain the TypeScript bindings.
 //! We recommend doing this in your tests.
 //! [See the example](https://github.com/Aleph-Alpha/ts-rs/blob/main/example/src/lib.rs) and [the docs](https://docs.rs/ts-rs/latest/ts_rs/).
 //!
-//! ## get started
+//! ## Get started
 //! ```toml
 //! [dependencies]
 //! ts-rs = "8.1"
@@ -56,7 +56,7 @@
 //! ```
 //! When running `cargo test`, the TypeScript bindings will be exported to the file `bindings/User.ts`.
 //!
-//! ## features
+//! ## Features
 //! - generate type declarations from rust structs
 //! - generate union declarations from rust enums
 //! - inline types
@@ -90,7 +90,7 @@
 //! If there's a type you're dealing with which doesn't implement `TS`, use either
 //! `#[ts(as = "..")]` or `#[ts(type = "..")]`, or open a PR.
 //!
-//! ## serde compatability
+//! ## `serde` compatability
 //! With the `serde-compat` feature (enabled by default), serde attributes can be parsed for enums and structs.
 //! Supported serde attributes:
 //! - `rename`
@@ -108,18 +108,13 @@
 //!
 //! When ts-rs encounters an unsupported serde attribute, a warning is emitted, unless the feature `no-serde-warnings` is enabled.
 //!
-//! ## contributing
+//! ## Contributing
 //! Contributions are always welcome!
 //! Feel free to open an issue, discuss using GitHub discussions or open a PR.
 //! [See CONTRIBUTING.md](https://github.com/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md)
 //!
-//! ## todo
-//! - [x] serde compatibility layer
-//! - [x] documentation
-//! - [x] use typescript types across files
-//! - [x] more enum representations
-//! - [x] generics
-//! - [x] don't require `'static`
+//! ## MSRV
+//! The Minimum Supported Rust Version for this crate is 1.75.0
 
 use std::{
     any::TypeId,
@@ -220,7 +215,7 @@ pub mod typelist;
 ///
 /// - **`#[ts(rename_all = "..")]`**  
 ///   Rename all fields/variants of the type.  
-///   Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
+///   Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE"
 ///   <br/><br/>
 ///
 /// - **`#[ts(concrete(..)]`**  
@@ -336,13 +331,13 @@ pub mod typelist;
 ///
 /// - **`#[ts(rename_all = "..")]`**  
 ///   Rename all variants of this enum.  
-///   Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
+///   Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE"
 ///   <br/><br/>
 ///
 /// - **`#[ts(rename_all_fieds = "..")]`**  
 ///   Renames the fields of all the struct variants of this enum. This is equivalent to using
 ///   `#[ts(rename_all = "..")]` on all of the enum's variants.
-///   Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
+///   Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE"
 ///   <br/><br/>
 ///  
 /// ### enum variant attributes
@@ -362,7 +357,7 @@ pub mod typelist;
 ///
 /// - **`#[ts(rename_all = "..")]`**  
 ///   Renames all the fields of a struct variant.
-///   Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
+///   Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case" and "SCREAMING-KEBAB-CASE"
 ///   <br/><br/>
 pub trait TS {
     /// If this type does not have generic parameters, then `WithoutGenerics` should just be `Self`.


Test patch:
diff --git a/ts-rs/tests/imports.rs b/ts-rs/tests/imports.rs
index 35cad17..85eca7c 100644
--- a/ts-rs/tests/imports.rs
+++ b/ts-rs/tests/imports.rs
@@ -3,19 +3,19 @@
 use ts_rs::TS;
 
 #[derive(TS)]
-#[ts(export, export_to = "imports/ts_rs_test_type_a.ts")]
+#[ts(export_to = "imports/ts_rs_test_type_a.ts")]
 pub struct TestTypeA<T> {
     value: T,
 }
 
 #[derive(TS)]
-#[ts(export, export_to = "imports/ts_rs_test_type_b.ts")]
+#[ts(export_to = "imports/ts_rs_test_type_b.ts")]
 pub struct TestTypeB<T> {
     value: T,
 }
 
 #[derive(TS)]
-#[ts(export, export_to = "imports/")]
+#[ts(export_to = "imports/")]
 pub enum TestEnum {
     C { value: TestTypeB<i8> },
     A1 { value: TestTypeA<i32> },
@@ -25,7 +25,7 @@ pub enum TestEnum {
 #[test]
 fn test_def() {
     // The only way to get access to how the imports look is to export the type and load the exported file
-    TestEnum::export().unwrap();
+    TestEnum::export_all().unwrap();
     let text = std::fs::read_to_string(TestEnum::default_output_path().unwrap()).unwrap();
 
     let expected = match (cfg!(feature = "format"), cfg!(feature = "import-esm")) {
diff --git a/ts-rs/tests/recursion_limit.rs b/ts-rs/tests/recursion_limit.rs
index f859023..1f0f98a 100644
--- a/ts-rs/tests/recursion_limit.rs
+++ b/ts-rs/tests/recursion_limit.rs
@@ -1,170 +1,82 @@
+use std::any::TypeId;
+
+use ts_rs::{
+    typelist::{TypeList, TypeVisitor},
+    TS,
+};
+
+#[rustfmt::skip]
 #[derive(Debug, ts_rs::TS)]
-#[ts(export)]
+#[ts(export, export_to = "very_big_types/")]
 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,
+    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,
+}
+
+#[rustfmt::skip]
+#[derive(Debug, ts_rs::TS)]
+#[ts(export, export_to = "very_big_types/")]
+pub enum VeryBigEnum {
+    V001(String), V002(String), V003(String), V004(String), V005(String), V006(String), V007(String),
+    V008(String), V009(String), V010(String), V011(String), V012(String), V013(String), V014(String),
+    V015(String), V016(String), V017(String), V018(String), V019(String), V020(String), V021(String),
+    V022(String), V023(String), V024(String), V025(String), V026(String), V027(String), V028(String),
+    V029(String), V030(String), V031(String), V032(String), V033(String), V034(String), V035(String),
+    V036(String), V037(String), V038(String), V039(String), V040(String), V041(String), V042(String),
+    V043(String), V044(String), V045(String), V046(String), V047(String), V048(String), V049(String),
+    V050(String), V051(String), V052(String), V053(String), V054(String), V055(String), V056(String),
+    V057(String), V058(String), V059(String), V060(String), V061(String), V062(String), V063(String),
+    V064(String), V065(String), V066(String), V067(String), V068(String), V069(String), V070(String),
+    V071(String), V072(String), V073(String), V074(String), V075(String), V076(String), V077(String),
+    V078(String), V079(String), V080(String), V081(String), V082(String), V083(String), V084(String),
+    V085(String), V086(String), V087(String), V088(String), V089(String), V090(String), V091(String),
+    V092(String), V093(String), V094(String), V095(String), V096(String), V097(String), V098(String),
+    V099(String), V100(String), V101(String), V102(String), V103(String), V104(String), V105(String),
+    V106(String), V107(String), V108(String), V109(String), V110(String), V111(String), V112(String),
+    V113(String), V114(String), V115(String), V116(String), V117(String), V118(String), V119(String),
+    V120(String), V121(String), V122(String), V123(String), V124(String), V125(String), V126(String),
+    V127(String), V128(String), V129(String), V130(String), V131(String), V132(String), V133(String),
+    V134(String), V135(String), V136(String), V137(String), V138(String), V139(String), V140(String),
+    V141(String), V142(String), V143(String), V144(String), V145(String), V146(String), V147(String),
+    V148(String), V149(String), V150(String), V151(String), V152(String), V153(String), V154(String),
+    V155(String), V156(String), V157(String), V158(String), V159(String), V160(String), V161(String),
+    V162(String), V163(String), V164(String), V165(String), V166(String), V167(String), V168(String),
+    V169(String), V170(String), V171(String), V172(String), V173(String), V174(String), V175(String),
+    V176(String), V177(String), V178(String), V179(String), V180(String), V181(String), V182(String),
+    V183(String), V184(String), V185(String), V186(String), V187(String), V188(String), V189(String),
+    V190(String), V191(String), V192(String), V193(String), V194(String), V195(String), V196(String),
+    V197(String), V198(String), V199(String), V200(String), V201(String), V202(String), V203(String),
+    V204(String), V205(String), V206(String), V207(String), V208(String), V209(String), V210(String),
+    V211(String), V212(String), V213(String), V214(String), V215(String), V216(String), V217(String),
+    V218(String), V219(String), V220(String), V221(String), V222(String), V223(String), V224(String),
+    V225(String), V226(String), V227(String), V228(String), V229(String), V230(String), V231(String),
+    V232(String), V233(String), V234(String), V235(String), V236(String), V237(String), V238(String),
+    V239(String), V240(String), V241(String), V242(String), V243(String), V244(String), V245(String),
+    V246(String), V247(String), V248(String), V249(String), V250(String), V251(String), V252(String),
+    V253(String), V254(String), V255(String), V256(String), 
 }
 
-fn main() {
-    println!("{:?}", Iso4217CurrencyCode::USD)
+#[test]
+fn very_big_enum() {
+    struct Visitor(bool);
+
+    impl TypeVisitor for Visitor {
+        fn visit<T: TS + 'static + ?Sized>(&mut self) {
+            assert!(!self.0, "there must only be one dependency");
+            assert_eq!(TypeId::of::<T>(), TypeId::of::<String>());
+            self.0 = true;
+        }
+    }
+
+    let mut visitor = Visitor(false);
+    VeryBigEnum::dependency_types().for_each(&mut visitor);
+
+    assert!(visitor.0, "there must be at least one dependency");
 }
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,
diff --git a/ts-rs/tests/serde_with.rs b/ts-rs/tests/serde_with.rs
new file mode 100644
index 0000000..edc1349
--- /dev/null
+++ b/ts-rs/tests/serde_with.rs
@@ -0,0 +1,66 @@
+#![allow(unused, dead_code, clippy::disallowed_names)]
+
+use serde::{Deserialize, Serialize};
+use ts_rs::TS;
+
+#[derive(Serialize, Deserialize, TS)]
+struct Foo {
+    a: i32,
+}
+
+#[derive(Serialize, Deserialize, TS)]
+struct Bar {
+    a: i32,
+}
+
+mod deser {
+    use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+    use super::Foo;
+
+    pub fn serialize<S: Serializer>(foo: &Foo, serializer: S) -> Result<S::Ok, S::Error> {
+        foo.serialize(serializer)
+    }
+
+    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Foo, D::Error> {
+        Foo::deserialize(deserializer)
+    }
+}
+
+// This test should pass when serde-compat is disabled,
+// otherwise, it should fail to compile
+#[test]
+#[cfg(not(feature = "serde-compat"))]
+fn no_serde_compat() {
+    #[derive(Serialize, Deserialize, TS)]
+    struct Baz {
+        #[serde(with = "deser")]
+        a: Foo,
+    }
+
+    assert_eq!(Baz::inline(), "{ a: Foo, }")
+}
+
+#[test]
+fn serde_compat_as() {
+    #[derive(Serialize, Deserialize, TS)]
+    struct Baz {
+        #[serde(with = "deser")]
+        #[ts(as = "Bar")]
+        a: Foo,
+    }
+
+    assert_eq!(Baz::inline(), "{ a: Bar, }")
+}
+
+#[test]
+fn serde_compat_type() {
+    #[derive(Serialize, Deserialize, TS)]
+    struct Baz {
+        #[serde(with = "deser")]
+        #[ts(type = "{ a: number }")]
+        a: Foo,
+    }
+
+    assert_eq!(Baz::inline(), "{ a: { a: number }, }")
+}
diff --git a/ts-rs/tests/struct_rename.rs b/ts-rs/tests/struct_rename.rs
index ef4ed77..9536fca 100644
--- a/ts-rs/tests/struct_rename.rs
+++ b/ts-rs/tests/struct_rename.rs
@@ -46,6 +46,29 @@ fn rename_all_pascal_case() {
     );
 }
 
+#[derive(TS, Default, serde::Serialize)]
+#[ts(export, export_to = "struct_rename/")]
+#[cfg_attr(feature = "serde-compat", serde(rename_all = "SCREAMING-KEBAB-CASE"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(rename_all = "SCREAMING-KEBAB-CASE"))]
+struct RenameAllScreamingKebab {
+    crc32c_hash: i32,
+    some_field: i32,
+    some_other_field: i32,
+}
+
+#[test]
+fn rename_all_screaming_kebab_case() {
+    let rename_all = RenameAllScreamingKebab::default();
+    assert_eq!(
+        serde_json::to_string(&rename_all).unwrap(),
+        r#"{"CRC32C-HASH":0,"SOME-FIELD":0,"SOME-OTHER-FIELD":0}"#
+    );
+    assert_eq!(
+        RenameAllScreamingKebab::inline(),
+        r#"{ "CRC32C-HASH": number, "SOME-FIELD": number, "SOME-OTHER-FIELD": number, }"#
+    );
+}
+
 #[derive(serde::Serialize, TS)]
 #[ts(export, export_to = "struct_rename/", rename_all = "camelCase")]
 struct RenameSerdeSpecialChar {