Repository: Aleph-Alpha/ts-rs
Pull request #398 (2025-04-04 13:35:38)

Allow arbitrary expressions in top-level `#[ts(rename)]`
## Goal

Allow arbitrary expressions in `#[ts(rename)]` on structs, enums, and variants.  
This PR does not touch the `#[ts(rename)]` attribute for fields.  

The motivation for this was #397, but I suspect this might be useful in other cases.  
Since this doesn't really complicate the codebase, I thought it might be a nice addition.

## Changes

- In `DerivedTS`, `ts_name` is now of type `Expr`.
- Slight adjustment in code for parsing attributes.
- Instead of the various `name: &str` parameters, we now pass the `Expr` around instead
- Code-generation was slightly adjusted accordingly
- Updated docs & changelog

This is a technically breaking change, since `TS::output_path` now returns `Option<PathBuf>` instead of `Option<&'static Path>`.  
All existing `#[ts(rename = "literal")]` attributes work the same as before though, since `"literal"` is obviously a valid expression.

## 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: `cargo +nightly fmt` seemed to have corrected some messed-up some line endings. You'll want to look at the diff with whitespace hidden.
gustavo-shigueo: This is awesome! Can't wait to see where else this can be used

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a8ed3aa..933fc3b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,17 @@
 # master
 ### Breaking
+- Changed return type of `TS::output_path()` from `Option<&'static Path>` to `Option<PathBuf>`.  
+  This will only break your code if you manually implement `TS` or directly interact with the `TS` trait.
 - Added `OptionInnerType` associated type to the `TS` trait. If you manually implement `TS`, you must set this associated type to `Self` in all of your implementations.
 - Raised MSRV to `1.78.0` due to use of `#[diagnostic::on_unimplemented]` and `let ... else { ... }`
 
 ### Features
+- The `#[ts(rename)]` attribute on structs, enums and variants now accepts any expression.  
+  This makes it possible to, for example, rename a struct to the name of a module it is contained in using `#[ts(rename = module_path!().rsplit_once("::").unwrap().1)]`
 - Added `#[ts(optional_fields)]` and `#[ts(optional_fields = nullable)]` attribute to structs, this attribute is equivalent to using the corresponding `#[ts(optional)]` or `#[ts(optional = nullable)]` on every field of the struct. ([#366](https://github.com/Aleph-Alpha/ts-rs/pull/366))
 
 ### Fixes
-- Fix `#[ts(optional)]` error when using a type alias for `Option` or fully qqualifying it as `core::option::Option` ([#366](https://github.com/Aleph-Alpha/ts-rs/pull/366))
+- Fix `#[ts(optional)]` error when using a type alias for `Option` or fully qualifying it as `core::option::Option` ([#366](https://github.com/Aleph-Alpha/ts-rs/pull/366))
 - Fix missing import statements when using `#[ts(as = "...")]` at the top level of a struct/enum ([#385](https://github.com/Aleph-Alpha/ts-rs/pull/385))
 
 # 10.1.0
diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs
index 21ff59c..efd542d 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, ItemEnum, Path, Result, Type, WherePredicate};
+use syn::{parse_quote, Attribute, Expr, Ident, ItemEnum, Path, Result, Type, WherePredicate};
 
-use super::{parse_assign_from_str, parse_bound, Attr, ContainerAttr, Serde};
+use super::{parse_assign_expr, 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},
@@ -15,7 +15,7 @@ pub struct EnumAttr {
     pub type_override: Option<String>,
     pub rename_all: Option<Inflection>,
     pub rename_all_fields: Option<Inflection>,
-    pub rename: Option<String>,
+    pub rename: Option<Expr>,
     pub export_to: Option<String>,
     pub export: bool,
     pub docs: String,
@@ -209,7 +209,7 @@ impl_parse! {
         "crate" => out.crate_rename = Some(parse_assign_from_str(input)?),
         "as" => out.type_as = Some(parse_assign_from_str(input)?),
         "type" => out.type_override = Some(parse_assign_str(input)?),
-        "rename" => out.rename = Some(parse_assign_str(input)?),
+        "rename" => out.rename = Some(parse_assign_expr(input)?),
         "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?),
         "rename_all_fields" => out.rename_all_fields = Some(parse_assign_inflection(input)?),
         "export_to" => out.export_to = Some(parse_assign_str(input)?),
@@ -224,7 +224,7 @@ impl_parse! {
 
 impl_parse! {
     Serde<EnumAttr>(input, out) {
-        "rename" => out.0.rename = Some(parse_assign_str(input)?),
+        "rename" => out.0.rename = Some(parse_assign_expr(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)?),
         "tag" => out.0.tag = Some(parse_assign_str(input)?),
diff --git a/macros/src/attr/mod.rs b/macros/src/attr/mod.rs
index 681c834..9ee5072 100644
--- a/macros/src/attr/mod.rs
+++ b/macros/src/attr/mod.rs
@@ -2,13 +2,13 @@ use std::collections::HashMap;
 
 pub use field::*;
 use proc_macro2::TokenTree;
+use quote::quote;
 pub use r#enum::*;
 pub use r#struct::*;
-use quote::quote;
 use syn::{
     parse::{Parse, ParseStream},
     punctuated::Punctuated,
-    Error, Ident, Lit, Path, Result, Token, WherePredicate,
+    Error, Expr, Ident, Lit, Path, Result, Token, WherePredicate,
 };
 pub use variant::*;
 
@@ -149,6 +149,11 @@ fn skip_until_next_comma(input: ParseStream) -> proc_macro2::TokenStream {
         .unwrap()
 }
 
+fn parse_assign_expr(input: ParseStream) -> Result<Expr> {
+    input.parse::<Token![=]>()?;
+    Expr::parse(input)
+}
+
 fn parse_assign_str(input: ParseStream) -> Result<String> {
     input.parse::<Token![=]>()?;
     match Lit::parse(input)? {
diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs
index 80d1e12..bbf231d 100644
--- a/macros/src/attr/struct.rs
+++ b/macros/src/attr/struct.rs
@@ -1,188 +1,188 @@
-use std::collections::HashMap;
-
-use syn::{parse_quote, Attribute, Fields, Ident, Path, Result, Type, WherePredicate};
-
-use super::{
-    parse_assign_from_str, parse_assign_inflection, parse_bound, parse_concrete, parse_optional,
-    Attr, ContainerAttr, Optional, Serde, Tagged,
-};
-use crate::{
-    attr::{parse_assign_str, EnumAttr, Inflection, VariantAttr},
-    utils::{parse_attrs, parse_docs},
-};
-
-#[derive(Default, Clone)]
-pub struct StructAttr {
-    crate_rename: Option<Path>,
-    pub type_as: Option<Type>,
-    pub type_override: Option<String>,
-    pub rename_all: Option<Inflection>,
-    pub rename: Option<String>,
-    pub export_to: Option<String>,
-    pub export: bool,
-    pub tag: Option<String>,
-    pub docs: String,
-    pub concrete: HashMap<Ident, Type>,
-    pub bound: Option<Vec<WherePredicate>>,
-    pub optional_fields: Optional,
-}
-
-impl StructAttr {
-    pub fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
-        let mut result = parse_attrs::<Self>(attrs)?;
-
-        if 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;
-
-        Ok(result)
-    }
-
-    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.or(match variant_fields {
-                Fields::Named(_) => enum_attr.rename_all_fields,
-                Fields::Unnamed(_) | Fields::Unit => None,
-            }),
-            tag: match variant_fields {
-                Fields::Named(_) => match enum_attr
-                    .tagged()
-                    .expect("The variant attribute is known to be valid at this point")
-                {
-                    Tagged::Internally { tag } => Some(tag.to_owned()),
-                    _ => None,
-                },
-                _ => 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,
-            },
-            optional_fields: self.optional_fields.or(other.optional_fields),
-        }
-    }
-
-    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 let Optional::Optional { .. } = self.optional_fields {
-                syn_err!("`optional_fields` 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 let Optional::Optional { .. } = self.optional_fields {
-                syn_err!("`optional_fields` 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");
-            }
-
-            if let Optional::Optional { .. } = self.optional_fields {
-                syn_err!("`optional_fields` 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))
-    }
-}
-
-impl_parse! {
-    StructAttr(input, out) {
-        "crate" => out.crate_rename = Some(parse_assign_from_str(input)?),
-        "as" => out.type_as = Some(parse_assign_from_str(input)?),
-        "type" => out.type_override = Some(parse_assign_str(input)?),
-        "rename" => out.rename = Some(parse_assign_str(input)?),
-        "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?),
-        "tag" => out.tag = Some(parse_assign_str(input)?),
-        "export" => out.export = true,
-        "export_to" => out.export_to = Some(parse_assign_str(input)?),
-        "concrete" => out.concrete = parse_concrete(input)?,
-        "bound" => out.bound = Some(parse_bound(input)?),
-        "optional_fields" => out.optional_fields = parse_optional(input)?,
-    }
-}
-
-impl_parse! {
-    Serde<StructAttr>(input, out) {
-        "rename" => out.0.rename = Some(parse_assign_str(input)?),
-        "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
-        "deny_unknown_fields" | "default" => {
-            use syn::Token;
-            if input.peek(Token![=]) {
-                input.parse::<Token![=]>()?;
-                parse_assign_str(input)?;
-            }
-        },
-    }
-}
+use std::collections::HashMap;
+
+use syn::{parse_quote, Attribute, Expr, Fields, Ident, Path, Result, Type, WherePredicate};
+
+use super::{
+    parse_assign_expr, parse_assign_from_str, parse_assign_inflection, parse_bound, parse_concrete,
+    parse_optional, Attr, ContainerAttr, Optional, Serde, Tagged,
+};
+use crate::{
+    attr::{parse_assign_str, EnumAttr, Inflection, VariantAttr},
+    utils::{parse_attrs, parse_docs},
+};
+
+#[derive(Default, Clone)]
+pub struct StructAttr {
+    crate_rename: Option<Path>,
+    pub type_as: Option<Type>,
+    pub type_override: Option<String>,
+    pub rename_all: Option<Inflection>,
+    pub rename: Option<Expr>,
+    pub export_to: Option<String>,
+    pub export: bool,
+    pub tag: Option<String>,
+    pub docs: String,
+    pub concrete: HashMap<Ident, Type>,
+    pub bound: Option<Vec<WherePredicate>>,
+    pub optional_fields: Optional,
+}
+
+impl StructAttr {
+    pub fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
+        let mut result = parse_attrs::<Self>(attrs)?;
+
+        if 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;
+
+        Ok(result)
+    }
+
+    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.or(match variant_fields {
+                Fields::Named(_) => enum_attr.rename_all_fields,
+                Fields::Unnamed(_) | Fields::Unit => None,
+            }),
+            tag: match variant_fields {
+                Fields::Named(_) => match enum_attr
+                    .tagged()
+                    .expect("The variant attribute is known to be valid at this point")
+                {
+                    Tagged::Internally { tag } => Some(tag.to_owned()),
+                    _ => None,
+                },
+                _ => 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,
+            },
+            optional_fields: self.optional_fields.or(other.optional_fields),
+        }
+    }
+
+    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 let Optional::Optional { .. } = self.optional_fields {
+                syn_err!("`optional_fields` 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 let Optional::Optional { .. } = self.optional_fields {
+                syn_err!("`optional_fields` 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");
+            }
+
+            if let Optional::Optional { .. } = self.optional_fields {
+                syn_err!("`optional_fields` 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))
+    }
+}
+
+impl_parse! {
+    StructAttr(input, out) {
+        "crate" => out.crate_rename = Some(parse_assign_from_str(input)?),
+        "as" => out.type_as = Some(parse_assign_from_str(input)?),
+        "type" => out.type_override = Some(parse_assign_str(input)?),
+        "rename" => out.rename = Some(parse_assign_expr(input)?),
+        "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)?),
+        "concrete" => out.concrete = parse_concrete(input)?,
+        "bound" => out.bound = Some(parse_bound(input)?),
+        "optional_fields" => out.optional_fields = parse_optional(input)?,
+    }
+}
+
+impl_parse! {
+    Serde<StructAttr>(input, out) {
+        "rename" => out.0.rename = Some(parse_assign_expr(input)?),
+        "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
+        "deny_unknown_fields" | "default" => {
+            use syn::Token;
+            if input.peek(Token![=]) {
+                input.parse::<Token![=]>()?;
+                parse_assign_str(input)?;
+            }
+        },
+    }
+}
diff --git a/macros/src/attr/variant.rs b/macros/src/attr/variant.rs
index 51d1da4..44787ae 100644
--- a/macros/src/attr/variant.rs
+++ b/macros/src/attr/variant.rs
@@ -1,6 +1,6 @@
-use syn::{Attribute, Fields, Ident, Result, Type, Variant};
+use syn::{Attribute, Expr, Fields, Ident, Result, Type, Variant};
 
-use super::{Attr, Serde};
+use super::{parse_assign_expr, Attr, Serde};
 use crate::{
     attr::{parse_assign_from_str, parse_assign_inflection, parse_assign_str, Inflection},
     utils::parse_attrs,
@@ -10,7 +10,7 @@ use crate::{
 pub struct VariantAttr {
     pub type_as: Option<Type>,
     pub type_override: Option<String>,
-    pub rename: Option<String>,
+    pub rename: Option<Expr>,
     pub rename_all: Option<Inflection>,
     pub inline: bool,
     pub skip: bool,
@@ -91,7 +91,7 @@ impl_parse! {
     VariantAttr(input, out) {
         "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" => out.rename = Some(parse_assign_expr(input)?),
         "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?),
         "inline" => out.inline = true,
         "skip" => out.skip = true,
@@ -101,7 +101,7 @@ impl_parse! {
 
 impl_parse! {
     Serde<VariantAttr>(input, out) {
-        "rename" => out.0.rename = Some(parse_assign_str(input)?),
+        "rename" => out.0.rename = Some(parse_assign_expr(input)?),
         "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?),
         "skip" => out.0.skip = true,
         "untagged" => out.0.untagged = true,
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index a1ade3a..1862caf 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -6,9 +6,9 @@ use std::collections::{HashMap, HashSet};
 use proc_macro2::{Ident, TokenStream};
 use quote::{format_ident, quote};
 use syn::{
-    parse_quote, spanned::Spanned, ConstParam, GenericParam, Generics, Item, LifetimeParam, Path,
-    Result, Type, TypeArray, TypeParam, TypeParen, TypePath, TypeReference, TypeSlice, TypeTuple,
-    WhereClause, WherePredicate,
+    parse_quote, spanned::Spanned, ConstParam, Expr, GenericParam, Generics, Item, LifetimeParam,
+    Path, Result, Type, TypeArray, TypeParam, TypeParen, TypePath, TypeReference, TypeSlice,
+    TypeTuple, WhereClause, WherePredicate,
 };
 
 use crate::{deps::Dependencies, utils::format_generics};
@@ -21,7 +21,7 @@ mod types;
 
 struct DerivedTS {
     crate_rename: Path,
-    ts_name: String,
+    ts_name: Expr,
     docs: String,
     inline: TokenStream,
     inline_flattened: Option<TokenStream>,
@@ -40,17 +40,21 @@ impl DerivedTS {
             .then(|| self.generate_export_test(&rust_ty, &generics));
 
         let output_path_fn = {
-            let path = match self.export_to.as_deref() {
+            let ts_name = &self.ts_name;
+            // expression of type `String` containing the file path
+            let path_string = match self.export_to.as_deref() {
                 Some(dirname) if dirname.ends_with('/') => {
-                    format!("{}{}.ts", dirname, self.ts_name)
+                    quote![format!("{}{}.ts", #dirname, #ts_name)]
+                }
+                Some(filename) => quote![#filename.to_owned()],
+                None => {
+                    quote![format!("{}.ts", #ts_name)]
                 }
-                Some(filename) => filename.to_owned(),
-                None => format!("{}.ts", self.ts_name),
             };
 
             quote! {
-                fn output_path() -> Option<&'static std::path::Path> {
-                    Some(std::path::Path::new(#path))
+                fn output_path() -> Option<std::path::PathBuf> {
+                    Some(std::path::PathBuf::from(#path_string))
                 }
             }
         };
@@ -83,7 +87,7 @@ impl DerivedTS {
                 type OptionInnerType = Self;
 
                 fn ident() -> String {
-                    #ident.to_owned()
+                    (#ident).to_string()
                 }
 
                 #docs
@@ -122,7 +126,7 @@ impl DerivedTS {
                 format!("{}<{}>", #name, vec![#(#generics_ts_names),*].join(", "))
             }
         } else {
-            quote!(#name.to_owned())
+            quote!((#name).to_string())
         }
     }
 
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index 1f43c3d..737efff 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -1,11 +1,12 @@
 use proc_macro2::TokenStream;
 use quote::quote;
-use syn::{Fields, ItemEnum, Variant};
+use syn::{ext::IdentExt, Expr, Fields, ItemEnum, Variant};
 
 use crate::{
     attr::{Attr, EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr},
     deps::Dependencies,
     types::{self, type_as, type_override},
+    utils::make_string_literal,
     DerivedTS,
 };
 
@@ -18,15 +19,15 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
 
     let name = match &enum_attr.rename {
         Some(existing) => existing.clone(),
-        None => s.ident.to_string(),
+        None => make_string_literal(&s.ident.unraw().to_string(), s.ident.span()),
     };
 
     if let Some(attr_type_override) = &enum_attr.type_override {
-        return type_override::type_override_enum(&enum_attr, &name, attr_type_override);
+        return type_override::type_override_enum(&enum_attr, name, attr_type_override);
     }
 
     if let Some(attr_type_as) = &enum_attr.type_as {
-        return type_as::type_as_enum(&enum_attr, &name, attr_type_as);
+        return type_as::type_as_enum(&enum_attr, name, attr_type_as);
     }
 
     if s.variants.is_empty() {
@@ -81,17 +82,22 @@ fn format_variant(
     }
 
     let untagged_variant = variant_attr.untagged;
-    let name = match (variant_attr.rename.clone(), &enum_attr.rename_all) {
+    let ts_name = match (variant_attr.rename.clone(), &enum_attr.rename_all) {
         (Some(rn), _) => rn,
-        (None, None) => variant.ident.to_string(),
-        (None, Some(rn)) => rn.apply(&variant.ident.to_string()),
+        (None, None) => {
+            make_string_literal(&variant.ident.unraw().to_string(), variant.ident.span())
+        }
+        (None, Some(rn)) => make_string_literal(
+            &rn.apply(&variant.ident.unraw().to_string()),
+            variant.ident.span(),
+        ),
     };
 
     let struct_attr = StructAttr::from_variant(enum_attr, &variant_attr, &variant.fields);
     let variant_type = types::type_def(
         &struct_attr,
         // In internally tagged enums, we can tag the struct
-        &name,
+        ts_name.clone(),
         &variant.fields,
     )?;
 
@@ -114,7 +120,7 @@ fn format_variant(
     let formatted = match (untagged_variant, enum_attr.tagged()?) {
         (true, _) | (_, Tagged::Untagged) => quote!(#parsed_ty),
         (false, Tagged::Externally) => match &variant.fields {
-            Fields::Unit => quote!(format!("\"{}\"", #name)),
+            Fields::Unit => quote!(format!("\"{}\"", #ts_name)),
             Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
                 let field = &unnamed.unnamed[0];
                 let field_attr = FieldAttr::from_attrs(&field.attrs)?;
@@ -122,12 +128,12 @@ fn format_variant(
                 field_attr.assert_validity(field)?;
 
                 if field_attr.skip {
-                    quote!(format!("\"{}\"", #name))
+                    quote!(format!("\"{}\"", #ts_name))
                 } else {
-                    quote!(format!("{{ \"{}\": {} }}", #name, #parsed_ty))
+                    quote!(format!("{{ \"{}\": {} }}", #ts_name, #parsed_ty))
                 }
             }
-            _ => quote!(format!("{{ \"{}\": {} }}", #name, #parsed_ty)),
+            _ => quote!(format!("{{ \"{}\": {} }}", #ts_name, #parsed_ty)),
         },
         (false, Tagged::Adjacently { tag, content }) => match &variant.fields {
             Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
@@ -137,7 +143,7 @@ fn format_variant(
                 field_attr.assert_validity(field)?;
 
                 if field_attr.skip {
-                    quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
+                    quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #ts_name))
                 } else {
                     let ty = match field_attr.type_override {
                         Some(type_override) => quote!(#type_override),
@@ -146,12 +152,14 @@ fn format_variant(
                             quote!(<#ty as #crate_rename::TS>::name())
                         }
                     };
-                    quote!(format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #ty))
+                    quote!(
+                        format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #ts_name, #content, #ty)
+                    )
                 }
             }
-            Fields::Unit => quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name)),
+            Fields::Unit => quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #ts_name)),
             _ => quote!(
-                format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #parsed_ty)
+                format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #ts_name, #content, #parsed_ty)
             ),
         },
         (false, Tagged::Internally { tag }) => match variant_type.inline_flattened {
@@ -166,7 +174,7 @@ fn format_variant(
                     field_attr.assert_validity(field)?;
 
                     if field_attr.skip {
-                        quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
+                        quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #ts_name))
                     } else {
                         let ty = match field_attr.type_override {
                             Some(type_override) => quote! { #type_override },
@@ -176,12 +184,12 @@ fn format_variant(
                             }
                         };
 
-                        quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #name, #ty))
+                        quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #ts_name, #ty))
                     }
                 }
-                Fields::Unit => quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name)),
+                Fields::Unit => quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #ts_name)),
                 _ => {
-                    quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #name, #parsed_ty))
+                    quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #ts_name, #parsed_ty))
                 }
             },
         },
@@ -192,8 +200,7 @@ fn format_variant(
 }
 
 // bindings for an empty enum (`never` in TS)
-fn empty_enum(name: impl Into<String>, enum_attr: EnumAttr) -> DerivedTS {
-    let name = name.into();
+fn empty_enum(ts_name: Expr, enum_attr: EnumAttr) -> DerivedTS {
     let crate_rename = enum_attr.crate_rename();
     DerivedTS {
         crate_rename: crate_rename.clone(),
@@ -203,7 +210,7 @@ fn empty_enum(name: impl Into<String>, enum_attr: EnumAttr) -> DerivedTS {
         dependencies: Dependencies::new(crate_rename),
         export: enum_attr.export,
         export_to: enum_attr.export_to,
-        ts_name: name,
+        ts_name,
         concrete: enum_attr.concrete,
         bound: enum_attr.bound,
     }
diff --git a/macros/src/types/mod.rs b/macros/src/types/mod.rs
index 50bf3c4..417ccc2 100644
--- a/macros/src/types/mod.rs
+++ b/macros/src/types/mod.rs
@@ -1,50 +1,52 @@
-use syn::{Fields, ItemStruct, Result};
-
-use crate::{
-    attr::{Attr, StructAttr},
-    DerivedTS,
-};
-
-mod r#enum;
-mod named;
-mod newtype;
-mod tuple;
-mod type_as;
-mod type_override;
-mod unit;
-
-pub(crate) use r#enum::r#enum_def;
-
-pub(crate) fn struct_def(s: &ItemStruct) -> Result<DerivedTS> {
-    let attr = StructAttr::from_attrs(&s.attrs)?;
-
-    type_def(&attr, &s.ident.to_string(), &s.fields)
-}
-
-fn type_def(attr: &StructAttr, ident: &str, fields: &Fields) -> Result<DerivedTS> {
-    attr.assert_validity(fields)?;
-
-    let name = attr
-        .rename
-        .clone()
-        .unwrap_or_else(|| ident.trim_start_matches("r#").to_owned());
-    if let Some(attr_type_override) = &attr.type_override {
-        return type_override::type_override_struct(attr, &name, attr_type_override);
-    }
-    if let Some(attr_type_as) = &attr.type_as {
-        return type_as::type_as_struct(attr, &name, attr_type_as);
-    }
-
-    match fields {
-        Fields::Named(named) => match named.named.len() {
-            0 if attr.tag.is_none() => unit::empty_object(attr, &name),
-            _ => named::named(attr, &name, named),
-        },
-        Fields::Unnamed(unnamed) => match unnamed.unnamed.len() {
-            0 => unit::empty_array(attr, &name),
-            1 => newtype::newtype(attr, &name, unnamed),
-            _ => tuple::tuple(attr, &name, unnamed),
-        },
-        Fields::Unit => unit::null(attr, &name),
-    }
-}
+use syn::{ext::IdentExt, Expr, Fields, ItemStruct, Result};
+
+use crate::{
+    attr::{Attr, StructAttr},
+    DerivedTS,
+};
+
+mod r#enum;
+mod named;
+mod newtype;
+mod tuple;
+mod type_as;
+mod type_override;
+mod unit;
+
+pub(crate) use r#enum::r#enum_def;
+
+use crate::utils::make_string_literal;
+
+pub(crate) fn struct_def(s: &ItemStruct) -> Result<DerivedTS> {
+    let attr = StructAttr::from_attrs(&s.attrs)?;
+
+    let ts_name = attr
+        .rename
+        .clone()
+        .unwrap_or_else(|| make_string_literal(&s.ident.unraw().to_string(), s.ident.span()));
+    type_def(&attr, ts_name, &s.fields)
+}
+
+fn type_def(attr: &StructAttr, ts_name: Expr, fields: &Fields) -> Result<DerivedTS> {
+    attr.assert_validity(fields)?;
+
+    if let Some(attr_type_override) = &attr.type_override {
+        return type_override::type_override_struct(attr, ts_name, attr_type_override);
+    }
+    if let Some(attr_type_as) = &attr.type_as {
+        return type_as::type_as_struct(attr, ts_name, attr_type_as);
+    }
+
+    match fields {
+        Fields::Named(named) => match named.named.len() {
+            0 if attr.tag.is_none() => unit::empty_object(attr, ts_name),
+            _ => named::named(attr, ts_name, named),
+        },
+        Fields::Unnamed(unnamed) => match unnamed.unnamed.len() {
+            0 => unit::empty_array(attr, ts_name),
+            1 => newtype::newtype(attr, ts_name, unnamed),
+            _ => tuple::tuple(attr, ts_name, unnamed),
+        },
+        Fields::Unit => unit::null(attr, ts_name),
+    }
+}
diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs
index 6b9a2de..c15e9c4 100644
--- a/macros/src/types/named.rs
+++ b/macros/src/types/named.rs
@@ -1,191 +1,190 @@
-use crate::{
-    attr::{Attr, ContainerAttr, FieldAttr, Inflection, Optional, StructAttr},
-    deps::Dependencies,
-    utils::{raw_name_to_ts_field, to_ts_ident},
-    DerivedTS,
-};
-use proc_macro2::TokenStream;
-use quote::{quote, quote_spanned};
-use syn::spanned::Spanned;
-use syn::{parse_quote, Field, FieldsNamed, Path, Result};
-
-pub(crate) fn named(attr: &StructAttr, name: &str, fields: &FieldsNamed) -> Result<DerivedTS> {
-    let crate_rename = attr.crate_rename();
-
-    let mut formatted_fields = Vec::new();
-    let mut flattened_fields = Vec::new();
-    let mut dependencies = Dependencies::new(crate_rename.clone());
-
-    if let Some(tag) = &attr.tag {
-        let formatted = format!("\"{}\": \"{}\",", tag, name);
-        formatted_fields.push(quote! {
-            #formatted.to_string()
-        });
-    }
-
-    for field in &fields.named {
-        format_field(
-            &crate_rename,
-            &mut formatted_fields,
-            &mut flattened_fields,
-            &mut dependencies,
-            field,
-            &attr.rename_all,
-            attr.optional_fields,
-        )?;
-    }
-
-    let fields = quote!(<[String]>::join(&[#(#formatted_fields),*], " "));
-    let flattened = quote!(<[String]>::join(&[#(#flattened_fields),*], " & "));
-
-    let inline = match (formatted_fields.len(), flattened_fields.len()) {
-        (0, 0) => quote!("{  }".to_owned()),
-        (_, 0) => quote!(format!("{{ {} }}", #fields)),
-        (0, 1) => quote! {{
-            if #flattened.starts_with('(') && #flattened.ends_with(')') {
-                #flattened[1..#flattened.len() - 1].trim().to_owned()
-            } else {
-                #flattened.trim().to_owned()
-            }
-        }},
-        (0, _) => quote!(#flattened),
-        (_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
-    };
-
-    let inline_flattened = match (formatted_fields.len(), flattened_fields.len()) {
-        (0, 0) => quote!("{  }".to_owned()),
-        (_, 0) => quote!(format!("{{ {} }}", #fields)),
-        (0, _) => quote!(#flattened),
-        (_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
-    };
-
-    Ok(DerivedTS {
-        crate_rename,
-        // the `replace` combines `{ ... } & { ... }` into just one `{ ... }`. Not necessary, but it
-        // results in simpler type definitions.
-        inline: quote!(#inline.replace(" } & { ", " ")),
-        inline_flattened: Some(quote!(#inline_flattened.replace(" } & { ", " "))),
-        docs: attr.docs.clone(),
-        dependencies,
-        export: attr.export,
-        export_to: attr.export_to.clone(),
-        ts_name: name.to_owned(),
-        concrete: attr.concrete.clone(),
-        bound: attr.bound.clone(),
-    })
-}
-
-// build an expression which expands to a string, representing a single field of a struct.
-//
-// formatted_fields will contain all the fields that do not contain the flatten
-// attribute, in the format
-// key: type,
-//
-// flattened_fields will contain all the fields that contain the flatten attribute
-// in their respective formats, which for a named struct is the same as formatted_fields,
-// but for enums is
-// ({ /* variant data */ } | { /* variant data */ })
-fn format_field(
-    crate_rename: &Path,
-    formatted_fields: &mut Vec<TokenStream>,
-    flattened_fields: &mut Vec<TokenStream>,
-    dependencies: &mut Dependencies,
-    field: &Field,
-    rename_all: &Option<Inflection>,
-    struct_optional: Optional,
-) -> Result<()> {
-    let field_attr = FieldAttr::from_attrs(&field.attrs)?;
-
-    field_attr.assert_validity(field)?;
-
-    if field_attr.skip {
-        return Ok(());
-    }
-
-    if let Some(ref type_override) = field_attr.type_override {
-        let field_name = to_ts_ident(field.ident.as_ref().unwrap());
-        let name = match (field_attr.rename.as_ref(), rename_all) {
-            (Some(rn), _) => rn.to_owned(),
-            (None, Some(rn)) => rn.apply(&field_name),
-            (None, None) => field_name,
-        };
-        let valid_name = raw_name_to_ts_field(name);
-
-        // Start every doc string with a newline, because when other characters are in front, it is not "understood" by VSCode
-        let docs = match field_attr.docs.is_empty() {
-            true => "".to_string(),
-            false => format!("\n{}", &field_attr.docs),
-        };
-
-        formatted_fields.push(quote! {
-            format!("{}{}: {},", #docs, #valid_name, #type_override)
-        });
-
-        return Ok(());
-    }
-
-    let ty = field_attr.type_as(&field.ty);
-
-    let (optional_annotation, nullable) = match (struct_optional, field_attr.optional) {
-        // `#[ts(optional)]` on field takes precedence, and is enforced **AT COMPILE TIME**
-        (_, Optional::Optional { nullable }) => (
-            // expression that evaluates to the string "?", but fails to compile if `ty` is not an `Option`.
-            quote_spanned! { field.span() => {
-                fn check_that_field_is_option<T: #crate_rename::IsOption>(_: std::marker::PhantomData<T>) {}
-                let x: std::marker::PhantomData<#ty> = std::marker::PhantomData;
-                check_that_field_is_option(x);
-                "?"
-            }},
-            nullable,
-        ),
-        // `#[ts(optional)]` on the struct acts as `#[ts(optional)]` on a field, but does not error on non-`Option`
-        // fields. Instead, it is a no-op.
-        (Optional::Optional { nullable }, _) => (
-            quote! {
-                if <#ty as #crate_rename::TS>::IS_OPTION { "?" } else { "" }
-            },
-            nullable,
-        ),
-        _ => (quote!(""), true),
-    };
-
-    let ty = if nullable {
-        ty
-    } else {
-        parse_quote! {<#ty as #crate_rename::TS>::OptionInnerType}
-    };
-
-    if field_attr.flatten {
-        flattened_fields.push(quote!(<#ty as #crate_rename::TS>::inline_flattened()));
-        dependencies.append_from(&ty);
-        return Ok(());
-    }
-
-    let formatted_ty = if field_attr.inline {
-        dependencies.append_from(&ty);
-        quote!(<#ty as #crate_rename::TS>::inline())
-    } else {
-        dependencies.push(&ty);
-        quote!(<#ty as #crate_rename::TS>::name())
-    };
-
-    let field_name = to_ts_ident(field.ident.as_ref().unwrap());
-    let name = match (field_attr.rename, rename_all) {
-        (Some(rn), _) => rn,
-        (None, Some(rn)) => rn.apply(&field_name),
-        (None, None) => field_name,
-    };
-    let valid_name = raw_name_to_ts_field(name);
-
-    // Start every doc string with a newline, because when other characters are in front, it is not "understood" by VSCode
-    let docs = match field_attr.docs.is_empty() {
-        true => "".to_string(),
-        false => format!("\n{}", &field_attr.docs),
-    };
-
-    formatted_fields.push(quote! {
-        format!("{}{}{}: {},", #docs, #valid_name, #optional_annotation, #formatted_ty)
-    });
-
-    Ok(())
-}
+use proc_macro2::TokenStream;
+use quote::{quote, quote_spanned};
+use syn::{parse_quote, spanned::Spanned, Expr, Field, FieldsNamed, Path, Result};
+
+use crate::{
+    attr::{Attr, ContainerAttr, FieldAttr, Inflection, Optional, StructAttr},
+    deps::Dependencies,
+    utils::{raw_name_to_ts_field, to_ts_ident},
+    DerivedTS,
+};
+
+pub(crate) fn named(attr: &StructAttr, ts_name: Expr, fields: &FieldsNamed) -> Result<DerivedTS> {
+    let crate_rename = attr.crate_rename();
+
+    let mut formatted_fields = Vec::new();
+    let mut flattened_fields = Vec::new();
+    let mut dependencies = Dependencies::new(crate_rename.clone());
+
+    if let Some(tag) = &attr.tag {
+        formatted_fields.push(quote! {
+            format!("\"{}\": \"{}\",", #tag, #ts_name)
+        });
+    }
+
+    for field in &fields.named {
+        format_field(
+            &crate_rename,
+            &mut formatted_fields,
+            &mut flattened_fields,
+            &mut dependencies,
+            field,
+            &attr.rename_all,
+            attr.optional_fields,
+        )?;
+    }
+
+    let fields = quote!(<[String]>::join(&[#(#formatted_fields),*], " "));
+    let flattened = quote!(<[String]>::join(&[#(#flattened_fields),*], " & "));
+
+    let inline = match (formatted_fields.len(), flattened_fields.len()) {
+        (0, 0) => quote!("{  }".to_owned()),
+        (_, 0) => quote!(format!("{{ {} }}", #fields)),
+        (0, 1) => quote! {{
+            if #flattened.starts_with('(') && #flattened.ends_with(')') {
+                #flattened[1..#flattened.len() - 1].trim().to_owned()
+            } else {
+                #flattened.trim().to_owned()
+            }
+        }},
+        (0, _) => quote!(#flattened),
+        (_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
+    };
+
+    let inline_flattened = match (formatted_fields.len(), flattened_fields.len()) {
+        (0, 0) => quote!("{  }".to_owned()),
+        (_, 0) => quote!(format!("{{ {} }}", #fields)),
+        (0, _) => quote!(#flattened),
+        (_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
+    };
+
+    Ok(DerivedTS {
+        crate_rename,
+        // the `replace` combines `{ ... } & { ... }` into just one `{ ... }`. Not necessary, but it
+        // results in simpler type definitions.
+        inline: quote!(#inline.replace(" } & { ", " ")),
+        inline_flattened: Some(quote!(#inline_flattened.replace(" } & { ", " "))),
+        docs: attr.docs.clone(),
+        dependencies,
+        export: attr.export,
+        export_to: attr.export_to.clone(),
+        ts_name,
+        concrete: attr.concrete.clone(),
+        bound: attr.bound.clone(),
+    })
+}
+
+// build an expression which expands to a string, representing a single field of a struct.
+//
+// formatted_fields will contain all the fields that do not contain the flatten
+// attribute, in the format
+// key: type,
+//
+// flattened_fields will contain all the fields that contain the flatten attribute
+// in their respective formats, which for a named struct is the same as formatted_fields,
+// but for enums is
+// ({ /* variant data */ } | { /* variant data */ })
+fn format_field(
+    crate_rename: &Path,
+    formatted_fields: &mut Vec<TokenStream>,
+    flattened_fields: &mut Vec<TokenStream>,
+    dependencies: &mut Dependencies,
+    field: &Field,
+    rename_all: &Option<Inflection>,
+    struct_optional: Optional,
+) -> Result<()> {
+    let field_attr = FieldAttr::from_attrs(&field.attrs)?;
+
+    field_attr.assert_validity(field)?;
+
+    if field_attr.skip {
+        return Ok(());
+    }
+
+    if let Some(ref type_override) = field_attr.type_override {
+        let field_name = to_ts_ident(field.ident.as_ref().unwrap());
+        let name = match (field_attr.rename.as_ref(), rename_all) {
+            (Some(rn), _) => rn.to_owned(),
+            (None, Some(rn)) => rn.apply(&field_name),
+            (None, None) => field_name,
+        };
+        let valid_name = raw_name_to_ts_field(name);
+
+        // Start every doc string with a newline, because when other characters are in front, it is not "understood" by VSCode
+        let docs = match field_attr.docs.is_empty() {
+            true => "".to_string(),
+            false => format!("\n{}", &field_attr.docs),
+        };
+
+        formatted_fields.push(quote! {
+            format!("{}{}: {},", #docs, #valid_name, #type_override)
+        });
+
+        return Ok(());
+    }
+
+    let ty = field_attr.type_as(&field.ty);
+
+    let (optional_annotation, nullable) = match (struct_optional, field_attr.optional) {
+        // `#[ts(optional)]` on field takes precedence, and is enforced **AT COMPILE TIME**
+        (_, Optional::Optional { nullable }) => (
+            // expression that evaluates to the string "?", but fails to compile if `ty` is not an `Option`.
+            quote_spanned! { field.span() => {
+                fn check_that_field_is_option<T: #crate_rename::IsOption>(_: std::marker::PhantomData<T>) {}
+                let x: std::marker::PhantomData<#ty> = std::marker::PhantomData;
+                check_that_field_is_option(x);
+                "?"
+            }},
+            nullable,
+        ),
+        // `#[ts(optional)]` on the struct acts as `#[ts(optional)]` on a field, but does not error on non-`Option`
+        // fields. Instead, it is a no-op.
+        (Optional::Optional { nullable }, _) => (
+            quote! {
+                if <#ty as #crate_rename::TS>::IS_OPTION { "?" } else { "" }
+            },
+            nullable,
+        ),
+        _ => (quote!(""), true),
+    };
+
+    let ty = if nullable {
+        ty
+    } else {
+        parse_quote! {<#ty as #crate_rename::TS>::OptionInnerType}
+    };
+
+    if field_attr.flatten {
+        flattened_fields.push(quote!(<#ty as #crate_rename::TS>::inline_flattened()));
+        dependencies.append_from(&ty);
+        return Ok(());
+    }
+
+    let formatted_ty = if field_attr.inline {
+        dependencies.append_from(&ty);
+        quote!(<#ty as #crate_rename::TS>::inline())
+    } else {
+        dependencies.push(&ty);
+        quote!(<#ty as #crate_rename::TS>::name())
+    };
+
+    let field_name = to_ts_ident(field.ident.as_ref().unwrap());
+    let name = match (field_attr.rename, rename_all) {
+        (Some(rn), _) => rn,
+        (None, Some(rn)) => rn.apply(&field_name),
+        (None, None) => field_name,
+    };
+    let valid_name = raw_name_to_ts_field(name);
+
+    // Start every doc string with a newline, because when other characters are in front, it is not "understood" by VSCode
+    let docs = match field_attr.docs.is_empty() {
+        true => "".to_string(),
+        false => format!("\n{}", &field_attr.docs),
+    };
+
+    formatted_fields.push(quote! {
+        format!("{}{}{}: {},", #docs, #valid_name, #optional_annotation, #formatted_ty)
+    });
+
+    Ok(())
+}
diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs
index c48b955..f2863b0 100644
--- a/macros/src/types/newtype.rs
+++ b/macros/src/types/newtype.rs
@@ -1,5 +1,5 @@
 use quote::quote;
-use syn::{FieldsUnnamed, Result};
+use syn::{Expr, FieldsUnnamed, Result};
 
 use crate::{
     attr::{Attr, ContainerAttr, FieldAttr, StructAttr},
@@ -7,7 +7,11 @@ use crate::{
     DerivedTS,
 };
 
-pub(crate) fn newtype(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) -> Result<DerivedTS> {
+pub(crate) fn newtype(
+    attr: &StructAttr,
+    ts_name: Expr,
+    fields: &FieldsUnnamed,
+) -> Result<DerivedTS> {
     let inner = fields.unnamed.first().unwrap();
 
     let field_attr = FieldAttr::from_attrs(&inner.attrs)?;
@@ -16,7 +20,7 @@ pub(crate) fn newtype(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) ->
     let crate_rename = attr.crate_rename();
 
     if field_attr.skip {
-        return super::unit::null(attr, name);
+        return super::unit::null(attr, ts_name);
     }
 
     let inner_ty = field_attr.type_as(&inner.ty);
@@ -43,7 +47,7 @@ pub(crate) fn newtype(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) ->
         dependencies,
         export: attr.export,
         export_to: attr.export_to.clone(),
-        ts_name: name.to_owned(),
+        ts_name,
         concrete: attr.concrete.clone(),
         bound: attr.bound.clone(),
     })
diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs
index 0b8571a..6fb04a7 100644
--- a/macros/src/types/tuple.rs
+++ b/macros/src/types/tuple.rs
@@ -1,6 +1,6 @@
 use proc_macro2::TokenStream;
 use quote::quote;
-use syn::{Field, FieldsUnnamed, Path, Result};
+use syn::{Expr, Field, FieldsUnnamed, Path, Result};
 
 use crate::{
     attr::{Attr, ContainerAttr, FieldAttr, StructAttr},
@@ -8,7 +8,7 @@ use crate::{
     DerivedTS,
 };
 
-pub(crate) fn tuple(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) -> Result<DerivedTS> {
+pub(crate) fn tuple(attr: &StructAttr, ts_name: Expr, fields: &FieldsUnnamed) -> Result<DerivedTS> {
     let crate_rename = attr.crate_rename();
     let mut formatted_fields = Vec::new();
     let mut dependencies = Dependencies::new(crate_rename.clone());
@@ -34,7 +34,7 @@ pub(crate) fn tuple(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) -> Re
         dependencies,
         export: attr.export,
         export_to: attr.export_to.clone(),
-        ts_name: name.to_owned(),
+        ts_name,
         concrete: attr.concrete.clone(),
         bound: attr.bound.clone(),
     })
diff --git a/macros/src/types/type_as.rs b/macros/src/types/type_as.rs
index 88c9dac..ccdce14 100644
--- a/macros/src/types/type_as.rs
+++ b/macros/src/types/type_as.rs
@@ -1,5 +1,5 @@
 use quote::quote;
-use syn::{Result, Type};
+use syn::{Expr, Result, Type};
 
 use crate::{
     attr::{ContainerAttr, EnumAttr, StructAttr},
@@ -7,7 +7,11 @@ use crate::{
     DerivedTS,
 };
 
-pub(crate) fn type_as_struct(attr: &StructAttr, name: &str, type_as: &Type) -> Result<DerivedTS> {
+pub(crate) fn type_as_struct(
+    attr: &StructAttr,
+    ts_name: Expr,
+    type_as: &Type,
+) -> Result<DerivedTS> {
     let crate_rename = attr.crate_rename();
 
     let mut dependencies = Dependencies::new(crate_rename.clone());
@@ -21,13 +25,13 @@ pub(crate) fn type_as_struct(attr: &StructAttr, name: &str, type_as: &Type) -> R
         dependencies,
         export: attr.export,
         export_to: attr.export_to.clone(),
-        ts_name: name.to_owned(),
+        ts_name,
         concrete: attr.concrete.clone(),
         bound: attr.bound.clone(),
     })
 }
 
-pub(crate) fn type_as_enum(attr: &EnumAttr, name: &str, type_as: &Type) -> Result<DerivedTS> {
+pub(crate) fn type_as_enum(attr: &EnumAttr, ts_name: Expr, type_as: &Type) -> Result<DerivedTS> {
     let crate_rename = attr.crate_rename();
 
     let mut dependencies = Dependencies::new(crate_rename.clone());
@@ -41,7 +45,7 @@ pub(crate) fn type_as_enum(attr: &EnumAttr, name: &str, type_as: &Type) -> Resul
         dependencies,
         export: attr.export,
         export_to: attr.export_to.clone(),
-        ts_name: name.to_owned(),
+        ts_name,
         concrete: attr.concrete.clone(),
         bound: attr.bound.clone(),
     })
diff --git a/macros/src/types/type_override.rs b/macros/src/types/type_override.rs
index 1f9c06f..173f6f0 100644
--- a/macros/src/types/type_override.rs
+++ b/macros/src/types/type_override.rs
@@ -1,5 +1,5 @@
 use quote::quote;
-use syn::Result;
+use syn::{Expr, Result};
 
 use crate::{
     attr::{ContainerAttr, EnumAttr, StructAttr},
@@ -9,7 +9,7 @@ use crate::{
 
 pub(crate) fn type_override_struct(
     attr: &StructAttr,
-    name: &str,
+    ts_name: Expr,
     type_override: &str,
 ) -> Result<DerivedTS> {
     let crate_rename = attr.crate_rename();
@@ -22,7 +22,7 @@ pub(crate) fn type_override_struct(
         dependencies: Dependencies::new(crate_rename),
         export: attr.export,
         export_to: attr.export_to.clone(),
-        ts_name: name.to_owned(),
+        ts_name,
         concrete: attr.concrete.clone(),
         bound: attr.bound.clone(),
     })
@@ -30,7 +30,7 @@ pub(crate) fn type_override_struct(
 
 pub(crate) fn type_override_enum(
     attr: &EnumAttr,
-    name: &str,
+    ts_name: Expr,
     type_override: &str,
 ) -> Result<DerivedTS> {
     let crate_rename = attr.crate_rename();
@@ -43,7 +43,7 @@ pub(crate) fn type_override_enum(
         dependencies: Dependencies::new(crate_rename),
         export: attr.export,
         export_to: attr.export_to.clone(),
-        ts_name: name.to_owned(),
+        ts_name,
         concrete: attr.concrete.clone(),
         bound: attr.bound.clone(),
     })
diff --git a/macros/src/types/unit.rs b/macros/src/types/unit.rs
index 1a37954..0136635 100644
--- a/macros/src/types/unit.rs
+++ b/macros/src/types/unit.rs
@@ -1,5 +1,5 @@
 use quote::quote;
-use syn::Result;
+use syn::{Expr, Result};
 
 use crate::{
     attr::{ContainerAttr, StructAttr},
@@ -7,7 +7,7 @@ use crate::{
     DerivedTS,
 };
 
-pub(crate) fn empty_object(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
+pub(crate) fn empty_object(attr: &StructAttr, ts_name: Expr) -> Result<DerivedTS> {
     check_attributes(attr)?;
     let crate_rename = attr.crate_rename();
 
@@ -19,13 +19,13 @@ pub(crate) fn empty_object(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
         dependencies: Dependencies::new(crate_rename),
         export: attr.export,
         export_to: attr.export_to.clone(),
-        ts_name: name.to_owned(),
+        ts_name,
         concrete: attr.concrete.clone(),
         bound: attr.bound.clone(),
     })
 }
 
-pub(crate) fn empty_array(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
+pub(crate) fn empty_array(attr: &StructAttr, ts_name: Expr) -> Result<DerivedTS> {
     check_attributes(attr)?;
     let crate_rename = attr.crate_rename();
 
@@ -37,13 +37,13 @@ pub(crate) fn empty_array(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
         dependencies: Dependencies::new(crate_rename),
         export: attr.export,
         export_to: attr.export_to.clone(),
-        ts_name: name.to_owned(),
+        ts_name,
         concrete: attr.concrete.clone(),
         bound: attr.bound.clone(),
     })
 }
 
-pub(crate) fn null(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
+pub(crate) fn null(attr: &StructAttr, ts_name: Expr) -> Result<DerivedTS> {
     check_attributes(attr)?;
     let crate_rename = attr.crate_rename();
 
@@ -55,7 +55,7 @@ pub(crate) fn null(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
         dependencies: Dependencies::new(crate_rename),
         export: attr.export,
         export_to: attr.export_to.clone(),
-        ts_name: name.to_owned(),
+        ts_name,
         concrete: attr.concrete.clone(),
         bound: attr.bound.clone(),
     })
diff --git a/macros/src/utils.rs b/macros/src/utils.rs
index ceffdf8..7bd04b8 100644
--- a/macros/src/utils.rs
+++ b/macros/src/utils.rs
@@ -1,10 +1,10 @@
 use std::collections::HashMap;
 
-use proc_macro2::{Ident, TokenStream};
+use proc_macro2::{Ident, Span, TokenStream};
 use quote::quote;
 use syn::{
-    spanned::Spanned, Attribute, Error, Expr, ExprLit, GenericParam, Generics, Lit, Path, Result,
-    Type,
+    spanned::Spanned, Attribute, Error, Expr, ExprLit, GenericParam, Generics, Lit, LitStr, Path,
+    Result, Type,
 };
 
 use super::attr::{Attr, Serde};
@@ -313,3 +313,10 @@ pub fn format_generics(
     let comma_separated = quote!([#(#expanded_params),*].join(", "));
     quote!(format!("<{}>", #comma_separated))
 }
+
+pub fn make_string_literal(content: &str, span: Span) -> Expr {
+    Expr::Lit(ExprLit {
+        attrs: vec![],
+        lit: Lit::Str(LitStr::new(content, span)),
+    })
+}
diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs
index 03a40a6..420799d 100644
--- a/ts-rs/src/export.rs
+++ b/ts-rs/src/export.rs
@@ -342,7 +342,7 @@ fn generate_imports<T: TS + ?Sized + 'static>(
     let mut imports: BTreeMap<String, BTreeSet<&str>> = Default::default();
 
     for (_, dep) in deduplicated_deps {
-        let dep_path = out_dir.as_ref().join(dep.output_path);
+        let dep_path = out_dir.as_ref().join(&dep.output_path);
         let rel_path = import_path(&path, &dep_path)?;
 
         let is_same_file = path
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index 16f786d..49e65fb 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -208,7 +208,7 @@ mod tokio;
 ///   <br/><br/>
 ///
 /// - **`#[ts(as = "..")]`**  
-///   Overrides the type used in Typescript, using the provided Rust type instead.
+///   Overrides the type used in Typescript, using the provided Rust type instead.  
 ///   This is useful when you have a custom serializer and deserializer and don't want to implement `TS` manually
 ///   <br/><br/>
 ///
@@ -218,7 +218,8 @@ mod tokio;
 ///   <br/><br/>
 ///
 /// - **`#[ts(rename = "..")]`**  
-///   Sets the typescript name of the generated type
+///   Sets the typescript name of the generated type.  
+///   Also accepts expressions, e.g `#[ts(rename = module_path!().rsplit_once("::").unwrap().1)]`.
 ///   <br/><br/>
 ///
 /// - **`#[ts(rename_all = "..")]`**  
@@ -358,7 +359,8 @@ mod tokio;
 /// ### enum variant attributes
 ///
 /// - **`#[ts(rename = "..")]`**  
-///   Renames this variant. To rename all variants of an enum, see the container attribute `#[ts(rename_all = "..")]`.
+///   Renames this variant. To rename all variants of an enum, see the container attribute `#[ts(rename_all = "..")]`.  
+///   This attribute also accepts expressions, e.g `#[ts(rename = module_path!().rsplit_once("::").unwrap().1)]`.
 ///   <br/><br/>
 ///
 /// - **`#[ts(skip)]`**  
@@ -575,7 +577,7 @@ pub trait TS {
     ///
     /// If `T` cannot be exported (e.g because it's a primitive type), this function will return
     /// `None`.
-    fn output_path() -> Option<&'static Path> {
+    fn output_path() -> Option<PathBuf> {
         None
     }
 
@@ -613,10 +615,10 @@ pub struct Dependency {
     pub type_id: TypeId,
     /// Name of the type in TypeScript
     pub ts_name: String,
-    /// Path to where the type would be exported. By default a filename is derived from the types
+    /// Path to where the type would be exported. By default, a filename is derived from the types
     /// name, which can be customized with `#[ts(export_to = "..")]`.  
     /// This path does _not_ include a base directory.
-    pub output_path: &'static Path,
+    pub output_path: PathBuf,
 }
 
 impl Dependency {
@@ -743,7 +745,7 @@ macro_rules! impl_shadow {
             }
             fn decl() -> String { <$s as $crate::TS>::decl() }
             fn decl_concrete() -> String { <$s as $crate::TS>::decl_concrete() }
-            fn output_path() -> Option<&'static std::path::Path> { <$s as $crate::TS>::output_path() }
+            fn output_path() -> Option<std::path::PathBuf> { <$s as $crate::TS>::output_path() }
         }
     };
 }


Test patch:
diff --git a/ts-rs/tests/integration/issue_308.rs b/ts-rs/tests/integration/issue_308.rs
index 41fc5e7..524d028 100644
--- a/ts-rs/tests/integration/issue_308.rs
+++ b/ts-rs/tests/integration/issue_308.rs
@@ -20,7 +20,7 @@ trait Malicious {
     fn export_all() -> Result<(), ExportError> { unimplemented!() }
     fn export_all_to(out_dir: impl AsRef<Path>) -> Result<(), ExportError> { unimplemented!() }
     fn export_to_string() -> Result<String, ExportError> { unimplemented!() }
-    fn output_path() -> Option<&'static Path> { unimplemented!() }
+    fn output_path() -> Option<PathBuf> { unimplemented!() }
     fn default_output_path() -> Option<PathBuf> { unimplemented!() }
 }
 
diff --git a/ts-rs/tests/integration/issue_397.rs b/ts-rs/tests/integration/issue_397.rs
new file mode 100644
index 0000000..5bcfa0f
--- /dev/null
+++ b/ts-rs/tests/integration/issue_397.rs
@@ -0,0 +1,34 @@
+#[macro_export]
+macro_rules! get_model_name {
+    () => {{
+        let mut module = module_path!().rsplit_once("::").unwrap().1.to_owned();
+        module[0..1].make_ascii_uppercase();
+        format!("{module}Model")
+    }};
+}
+
+mod entities {
+    mod users {
+        use ts_rs::TS;
+
+        #[derive(TS)]
+        #[ts(export)]
+        #[ts(export_to = "issue_397/")]
+        #[ts(rename = {
+            let mut module = module_path!().rsplit_once("::").unwrap().1.to_owned();
+            module[0..1].make_ascii_uppercase();
+            format!("{module}Model")
+        })]
+        struct Model;
+    }
+
+    mod posts {
+        use ts_rs::TS;
+
+        #[derive(TS)]
+        #[ts(export)]
+        #[ts(export_to = "issue_397/")]
+        #[ts(rename = get_model_name!())]
+        struct Model;
+    }
+}
diff --git a/ts-rs/tests/integration/main.rs b/ts-rs/tests/integration/main.rs
index eeef6a4..c473395 100644
--- a/ts-rs/tests/integration/main.rs
+++ b/ts-rs/tests/integration/main.rs
@@ -29,6 +29,7 @@ mod issue_232;
 mod issue_308;
 mod issue_317;
 mod issue_338;
+mod issue_397;
 mod issue_70;
 mod issue_80;
 mod leading_colon;
diff --git a/ts-rs/tests/integration/merge_same_file_imports.rs b/ts-rs/tests/integration/merge_same_file_imports.rs
index 677861d..6022449 100644
--- a/ts-rs/tests/integration/merge_same_file_imports.rs
+++ b/ts-rs/tests/integration/merge_same_file_imports.rs
@@ -1,5 +1,6 @@
-use serde::de::Expected;
 use std::fmt::Write;
+
+use serde::de::Expected;
 use ts_rs::TS;
 
 #[derive(TS)]
diff --git a/ts-rs/tests/integration/struct_rename.rs b/ts-rs/tests/integration/struct_rename.rs
index 9976a5a..85217c1 100644
--- a/ts-rs/tests/integration/struct_rename.rs
+++ b/ts-rs/tests/integration/struct_rename.rs
@@ -77,3 +77,73 @@ struct RenameSerdeSpecialChar {
 fn serde_rename_special_char() {
     assert_eq!(RenameSerdeSpecialChar::inline(), r#"{ "a/b": number, }"#);
 }
+
+// struct-level renames
+
+#[derive(TS)]
+#[ts(export, export_to = "struct_rename/")]
+#[ts(rename = "RenamedWithStrLiteral")]
+enum WithStrLiteral {
+    A,
+    B,
+    C,
+}
+
+#[test]
+fn test_rename_with_str_literal() {
+    assert_eq!(
+        WithStrLiteral::decl(),
+        r#"type RenamedWithStrLiteral = "A" | "B" | "C";"#
+    )
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "struct_rename/")]
+#[ts(rename = format!("{}With{}", "Renamed", "StringExpression"))]
+enum WithStringExpression {
+    A,
+    B,
+    C,
+}
+
+#[test]
+fn test_rename_with_string_expression() {
+    assert_eq!(
+        WithStringExpression::decl(),
+        r#"type RenamedWithStringExpression = "A" | "B" | "C";"#
+    )
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "struct_rename/")]
+#[ts(rename = &"RenamedWithStrExpression")]
+enum WithStrExpression {
+    A,
+    B,
+    C,
+}
+
+#[test]
+fn test_rename_with_str_expression() {
+    assert_eq!(
+        WithStrExpression::decl(),
+        r#"type RenamedWithStrExpression = "A" | "B" | "C";"#
+    )
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "struct_rename/")]
+#[ts(rename = format!("i_am_inside_module_{}", module_path!().rsplit_once("::").unwrap().1))]
+enum RenameUsingModuleName {
+    A,
+    B,
+    C,
+}
+
+#[test]
+fn test_rename_using_module_name() {
+    assert_eq!(
+        RenameUsingModuleName::decl(),
+        r#"type i_am_inside_module_struct_rename = "A" | "B" | "C";"#
+    )
+}
diff --git a/ts-rs/tests/integration/top_level_type_as.rs b/ts-rs/tests/integration/top_level_type_as.rs
index c51949e..c62938f 100644
--- a/ts-rs/tests/integration/top_level_type_as.rs
+++ b/ts-rs/tests/integration/top_level_type_as.rs
@@ -1,6 +1,7 @@
+use std::collections::HashMap;
+
 #[cfg(feature = "serde-json-impl")]
 use serde_json::Value as JsonValue;
-use std::collections::HashMap;
 use ts_rs::TS;
 
 #[derive(TS)]
diff --git a/ts-rs/tests/integration/union.rs b/ts-rs/tests/integration/union.rs
index 16fb57f..78c05c5 100644
--- a/ts-rs/tests/integration/union.rs
+++ b/ts-rs/tests/integration/union.rs
@@ -9,6 +9,7 @@ enum SimpleEnum {
     A,
     B,
     C,
+    r#D,
 }
 
 #[test]
@@ -23,6 +24,6 @@ fn test_empty() {
 fn test_simple_enum() {
     assert_eq!(
         SimpleEnum::decl(),
-        r#"type SimpleEnum = "asdf" | "B" | "C";"#
+        r#"type SimpleEnum = "asdf" | "B" | "C" | "D";"#
     )
 }
diff --git a/ts-rs/tests/integration/union_rename.rs b/ts-rs/tests/integration/union_rename.rs
index f26ee9b..69dbf90 100644
--- a/ts-rs/tests/integration/union_rename.rs
+++ b/ts-rs/tests/integration/union_rename.rs
@@ -8,7 +8,9 @@ use ts_rs::TS;
 enum RenamedEnum {
     #[ts(rename = "ASDF")]
     A,
+    #[ts(rename = &"BB")]
     B,
+    #[ts(rename = "C".repeat(2))]
     C,
 }
 
@@ -16,6 +18,57 @@ enum RenamedEnum {
 fn test_simple_enum() {
     assert_eq!(
         RenamedEnum::decl(),
-        r#"type SimpleEnum = "ASDF" | "b" | "c";"#
+        r#"type SimpleEnum = "ASDF" | "BB" | "CC";"#
+    )
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "union_rename/")]
+#[ts(rename = format!("{}With{}", "Renamed", "StringExpression"))]
+enum WithStringExpression {
+    A,
+    B,
+    C,
+}
+
+#[test]
+fn test_rename_with_string_expression() {
+    assert_eq!(
+        WithStringExpression::decl(),
+        r#"type RenamedWithStringExpression = "A" | "B" | "C";"#
+    )
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "union_rename/")]
+#[ts(rename = &"RenamedWithStrExpression")]
+enum WithStrExpression {
+    A,
+    B,
+    C,
+}
+
+#[test]
+fn test_rename_with_str_expression() {
+    assert_eq!(
+        WithStrExpression::decl(),
+        r#"type RenamedWithStrExpression = "A" | "B" | "C";"#
+    )
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "union_rename/")]
+#[ts(rename = format!("i_am_inside_module_{}", module_path!().rsplit_once("::").unwrap().1))]
+enum RenameUsingModuleName {
+    A,
+    B,
+    C,
+}
+
+#[test]
+fn test_rename_using_module_name() {
+    assert_eq!(
+        RenameUsingModuleName::decl(),
+        r#"type i_am_inside_module_union_rename = "A" | "B" | "C";"#
     )
 }