Repository: Aleph-Alpha/ts-rs
Pull request #274 (2024-03-19 16:49:05)

Option to rename source of `TS` trait
## Goal

The goal of this PR is to provide an option which allows for re-exports of the `ts_rs::TS` trait.  
Similar to serde's `#[serde(crate = "other_serde")]` attribute.

Motivation is us evaluating `ts-rs` for generating TypeScript types directly without an intermediate JSON Schema step. We generate the derive annotations via an own procedural macro, which is why we need to re-export the crate.

## Changes

This PR introduces the new `crate_rename` attribute to the derive macro.  
The invocation looks similar to this:

```rust
#[derive(TS)]
#[ts(crate_rename = "::my_crate::ts_rs")]
```

## 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.

(Will add the documentation if this is actually a feature that would be accepted)


aumetra: This PR is missing like 90% of all renames. So far from complete.  
Will complete and clean it up once it is clear whether this would be accepted.
escritorio-gustavo: Hey, thanks for the PR! I've left a couple of changes I'd like to see here, apart from those, I don't see an issue with adding this feature.
aumetra: > I've left a couple of changes I'd like to see here, apart from those, I don't see an issue with adding this feature

Great! Then I will check on how to sync the same crate name across all the different code paths and will take care of your requested changes.
NyxCode: I'm open to adding this as well.  👍
NyxCode: Interestingly, this would come in handy in #276, allowing to `#[derive(TS)]` within `ts-rs` by using `#[ts(crate = "")]`
aumetra: Oh, you got there before I did. Cool!  
For a test, do you think introducing a separate test crate, such as `ts-test-rename` would be sensible and then importing it as a dependency via

```toml
ts-rename = { package = "ts-rs", path = "../ts-rs" }
```

That way you can be 100% sure the rename code works. The test I made is a bit.. lackluster 
escritorio-gustavo: > Oh, you got there before I did. Cool! For a test, do you think introducing a separate test crate, such as `ts-test-rename` would be sensible and then importing it as a dependency via
> 
> ```toml
> ts-rename = { package = "ts-rs", path = "../ts-rs" }
> ```
> 
> That way you can be 100% sure the rename code works. The test I made is a bit.. lackluster

This is a very good idea! You can do that inside the `e2e` directory

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 091cd7b..e474cd1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,10 @@
 # master
+### Breaking
+### Features
+- Add `#[ts(crate = "..")]` to allow usage of `#[derive(TS)]` from other proc-macro crates  ([#274](https://github.com/Aleph-Alpha/ts-rs/pull/274))
+### Fixes
+
+# v8.0.0
 
 ### Breaking
 
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 485516b..21910e2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -22,4 +22,11 @@ There is nothing special going on here - just run `cargo build`.
 To run the test suite, just run `cargo test` in the root directory.  
 
 ### Formatting
-To ensure proper formatting, please run `cargo +nightly fmt`.
+To ensure proper formatting, please make sure you have the nigthly toolchain installed.
+After that, in the project's root directory, create a file called `.git/hooks/pre-commit` without a file extension and add the following two lines:
+```sh
+#!/bin/sh
+cargo +nightly fmt
+```
+
+This will make sure your files are formatted before your commit is sent, so you don't have to manually run `cargo +nightly fmt`
diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs
index 232d61f..e6e4fdc 100644
--- a/macros/src/attr/enum.rs
+++ b/macros/src/attr/enum.rs
@@ -1,16 +1,16 @@
 use std::collections::HashMap;
 
-use syn::{Attribute, Ident, Result, Type, WherePredicate};
+use syn::{parse_quote, Attribute, Ident, Path, Result, Type, WherePredicate};
 
+use super::{parse_assign_from_str, parse_bound};
 use crate::{
     attr::{parse_assign_inflection, parse_assign_str, parse_concrete, Inflection},
     utils::{parse_attrs, parse_docs},
 };
 
-use super::parse_bound;
-
 #[derive(Default)]
 pub struct EnumAttr {
+    crate_rename: Option<Path>,
     pub rename_all: Option<Inflection>,
     pub rename_all_fields: Option<Inflection>,
     pub rename: Option<String>,
@@ -61,9 +61,16 @@ impl EnumAttr {
         Ok(result)
     }
 
+    pub fn crate_rename(&self) -> Path {
+        self.crate_rename
+            .clone()
+            .unwrap_or_else(|| parse_quote!(::ts_rs))
+    }
+
     fn merge(
         &mut self,
         EnumAttr {
+            crate_rename,
             rename_all,
             rename_all_fields,
             rename,
@@ -77,6 +84,7 @@ impl EnumAttr {
             bound,
         }: EnumAttr,
     ) {
+        self.crate_rename = self.crate_rename.take().or(crate_rename);
         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);
@@ -87,15 +95,21 @@ impl EnumAttr {
         self.export_to = self.export_to.take().or(export_to);
         self.docs = docs;
         self.concrete.extend(concrete);
-        self.bound = self.bound
+        self.bound = self
+            .bound
             .take()
-            .map(|b| b.into_iter().chain(bound.clone().unwrap_or_default()).collect())
+            .map(|b| {
+                b.into_iter()
+                    .chain(bound.clone().unwrap_or_default())
+                    .collect()
+            })
             .or(bound);
     }
 }
 
 impl_parse! {
     EnumAttr(input, out) {
+        "crate" => out.crate_rename = Some(parse_assign_from_str(input)?),
         "rename" => out.rename = Some(parse_assign_str(input)?),
         "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?),
         "rename_all_fields" => out.rename_all_fields = Some(parse_assign_inflection(input)?),
diff --git a/macros/src/attr/mod.rs b/macros/src/attr/mod.rs
index fcf9fbd..b80ea7b 100644
--- a/macros/src/attr/mod.rs
+++ b/macros/src/attr/mod.rs
@@ -1,11 +1,12 @@
-use std::{collections::HashMap, convert::TryFrom};
+use std::collections::HashMap;
 
 pub use field::*;
 pub use r#enum::*;
 pub use r#struct::*;
 use syn::{
     parse::{Parse, ParseStream},
-    Error, Lit, Result, Token, WherePredicate, punctuated::Punctuated,
+    punctuated::Punctuated,
+    Error, Lit, Result, Token, WherePredicate,
 };
 pub use variant::*;
 
@@ -136,7 +137,7 @@ fn parse_bound(input: ParseStream) -> Result<Vec<WherePredicate>> {
             let parser = Punctuated::<WherePredicate, Token![,]>::parse_terminated;
 
             Ok(string.parse_with(parser)?.into_iter().collect())
-        },
+        }
         other => Err(Error::new(other.span(), "expected string")),
     }
 }
diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs
index 6ed3bf9..ce58a0e 100644
--- a/macros/src/attr/struct.rs
+++ b/macros/src/attr/struct.rs
@@ -1,16 +1,17 @@
-use std::{collections::HashMap, convert::TryFrom};
+use std::collections::HashMap;
 
-use syn::{Attribute, Ident, Result, Type, WherePredicate};
+use syn::{parse_quote, Attribute, Ident, Path, Result, Type, WherePredicate};
 
+use super::{parse_assign_from_str, parse_bound, parse_concrete};
+use crate::attr::EnumAttr;
 use crate::{
-    attr::{parse_assign_str, parse_concrete, Inflection, VariantAttr},
+    attr::{parse_assign_str, Inflection, VariantAttr},
     utils::{parse_attrs, parse_docs},
 };
 
-use super::parse_bound;
-
 #[derive(Default, Clone)]
 pub struct StructAttr {
+    crate_rename: Option<Path>,
     pub rename_all: Option<Inflection>,
     pub rename: Option<String>,
     pub export_to: Option<String>,
@@ -38,9 +39,26 @@ impl StructAttr {
         Ok(result)
     }
 
+    pub fn from_variant(enum_attr: &EnumAttr, variant_attr: &VariantAttr) -> Self {
+        Self {
+            crate_rename: Some(enum_attr.crate_rename()),
+            rename: variant_attr.rename.clone(),
+            rename_all: variant_attr.rename_all,
+            // inline and skip are not supported on StructAttr
+            ..Self::default()
+        }
+    }
+
+    pub fn crate_rename(&self) -> Path {
+        self.crate_rename
+            .clone()
+            .unwrap_or_else(|| parse_quote!(::ts_rs))
+    }
+
     fn merge(
         &mut self,
         StructAttr {
+            crate_rename,
             rename_all,
             rename,
             export,
@@ -51,6 +69,7 @@ impl StructAttr {
             bound,
         }: StructAttr,
     ) {
+        self.crate_rename = self.crate_rename.take().or(crate_rename);
         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);
@@ -58,30 +77,21 @@ impl StructAttr {
         self.tag = self.tag.take().or(tag);
         self.docs = docs;
         self.concrete.extend(concrete);
-        self.bound = self.bound
+        self.bound = self
+            .bound
             .take()
-            .map(|b| b.into_iter().chain(bound.clone().unwrap_or_default()).collect())
+            .map(|b| {
+                b.into_iter()
+                    .chain(bound.clone().unwrap_or_default())
+                    .collect()
+            })
             .or(bound);
     }
 }
 
-impl From<VariantAttr> for StructAttr {
-    fn from(
-        VariantAttr {
-            rename, rename_all, ..
-        }: VariantAttr,
-    ) -> Self {
-        Self {
-            rename,
-            rename_all,
-            // inline and skip are not supported on StructAttr
-            ..Self::default()
-        }
-    }
-}
-
 impl_parse! {
     StructAttr(input, out) {
+        "crate" => out.crate_rename = Some(parse_assign_from_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)?),
         "tag" => out.tag = Some(parse_assign_str(input)?),
diff --git a/macros/src/deps.rs b/macros/src/deps.rs
index 630fda7..b99c49f 100644
--- a/macros/src/deps.rs
+++ b/macros/src/deps.rs
@@ -1,26 +1,35 @@
 use proc_macro2::TokenStream;
 use quote::{quote, ToTokens};
-use syn::Type;
+use syn::{Path, Type};
 
-#[derive(Default)]
 pub struct Dependencies {
     dependencies: Vec<TokenStream>,
+    crate_rename: Path,
     pub types: Vec<Type>,
 }
 
 impl Dependencies {
+    pub fn new(crate_rename: Path) -> Self {
+        Self {
+            dependencies: Vec::default(),
+            crate_rename,
+            types: Vec::default(),
+        }
+    }
     /// 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 ::ts_rs::TS>::dependency_types())]);
+            .push(quote![.extend(<#ty as #crate_rename::TS>::dependency_types())]);
         self.types.push(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 ::ts_rs::TS>::generics())
+            .extend(<#ty as #crate_rename::TS>::generics())
         ]);
         self.types.push(ty.clone());
     }
@@ -33,9 +42,10 @@ impl Dependencies {
 
 impl ToTokens for Dependencies {
     fn to_tokens(&self, tokens: &mut TokenStream) {
+        let crate_rename = &self.crate_rename;
         let lines = &self.dependencies;
         tokens.extend(quote![{
-            use ::ts_rs::typelist::TypeList;
+            use #crate_rename::typelist::TypeList;
             ()#(#lines)*
         }])
     }
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index c61e8b0..5370e46 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -6,8 +6,8 @@ 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, Result,
-    Type, TypeArray, TypeParam, TypeParen, TypePath, TypeReference, TypeSlice, TypeTuple,
+    parse_quote, spanned::Spanned, ConstParam, GenericParam, Generics, Item, LifetimeParam, Path,
+    Result, Type, TypeArray, TypeParam, TypeParen, TypePath, TypeReference, TypeSlice, TypeTuple,
     WhereClause, WherePredicate,
 };
 
@@ -20,6 +20,7 @@ mod deps;
 mod types;
 
 struct DerivedTS {
+    crate_rename: Path,
     ts_name: String,
     docs: String,
     inline: TokenStream,
@@ -59,14 +60,17 @@ impl DerivedTS {
             docs => Some(quote!(const DOCS: Option<&'static str> = Some(#docs);)),
         };
 
+        let crate_rename = self.crate_rename.clone();
+
         let ident = self.ts_name.clone();
         let impl_start = generate_impl_block_header(
+            &crate_rename,
             &rust_ty,
             &generics,
             self.bound.as_deref(),
             &self.dependencies,
         );
-        let assoc_type = generate_assoc_type(&rust_ty, &generics, &self.concrete);
+        let assoc_type = generate_assoc_type(&rust_ty, &crate_rename, &generics, &self.concrete);
         let name = self.generate_name_fn(&generics);
         let inline = self.generate_inline_fn();
         let decl = self.generate_decl_fn(&rust_ty, &generics);
@@ -89,7 +93,7 @@ impl DerivedTS {
                 #output_path_fn
 
                 #[allow(clippy::unused_unit)]
-                fn dependency_types() -> impl ::ts_rs::typelist::TypeList
+                fn dependency_types() -> impl #crate_rename::typelist::TypeList
                 where
                     Self: 'static,
                 {
@@ -105,11 +109,12 @@ impl DerivedTS {
     /// parameters.
     fn name_with_generics(&self, generics: &Generics) -> TokenStream {
         let name = &self.ts_name;
+        let crate_rename = &self.crate_rename;
         let mut generics_ts_names = generics
             .type_params()
             .filter(|ty| !self.concrete.contains_key(&ty.ident))
             .map(|ty| &ty.ident)
-            .map(|generic| quote!(<#generic as ::ts_rs::TS>::name()))
+            .map(|generic| quote!(<#generic as #crate_rename::TS>::name()))
             .peekable();
 
         if generics_ts_names.peek().is_some() {
@@ -166,13 +171,14 @@ impl DerivedTS {
             "export_bindings_{}",
             rust_ty.to_string().to_lowercase().replace("r#", "")
         );
+        let crate_rename = &self.crate_rename;
         let generic_params = generics
             .type_params()
             .map(|ty| match self.concrete.get(&ty.ident) {
-                None => quote! { ::ts_rs::Dummy },
+                None => quote! { #crate_rename::Dummy },
                 Some(ty) => quote! { #ty },
             });
-        let ty = quote!(<#rust_ty<#(#generic_params),*> as ::ts_rs::TS>);
+        let ty = quote!(<#rust_ty<#(#generic_params),*> as #crate_rename::TS>);
 
         quote! {
             #[cfg(test)]
@@ -184,17 +190,18 @@ impl DerivedTS {
     }
 
     fn generate_generics_fn(&self, generics: &Generics) -> TokenStream {
+        let crate_rename = &self.crate_rename;
         let generics = generics
             .type_params()
             .filter(|ty| !self.concrete.contains_key(&ty.ident))
-            .map(|TypeParam { ident, .. }| quote![.push::<#ident>().extend(<#ident as ::ts_rs::TS>::generics())]);
+            .map(|TypeParam { ident, .. }| quote![.push::<#ident>().extend(<#ident as #crate_rename::TS>::generics())]);
         quote! {
             #[allow(clippy::unused_unit)]
-            fn generics() -> impl ::ts_rs::typelist::TypeList
+            fn generics() -> impl #crate_rename::typelist::TypeList
             where
                 Self: 'static,
             {
-                use ::ts_rs::typelist::TypeList;
+                use #crate_rename::typelist::TypeList;
                 ()#(#generics)*
             }
         }
@@ -245,8 +252,14 @@ impl DerivedTS {
     /// them with the dummy types generated by `generate_generic_types()`.
     fn generate_decl_fn(&mut self, rust_ty: &Ident, generics: &Generics) -> TokenStream {
         let name = &self.ts_name;
+        let crate_rename = &self.crate_rename;
         let generic_types = self.generate_generic_types(generics);
-        let ts_generics = format_generics(&mut self.dependencies, generics, &self.concrete);
+        let ts_generics = format_generics(
+            &mut self.dependencies,
+            crate_rename,
+            generics,
+            &self.concrete,
+        );
 
         use GenericParam as G;
         // These are the generic parameters we'll be using.
@@ -270,7 +283,7 @@ impl DerivedTS {
             }
             fn decl() -> String {
                 #generic_types
-                let inline = <#rust_ty<#(#generic_idents,)*> as ::ts_rs::TS>::inline();
+                let inline = <#rust_ty<#(#generic_idents,)*> as #crate_rename::TS>::inline();
                 let generics = #ts_generics;
                 format!("type {}{generics} = {inline};", #name)
             }
@@ -280,6 +293,7 @@ impl DerivedTS {
 
 fn generate_assoc_type(
     rust_ty: &Ident,
+    crate_rename: &Path,
     generics: &Generics,
     concrete: &HashMap<Ident, Type>,
 ) -> TokenStream {
@@ -287,7 +301,7 @@ fn generate_assoc_type(
 
     let generics_params = generics.params.iter().map(|x| match x {
         G::Type(ty) => match concrete.get(&ty.ident) {
-            None => quote! { ::ts_rs::Dummy },
+            None => quote! { #crate_rename::Dummy },
             Some(ty) => quote! { #ty },
         },
         G::Const(ConstParam { ident, .. }) => quote! { #ident },
@@ -299,6 +313,7 @@ fn generate_assoc_type(
 
 // generate start of the `impl TS for #ty` block, up to (excluding) the open brace
 fn generate_impl_block_header(
+    crate_rename: &Path,
     ty: &Ident,
     generics: &Generics,
     bounds: Option<&[WherePredicate]>,
@@ -335,15 +350,19 @@ fn generate_impl_block_header(
     let where_bound = match bounds {
         Some(bounds) => quote! { where #(#bounds),* },
         None => {
-            let bounds = generate_where_clause(generics, dependencies);
+            let bounds = generate_where_clause(crate_rename, generics, dependencies);
             quote! { #bounds }
         }
     };
 
-    quote!(impl <#(#params),*> ::ts_rs::TS for #ty <#(#type_args),*> #where_bound)
+    quote!(impl <#(#params),*> #crate_rename::TS for #ty <#(#type_args),*> #where_bound)
 }
 
-fn generate_where_clause(generics: &Generics, dependencies: &Dependencies) -> WhereClause {
+fn generate_where_clause(
+    crate_rename: &Path,
+    generics: &Generics,
+    dependencies: &Dependencies,
+) -> WhereClause {
     let used_types = {
         let is_type_param = |id: &Ident| generics.type_params().any(|p| &p.ident == id);
 
@@ -356,7 +375,7 @@ fn generate_where_clause(generics: &Generics, dependencies: &Dependencies) -> Wh
 
     let existing = generics.where_clause.iter().flat_map(|w| &w.predicates);
     parse_quote! {
-        where #(#existing,)* #(#used_types: ::ts_rs::TS),*
+        where #(#existing,)* #(#used_types: #crate_rename::TS),*
     }
 }
 
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index f35cb98..130b4aa 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -11,6 +11,8 @@ use crate::{
 pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
     let enum_attr: EnumAttr = EnumAttr::from_attrs(&s.attrs)?;
 
+    let crate_rename = enum_attr.crate_rename();
+
     let name = match &enum_attr.rename {
         Some(existing) => existing.clone(),
         None => s.ident.to_string(),
@@ -22,11 +24,12 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
 
     if s.variants.is_empty() {
         return Ok(DerivedTS {
+            crate_rename: crate_rename.clone(),
             ts_name: name,
             docs: enum_attr.docs,
             inline: quote!("never".to_owned()),
             inline_flattened: None,
-            dependencies: Dependencies::default(),
+            dependencies: Dependencies::new(crate_rename),
             export: enum_attr.export,
             export_to: enum_attr.export_to,
             concrete: enum_attr.concrete,
@@ -35,7 +38,7 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
     }
 
     let mut formatted_variants = Vec::new();
-    let mut dependencies = Dependencies::default();
+    let mut dependencies = Dependencies::new(crate_rename.clone());
     for variant in &s.variants {
         format_variant(
             &mut formatted_variants,
@@ -46,6 +49,7 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
     }
 
     Ok(DerivedTS {
+        crate_rename,
         inline: quote!([#(#formatted_variants),*].join(" | ")),
         inline_flattened: Some(quote!(
             format!("({})", [#(#formatted_variants),*].join(" | "))
@@ -66,6 +70,7 @@ fn format_variant(
     enum_attr: &EnumAttr,
     variant: &Variant,
 ) -> syn::Result<()> {
+    let crate_rename = enum_attr.crate_rename();
     let variant_attr = VariantAttr::new(&variant.attrs, enum_attr)?;
 
     if variant_attr.skip {
@@ -79,8 +84,9 @@ fn format_variant(
         (None, Some(rn)) => rn.apply(&variant.ident.to_string()),
     };
 
+    let struct_attr = StructAttr::from_variant(enum_attr, &variant_attr);
     let variant_type = types::type_def(
-        &StructAttr::from(variant_attr),
+        &struct_attr,
         // since we are generating the variant as a struct, it doesn't have a name
         &format_ident!("_"),
         &variant.fields,
@@ -120,11 +126,11 @@ fn format_variant(
                         }
                         (Some(type_override), None) => quote! { #type_override },
                         (None, Some(type_as)) => {
-                            quote!(<#type_as as ::ts_rs::TS>::name())
+                            quote!(<#type_as as #crate_rename::TS>::name())
                         }
                         (None, None) => {
                             let ty = &unnamed.unnamed[0].ty;
-                            quote!(<#ty as ::ts_rs::TS>::name())
+                            quote!(<#ty as #crate_rename::TS>::name())
                         }
                     };
 
@@ -171,11 +177,11 @@ fn format_variant(
                             }
                             (Some(type_override), None) => quote! { #type_override },
                             (None, Some(type_as)) => {
-                                quote!(<#type_as as ::ts_rs::TS>::name())
+                                quote!(<#type_as as #crate_rename::TS>::name())
                             }
                             (None, None) => {
                                 let ty = &unnamed.unnamed[0].ty;
-                                quote!(<#ty as ::ts_rs::TS>::name())
+                                quote!(<#ty as #crate_rename::TS>::name())
                             }
                         };
 
@@ -198,11 +204,13 @@ 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();
+    let crate_rename = enum_attr.crate_rename();
     DerivedTS {
+        crate_rename: crate_rename.clone(),
         inline: quote!("never".to_owned()),
         docs: enum_attr.docs,
         inline_flattened: None,
-        dependencies: Dependencies::default(),
+        dependencies: Dependencies::new(crate_rename),
         export: enum_attr.export,
         export_to: enum_attr.export_to,
         ts_name: name,
diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs
index bd86ab6..c84fa6c 100644
--- a/macros/src/types/named.rs
+++ b/macros/src/types/named.rs
@@ -1,6 +1,8 @@
 use proc_macro2::TokenStream;
-use quote::{quote};
-use syn::{spanned::Spanned, Field, FieldsNamed, GenericArgument, PathArguments, Result, Type};
+use quote::quote;
+use syn::{
+    spanned::Spanned, Field, FieldsNamed, GenericArgument, Path, PathArguments, Result, Type,
+};
 
 use crate::{
     attr::{FieldAttr, Inflection, Optional, StructAttr},
@@ -10,9 +12,11 @@ use crate::{
 };
 
 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::default();
+    let mut dependencies = Dependencies::new(crate_rename.clone());
 
     if let Some(tag) = &attr.tag {
         let formatted = format!("{}: \"{}\",", tag, name);
@@ -23,6 +27,7 @@ pub(crate) fn named(attr: &StructAttr, name: &str, fields: &FieldsNamed) -> Resu
 
     for field in &fields.named {
         format_field(
+            &crate_rename,
             &mut formatted_fields,
             &mut flattened_fields,
             &mut dependencies,
@@ -43,6 +48,7 @@ pub(crate) fn named(attr: &StructAttr, name: &str, fields: &FieldsNamed) -> Resu
     };
 
     Ok(DerivedTS {
+        crate_rename,
         // the `replace` combines `{ ... } & { ... }` into just one `{ ... }`. Not necessary, but it
         // results in simpler type definitions.
         inline: quote!(#inline.replace(" } & { ", " ")),
@@ -68,6 +74,7 @@ pub(crate) fn named(attr: &StructAttr, name: &str, fields: &FieldsNamed) -> Resu
 // 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,
@@ -124,7 +131,7 @@ fn format_field(
             _ => {}
         }
 
-        flattened_fields.push(quote!(<#ty as ::ts_rs::TS>::inline_flattened()));
+        flattened_fields.push(quote!(<#ty as #crate_rename::TS>::inline_flattened()));
         dependencies.append_from(ty);
         return Ok(());
     }
@@ -132,10 +139,10 @@ fn format_field(
     let formatted_ty = type_override.map(|t| quote!(#t)).unwrap_or_else(|| {
         if inline {
             dependencies.append_from(ty);
-            quote!(<#ty as ::ts_rs::TS>::inline())
+            quote!(<#ty as #crate_rename::TS>::inline())
         } else {
             dependencies.push(ty);
-            quote!(<#ty as ::ts_rs::TS>::name())
+            quote!(<#ty as #crate_rename::TS>::name())
         }
     });
     let field_name = to_ts_ident(field.ident.as_ref().unwrap());
diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs
index 759ebec..df198da 100644
--- a/macros/src/types/newtype.rs
+++ b/macros/src/types/newtype.rs
@@ -1,4 +1,4 @@
-use quote::{quote};
+use quote::quote;
 use syn::{FieldsUnnamed, Result};
 
 use crate::{
@@ -26,6 +26,8 @@ pub(crate) fn newtype(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) ->
         docs: _,
     } = FieldAttr::from_attrs(&inner.attrs)?;
 
+    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),
@@ -44,7 +46,7 @@ pub(crate) fn newtype(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) ->
 
     let inner_ty = type_as.as_ref().unwrap_or(&inner.ty).clone();
 
-    let mut dependencies = Dependencies::default();
+    let mut dependencies = Dependencies::new(crate_rename.clone());
 
     match (type_override.is_none(), inline) {
         (false, _) => (),
@@ -54,11 +56,12 @@ pub(crate) fn newtype(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) ->
 
     let inline_def = match type_override {
         Some(ref o) => quote!(#o.to_owned()),
-        None if inline => quote!(<#inner_ty as ::ts_rs::TS>::inline()),
-        None => quote!(<#inner_ty as ::ts_rs::TS>::name()),
+        None if inline => quote!(<#inner_ty as #crate_rename::TS>::inline()),
+        None => quote!(<#inner_ty as #crate_rename::TS>::name()),
     };
 
     Ok(DerivedTS {
+        crate_rename,
         inline: inline_def,
         inline_flattened: None,
         docs: attr.docs.clone(),
diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs
index 4aa0f40..e85ca1a 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, Result};
+use quote::quote;
+use syn::{Field, FieldsUnnamed, Path, Result};
 
 use crate::{
     attr::{FieldAttr, StructAttr},
@@ -16,13 +16,20 @@ pub(crate) fn tuple(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) -> Re
         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::default();
+    let mut dependencies = Dependencies::new(crate_rename.clone());
     for field in &fields.unnamed {
-        format_field(&mut formatted_fields, &mut dependencies, field)?;
+        format_field(
+            &crate_rename,
+            &mut formatted_fields,
+            &mut dependencies,
+            field,
+        )?;
     }
 
     Ok(DerivedTS {
+        crate_rename,
         inline: quote! {
             format!(
                 "[{}]",
@@ -41,6 +48,7 @@ pub(crate) fn tuple(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) -> Re
 }
 
 fn format_field(
+    crate_rename: &Path,
     formatted_fields: &mut Vec<TokenStream>,
     dependencies: &mut Dependencies,
     field: &Field,
@@ -80,8 +88,8 @@ fn format_field(
 
     formatted_fields.push(match type_override {
         Some(ref o) => quote!(#o.to_owned()),
-        None if inline => quote!(<#ty as ::ts_rs::TS>::inline()),
-        None => quote!(<#ty as ::ts_rs::TS>::name()),
+        None if inline => quote!(<#ty as #crate_rename::TS>::inline()),
+        None => quote!(<#ty as #crate_rename::TS>::name()),
     });
 
     match (inline, type_override) {
diff --git a/macros/src/types/unit.rs b/macros/src/types/unit.rs
index 88cad88..6a3c0c3 100644
--- a/macros/src/types/unit.rs
+++ b/macros/src/types/unit.rs
@@ -5,12 +5,14 @@ use crate::{attr::StructAttr, deps::Dependencies, DerivedTS};
 
 pub(crate) fn empty_object(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
     check_attributes(attr)?;
+    let crate_rename = attr.crate_rename();
 
     Ok(DerivedTS {
+        crate_rename: crate_rename.clone(),
         inline: quote!("Record<string, never>".to_owned()),
         inline_flattened: None,
         docs: attr.docs.clone(),
-        dependencies: Dependencies::default(),
+        dependencies: Dependencies::new(crate_rename),
         export: attr.export,
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
@@ -21,12 +23,14 @@ pub(crate) fn empty_object(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
 
 pub(crate) fn empty_array(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
     check_attributes(attr)?;
+    let crate_rename = attr.crate_rename();
 
     Ok(DerivedTS {
+        crate_rename: crate_rename.clone(),
         inline: quote!("never[]".to_owned()),
         inline_flattened: None,
         docs: attr.docs.clone(),
-        dependencies: Dependencies::default(),
+        dependencies: Dependencies::new(crate_rename),
         export: attr.export,
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
@@ -37,12 +41,14 @@ pub(crate) fn empty_array(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
 
 pub(crate) fn null(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
     check_attributes(attr)?;
+    let crate_rename = attr.crate_rename();
 
     Ok(DerivedTS {
+        crate_rename: crate_rename.clone(),
         inline: quote!("null".to_owned()),
         inline_flattened: None,
         docs: attr.docs.clone(),
-        dependencies: Dependencies::default(),
+        dependencies: Dependencies::new(crate_rename),
         export: attr.export,
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
diff --git a/macros/src/utils.rs b/macros/src/utils.rs
index c7c4bee..01a9953 100644
--- a/macros/src/utils.rs
+++ b/macros/src/utils.rs
@@ -1,10 +1,10 @@
-use std::{collections::HashMap, convert::TryFrom};
+use std::collections::HashMap;
 
 use proc_macro2::{Ident, TokenStream};
 use quote::quote;
 use syn::{
-    spanned::Spanned, Attribute, Error, Expr, ExprLit, GenericParam, Generics, Lit, Meta, Result,
-    Type,
+    spanned::Spanned, Attribute, Error, Expr, ExprLit, GenericParam, Generics, Lit, Meta, Path,
+    Result, Type,
 };
 
 use crate::deps::Dependencies;
@@ -226,6 +226,7 @@ mod warning {
 /// If a default type arg is encountered, it will be added to the dependencies.
 pub fn format_generics(
     deps: &mut Dependencies,
+    crate_rename: &Path,
     generics: &Generics,
     concrete: &HashMap<Ident, Type>,
 ) -> TokenStream {
@@ -241,7 +242,7 @@ pub fn format_generics(
                 if let Some(default) = &type_param.default {
                     deps.push(default);
                     Some(quote!(
-                        format!("{} = {}", #ty, <#default as ::ts_rs::TS>::name())
+                        format!("{} = {}", #ty, <#default as #crate_rename::TS>::name())
                     ))
                 } else {
                     Some(quote!(#ty.to_owned()))
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index 69d6ed6..bb2ae31 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -177,6 +177,10 @@ pub mod typelist;
 /// ### container attributes
 /// attributes applicable for both structs and enums
 ///
+/// - **`#[ts(crate = "..")]`**
+///   Generates code which references the module passed to it instead of defaulting to `::ts_rs`
+///   This is useful for cases where you have to re-export the crate.
+///
 /// - **`#[ts(export)]`**  
 ///   Generates a test which will export the type, by default to `bindings/<name>.ts` when running
 ///   `cargo test`. The default base directory can be overridden with the `TS_RS_EXPORT_DIR` environment variable.
@@ -863,15 +867,15 @@ impl<K: TS, V: TS, H> TS for HashMap<K, V, H> {
     type WithoutGenerics = HashMap<Dummy, Dummy>;
 
     fn ident() -> String {
-        "Record".to_owned()
+        panic!()
     }
 
     fn name() -> String {
-        format!("Record<{}, {}>", K::name(), V::name())
+        format!("{{ [key: {}]: {} }}", K::name(), V::name())
     }
 
     fn inline() -> String {
-        format!("Record<{}, {}>", K::inline(), V::inline())
+        format!("{{ [key: {}]: {} }}", K::inline(), V::inline())
     }
 
     fn dependency_types() -> impl TypeList


Test patch:
diff --git a/e2e/workspace/Cargo.toml b/e2e/workspace/Cargo.toml
index d06ca93..471e90b 100644
--- a/e2e/workspace/Cargo.toml
+++ b/e2e/workspace/Cargo.toml
@@ -1,2 +1,3 @@
 [workspace]
-members = ["crate1", "crate2", "parent"]
\ No newline at end of file
+members = ["crate1", "crate2", "parent", "renamed"]
+resolver = "2"
diff --git a/e2e/workspace/renamed/Cargo.toml b/e2e/workspace/renamed/Cargo.toml
new file mode 100644
index 0000000..aad7856
--- /dev/null
+++ b/e2e/workspace/renamed/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "renamed"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+ts-renamed = { package = "ts-rs", path = "../../../ts-rs" }
diff --git a/e2e/workspace/renamed/src/main.rs b/e2e/workspace/renamed/src/main.rs
new file mode 100644
index 0000000..fa6cf12
--- /dev/null
+++ b/e2e/workspace/renamed/src/main.rs
@@ -0,0 +1,12 @@
+use ts_renamed::TS;
+
+#[derive(TS)]
+#[ts(crate = "ts_renamed", export)]
+pub struct SimpleStruct {
+    hello: String,
+    world: u32,
+}
+
+fn main() {
+    println!("Hello, world!");
+}
diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs
index e9e10dd..fdf4ec2 100644
--- a/ts-rs/tests/generics.rs
+++ b/ts-rs/tests/generics.rs
@@ -85,7 +85,7 @@ fn test() {
 
     assert_eq!(
         Container::decl(),
-        "type Container = { foo: Generic<number>, bar: Array<Generic<number>>, baz: Record<string, Generic<string>>, };"
+        "type Container = { foo: Generic<number>, bar: Array<Generic<number>>, baz: { [key: string]: Generic<string> }, };"
     );
 }
 
diff --git a/ts-rs/tests/hashmap.rs b/ts-rs/tests/hashmap.rs
index 99512ea..e6070b0 100644
--- a/ts-rs/tests/hashmap.rs
+++ b/ts-rs/tests/hashmap.rs
@@ -15,7 +15,7 @@ struct Hashes {
 fn hashmap() {
     assert_eq!(
         Hashes::decl(),
-        "type Hashes = { map: Record<string, string>, set: Array<string>, };"
+        "type Hashes = { map: { [key: string]: string }, set: Array<string>, };"
     )
 }
 
@@ -35,7 +35,7 @@ struct HashesHasher {
 fn hashmap_with_custom_hasher() {
     assert_eq!(
         HashesHasher::decl(),
-        "type HashesHasher = { map: Record<string, string>, set: Array<string>, };"
+        "type HashesHasher = { map: { [key: string]: string }, set: Array<string>, };"
     )
 }
 
@@ -67,6 +67,6 @@ fn with_custom_types() {
     );
     assert_eq!(
         HashMapWithCustomTypes::decl(),
-        "type HashMapWithCustomTypes = { map: Record<CustomKey, CustomValue>, };"
+        "type HashMapWithCustomTypes = { map: { [key: CustomKey]: CustomValue }, };"
     );
 }
diff --git a/ts-rs/tests/indexmap.rs b/ts-rs/tests/indexmap.rs
index bf95b8a..9af682a 100644
--- a/ts-rs/tests/indexmap.rs
+++ b/ts-rs/tests/indexmap.rs
@@ -15,6 +15,6 @@ struct Indexes {
 fn indexmap() {
     assert_eq!(
         Indexes::decl(),
-        "type Indexes = { map: Record<string, string>, set: Array<string>, };"
+        "type Indexes = { map: { [key: string]: string }, set: Array<string>, };"
     )
 }
diff --git a/ts-rs/tests/issue-168.rs b/ts-rs/tests/issue-168.rs
index 7490860..d4f687e 100644
--- a/ts-rs/tests/issue-168.rs
+++ b/ts-rs/tests/issue-168.rs
@@ -38,13 +38,13 @@ fn issue_168() {
         FooInlined::export_to_string().unwrap(),
         "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\
          \n\
-         export type FooInlined = { map: Record<number, { map: Record<number, { map: Record<number, string>, }>, }>, };"
+         export type FooInlined = { map: { [key: number]: { map: { [key: number]: { map: { [key: number]: string }, } }, } }, };"
     );
     assert_eq!(
         Foo::export_to_string().unwrap(),
         "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\
          import type { Bar } from \"./Bar\";\n\
          \n\
-         export type Foo = { map: Record<number, Bar>, };"
+         export type Foo = { map: { [key: number]: Bar }, };"
     );
 }
diff --git a/ts-rs/tests/issue-70.rs b/ts-rs/tests/issue-70.rs
index cc57bdc..4db0ee8 100644
--- a/ts-rs/tests/issue-70.rs
+++ b/ts-rs/tests/issue-70.rs
@@ -24,11 +24,11 @@ struct Struct {
 fn issue_70() {
     assert_eq!(
         Enum::decl(),
-        "type Enum = { \"A\": Record<string, string> } | { \"B\": Record<string, string> };"
+        "type Enum = { \"A\": { [key: string]: string } } | { \"B\": { [key: string]: string } };"
     );
     assert_eq!(
         Struct::decl(),
-        "type Struct = { a: Record<string, string>, b: Record<string, string>, };"
+        "type Struct = { a: { [key: string]: string }, b: { [key: string]: string }, };"
     );
 }
 
diff --git a/ts-rs/tests/lifetimes.rs b/ts-rs/tests/lifetimes.rs
index b7b78fd..93f0cee 100644
--- a/ts-rs/tests/lifetimes.rs
+++ b/ts-rs/tests/lifetimes.rs
@@ -31,6 +31,6 @@ fn contains_borrow() {
 fn contains_borrow_type_args() {
     assert_eq!(
         A::decl(),
-        "type A = { a: Array<number>, b: Array<B<number>>, c: Record<string, boolean>, };"
+        "type A = { a: Array<number>, b: Array<B<number>>, c: { [key: string]: boolean }, };"
     );
 }
diff --git a/ts-rs/tests/self_referential.rs b/ts-rs/tests/self_referential.rs
index f800f7d..067f007 100644
--- a/ts-rs/tests/self_referential.rs
+++ b/ts-rs/tests/self_referential.rs
@@ -83,8 +83,8 @@ fn enum_externally_tagged() {
                  { \"C\": E } | \
                  { \"D\": E } | \
                  { \"E\": [E, E, E, E] } | \
-                 { \"F\": { a: E, b: E, c: Record<string, E>, d: E | null, e?: E | null, f?: E, } } | \
-                 { \"G\": [Array<E>, Array<E>, Record<string, E>] };"
+                 { \"F\": { a: E, b: E, c: { [key: string]: E }, d: E | null, e?: E | null, f?: E, } } | \
+                 { \"G\": [Array<E>, Array<E>, { [key: string]: E }] };"
     );
 }
 
@@ -122,7 +122,7 @@ fn enum_internally_tagged() {
                   { \"tag\": \"C\" } & I | \
                   { \"tag\": \"D\" } & I | \
                   { \"tag\": \"E\" } & Array<I> | \
-                  { \"tag\": \"F\", a: I, b: I, c: Record<I, I>, d: I | null, e?: I | null, f?: I, };"
+                  { \"tag\": \"F\", a: I, b: I, c: { [key: I]: I }, d: I | null, e?: I | null, f?: I, };"
     );
 }
 
@@ -170,7 +170,7 @@ fn enum_adjacently_tagged() {
                      \"content\": { \
                          a: A, \
                          b: A, \
-                         c: Record<string, A>, \
+                         c: { [key: string]: A }, \
                          d: A | null, \
                          e?: A | null, \
                          f?: A, \
@@ -179,9 +179,9 @@ fn enum_adjacently_tagged() {
                   { \
                      \"tag\": \"G\", \
                      \"content\": [\
-                         Array<A>, \
-                         [A, A, A, A], \
-                         Record<string, A>\
+                        Array<A>, \
+                        [A, A, A, A], \
+                        { [key: string]: A }\
                      ] \
                   };"
     );