Repository: Aleph-Alpha/ts-rs
Pull request #241 (2024-02-22 18:14:27)

Bug: Type Parameters are not imported


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eae40cc..0e3844a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,19 +1,25 @@
 # master
 
-### Breaking 
+### Breaking
+
 - Export types as `type` instead of `ìnterface` ([#203](https://github.com/Aleph-Alpha/ts-rs/pull/203))
 - Automatically export all dependencies when using `#[ts(export)]`, add `TS::dependency_types()` ([#221](https://github.com/Aleph-Alpha/ts-rs/pull/221))
 - Remove support for "skip_serializing", "skip_serializing_if" and "skip_deserializing". ([#204](https://github.com/Aleph-Alpha/ts-rs/pull/204))
-    - Initially supporting these by skipping a field was a mistake. If a user wishes to skip a field, they can still
-      annotate it with `#[ts(skip)]`
- 
+  - Initially supporting these by skipping a field was a mistake. If a user wishes to skip a field, they can still
+    annotate it with `#[ts(skip)]`
+- Added `TS::dependency_types()` ([#221](https://github.com/Aleph-Alpha/ts-rs/pull/221))
+- Added `TS::generics()` ([#241](https://github.com/Aleph-Alpha/ts-rs/pull/241))
+- Added `TS::WithoutGenerics` ([#241](https://github.com/Aleph-Alpha/ts-rs/pull/241))
+- `Result`, `Option`, `HashMap` and `Vec` had their implementations of `TS` changed ([#241](https://github.com/Aleph-Alpha/ts-rs/pull/241))
+
 ### Features
+
 - Implement `#[ts(as = "..")]` ([#174](https://github.com/Aleph-Alpha/ts-rs/pull/174))
 - For small arrays, generate tuples instead of `Array<T>` ([#209](https://github.com/Aleph-Alpha/ts-rs/pull/209))
 - Implement `#[ts(optional = nullable)]` ([#213](https://github.com/Aleph-Alpha/ts-rs/pull/213))
 - Allow inlining of fields with generic types ([#212](https://github.com/Aleph-Alpha/ts-rs/pull/212), [#215](https://github.com/Aleph-Alpha/ts-rs/pull/215), [#216](https://github.com/Aleph-Alpha/ts-rs/pull/216))
 - Allow flattening enum fields ([#206](https://github.com/Aleph-Alpha/ts-rs/pull/206))
-- Add `semver-impl` cargo feature with support for the *semver* crate ([#176](https://github.com/Aleph-Alpha/ts-rs/pull/176))
+- Add `semver-impl` cargo feature with support for the _semver_ crate ([#176](https://github.com/Aleph-Alpha/ts-rs/pull/176))
 - Support `HashMap` with custom hashers ([#173](https://github.com/Aleph-Alpha/ts-rs/pull/173))
 - Add `import-esm` cargo feature to import files with a `.js` extension ([#192](https://github.com/Aleph-Alpha/ts-rs/pull/192))
 - Implement `#[ts(...)]` equivalents for `#[serde(tag = "...")]`, `#[serde(tag = "...", content = "...")]` and `#[serde(untagged)]` ([#227](https://github.com/Aleph-Alpha/ts-rs/pull/227))
@@ -22,7 +28,8 @@
 - Export Rust doc comments/attributes on structs/enums as TSDoc strings ([#187](https://github.com/Aleph-Alpha/ts-rs/pull/187))
 
 ### Fixes
+
 - fix `#[ts(skip)]` and `#[serde(skip)]` in variants of adjacently or internally tagged enums ([#231](https://github.com/Aleph-Alpha/ts-rs/pull/231))
 - `rename_all` with `camelCase` produces wrong names if fields were already in camelCase ([#198](https://github.com/Aleph-Alpha/ts-rs/pull/198))
 - Improve support for references ([#199](https://github.com/Aleph-Alpha/ts-rs/pull/199))
-
+- Generic type aliases generate correctly ([#233](https://github.com/Aleph-Alpha/ts-rs/pull/233))
diff --git a/macros/src/deps.rs b/macros/src/deps.rs
index 735df35..4d096c0 100644
--- a/macros/src/deps.rs
+++ b/macros/src/deps.rs
@@ -16,6 +16,9 @@ impl Dependencies {
     /// If the type is transparent, then we'll get resolve the child dependencies during runtime.
     pub fn push(&mut self, ty: &Type) {
         self.0.push(quote![.push::<#ty>()]);
+        self.0.push(quote![
+            .extend(<#ty as ts_rs::TS>::generics())
+        ]);
     }
 
     pub fn append(&mut self, other: Dependencies) {
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 7844fa7..d5d4dbf 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -8,8 +8,7 @@ use syn::{
     TypeParam, WhereClause,
 };
 
-use crate::deps::Dependencies;
-use crate::utils::format_generics;
+use crate::{deps::Dependencies, utils::format_generics};
 
 #[macro_use]
 mod utils;
@@ -47,9 +46,9 @@ impl DerivedTS {
             }
         };
 
-        let export = self.export.then(
-            || self.generate_export_test(&rust_ty, &generics)
-        );
+        let export = self
+            .export
+            .then(|| self.generate_export_test(&rust_ty, &generics));
 
         let docs = match &*self.docs {
             "" => None,
@@ -58,24 +57,28 @@ impl DerivedTS {
 
         let ident = self.ts_name.clone();
         let impl_start = generate_impl_block_header(&rust_ty, &generics);
+        let assoc_type = generate_assoc_type(&rust_ty, &generics);
         let name = self.generate_name_fn();
         let inline = self.generate_inline_fn();
         let decl = self.generate_decl_fn(&rust_ty);
         let dependencies = &self.dependencies;
+        let generics_fn = self.generate_generics_fn();
 
         quote! {
             #impl_start {
+                #assoc_type
                 const EXPORT_TO: Option<&'static str> = Some(#export_to);
 
                 fn ident() -> String {
                     #ident.to_owned()
                 }
-                
+
                 #get_export_to
                 #docs
                 #name
                 #decl
                 #inline
+                #generics_fn
 
                 #[allow(clippy::unused_unit)]
                 fn dependency_types() -> impl ts_rs::typelist::TypeList
@@ -140,6 +143,7 @@ impl DerivedTS {
                     }
                 }
                 impl TS for #generics {
+                    type WithoutGenerics = #generics;
                     fn name() -> String { stringify!(#generics).to_owned() }
                     fn transparent() -> bool { false }
                 }
@@ -148,10 +152,11 @@ impl DerivedTS {
     }
 
     fn generate_export_test(&self, rust_ty: &Ident, generics: &Generics) -> TokenStream {
-        let test_fn = format_ident!("export_bindings_{}", rust_ty.to_string().to_lowercase());
-        let generic_params = generics
-            .type_params()
-            .map(|_| quote! { ts_rs::Dummy });
+        let test_fn = format_ident!(
+            "export_bindings_{}",
+            rust_ty.to_string().to_lowercase().replace("r#", "")
+        );
+        let generic_params = generics.type_params().map(|_| quote! { ts_rs::Dummy });
         let ty = quote!(<#rust_ty<#(#generic_params),*> as ts_rs::TS>);
 
         quote! {
@@ -163,12 +168,29 @@ impl DerivedTS {
         }
     }
 
+    fn generate_generics_fn(&self) -> TokenStream {
+        let generics = self
+            .generics
+            .type_params()
+            .map(|TypeParam { ident, .. }| quote![.push::<#ident>().extend(<#ident as ts_rs::TS>::generics())]);
+        quote! {
+            #[allow(clippy::unused_unit)]
+            fn generics() -> impl ts_rs::typelist::TypeList
+            where
+                Self: 'static,
+            {
+                use ts_rs::typelist::TypeList;
+                ()#(#generics)*
+            }
+        }
+    }
+
     fn generate_name_fn(&self) -> TokenStream {
         let name = self.name_with_generics();
         quote! {
-             fn name() -> String {
-                 #name
-             }
+            fn name() -> String {
+                #name
+            }
         }
     }
 
@@ -211,8 +233,9 @@ impl DerivedTS {
             //
             // We keep const parameters as they are, since there's no sensible default value we can
             // use instead. This might be something to change in the future.
-            G::Type(TypeParam { ident, .. }) |
-            G::Const(ConstParam { ident, .. }) => Some(quote!(#ident)),
+            G::Type(TypeParam { ident, .. }) | G::Const(ConstParam { ident, .. }) => {
+                Some(quote!(#ident))
+            }
         });
         quote! {
             fn decl_concrete() -> String {
@@ -228,6 +251,26 @@ impl DerivedTS {
     }
 }
 
+fn generate_assoc_type(rust_ty: &Ident, generics: &Generics) -> TokenStream {
+    use GenericParam as G;
+
+    let generics_params = generics
+        .params
+        .iter()
+        .map(|x| match x {
+            G::Type(_) => quote! { ts_rs::Dummy },
+            G::Const(ConstParam { ident, .. }) => quote! { #ident },
+            G::Lifetime(LifetimeParam { lifetime, .. }) => quote! { #lifetime },
+        })
+        .collect::<Vec<_>>();
+
+    if generics_params.is_empty() {
+        quote! { type WithoutGenerics = #rust_ty; }
+    } else {
+        quote! { type WithoutGenerics = #rust_ty<#(#generics_params),*>; }
+    }
+}
+
 // generate start of the `impl TS for #ty` block, up to (excluding) the open brace
 fn generate_impl_block_header(ty: &Ident, generics: &Generics) -> TokenStream {
     use GenericParam as G;
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index f99a4f0..e9ebfbb 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -5,8 +5,7 @@ use syn::{Fields, Generics, ItemEnum, Type, Variant};
 use crate::{
     attr::{EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr},
     deps::Dependencies,
-    DerivedTS,
-    types,
+    types, DerivedTS,
 };
 
 pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
@@ -113,8 +112,6 @@ fn format_variant(
                     ..
                 } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?;
 
-
-
                 if skip {
                     quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
                 } else {
@@ -128,7 +125,7 @@ fn format_variant(
                         (None, None) => {
                             let ty = &unnamed.unnamed[0].ty;
                             quote!(<#ty as ts_rs::TS>::name())
-                        },
+                        }
                     };
 
                     quote!(format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #ty))
@@ -178,7 +175,7 @@ fn format_variant(
                             (None, None) => {
                                 let ty = &unnamed.unnamed[0].ty;
                                 quote!(<#ty as ts_rs::TS>::name())
-                            },
+                            }
                         };
 
                         quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #name, #ty))
diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs
index f3cc0dc..2df0212 100644
--- a/macros/src/types/named.rs
+++ b/macros/src/types/named.rs
@@ -5,8 +5,8 @@ use syn::{Field, FieldsNamed, GenericArgument, Generics, PathArguments, Result,
 use crate::{
     attr::{FieldAttr, Inflection, Optional, StructAttr},
     deps::Dependencies,
-    DerivedTS,
     utils::{raw_name_to_ts_field, to_ts_ident},
+    DerivedTS,
 };
 
 pub(crate) fn named(
diff --git a/macros/src/utils.rs b/macros/src/utils.rs
index 234d29d..0b7205a 100644
--- a/macros/src/utils.rs
+++ b/macros/src/utils.rs
@@ -1,8 +1,11 @@
 use std::convert::TryFrom;
 
 use proc_macro2::{Ident, TokenStream};
-use syn::{Attribute, Error, Expr, ExprLit, GenericParam, Generics, Lit, Meta, Result, spanned::Spanned};
 use quote::quote;
+use syn::{
+    spanned::Spanned, Attribute, Error, Expr, ExprLit, GenericParam, Generics, Lit, Meta, Result,
+};
+
 use crate::deps::Dependencies;
 
 macro_rules! syn_err {
@@ -26,11 +29,16 @@ macro_rules! impl_parse {
             fn parse($input: syn::parse::ParseStream) -> syn::Result<Self> {
                 let mut $out = $i::default();
                 loop {
+                    let span = $input.span();
                     let key: Ident = $input.call(syn::ext::IdentExt::parse_any)?;
                     match &*key.to_string() {
                         $($k => $e,)*
                         #[allow(unreachable_patterns)]
-                        _ => syn_err!($input.span(); "unexpected attribute")
+                        x => syn_err!(
+                            span;
+                            "Unknown attribute \"{x}\". Allowed attributes are: {}",
+                            [$(stringify!($k),)*].join(", ")
+                        )
                     }
 
                     match $input.is_empty() {
@@ -65,7 +73,7 @@ pub fn raw_name_to_ts_field(value: String) -> String {
     let valid_chars = value
         .chars()
         .all(|c| c.is_alphanumeric() || c == '_' || c == '$');
-    
+
     let does_not_start_with_digit = value
         .chars()
         .next()
@@ -140,7 +148,7 @@ pub fn parse_docs(attrs: &[Attribute]) -> Result<String> {
         .map(|attr| {
             attr.map(|line| match line.trim() {
                 "" => " *".to_owned(),
-                _ => format!(" *{}", line.trim_end())
+                _ => format!(" *{}", line.trim_end()),
             })
         })
         .collect::<Result<Vec<_>>>()?;
diff --git a/ts-rs/src/chrono.rs b/ts-rs/src/chrono.rs
index 797289e..e5acfd7 100644
--- a/ts-rs/src/chrono.rs
+++ b/ts-rs/src/chrono.rs
@@ -11,6 +11,7 @@ use super::{impl_primitives, TS};
 macro_rules! impl_dummy {
     ($($t:ty),*) => {$(
         impl TS for $t {
+            type WithoutGenerics = $t;
             fn name() -> String { String::new() }
             fn inline() -> String { String::new() }
             fn transparent() -> bool { false }
@@ -22,6 +23,7 @@ impl_primitives!(NaiveDateTime, NaiveDate, NaiveTime, Month, Weekday, Duration =
 impl_dummy!(Utc, Local, FixedOffset);
 
 impl<T: TimeZone + 'static> TS for DateTime<T> {
+    type WithoutGenerics = Self;
     fn ident() -> String {
         "string".to_owned()
     }
@@ -37,6 +39,7 @@ impl<T: TimeZone + 'static> TS for DateTime<T> {
 }
 
 impl<T: TimeZone + 'static> TS for Date<T> {
+    type WithoutGenerics = Self;
     fn ident() -> String {
         "string".to_owned()
     }
diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs
index 7ecccc9..41e96c5 100644
--- a/ts-rs/src/export.rs
+++ b/ts-rs/src/export.rs
@@ -3,8 +3,7 @@ use std::{
     collections::BTreeMap,
     fmt::Write,
     path::{Component, Path, PathBuf},
-    sync::Mutex,
-    sync::OnceLock,
+    sync::{Mutex, OnceLock},
 };
 
 use thiserror::Error;
@@ -131,7 +130,9 @@ pub mod __private {
     const EXPORT_DIR_ENV_VAR: &str = "TS_RS_EXPORT_DIR";
     fn provided_default_dir() -> Option<&'static str> {
         static EXPORT_TO: OnceLock<Option<String>> = OnceLock::new();
-        EXPORT_TO.get_or_init(|| std::env::var(EXPORT_DIR_ENV_VAR).ok()).as_deref()
+        EXPORT_TO
+            .get_or_init(|| std::env::var(EXPORT_DIR_ENV_VAR).ok())
+            .as_deref()
     }
 
     /// Returns the path to where `T` should be exported using the `TS_RS_EXPORT_DIR` environment variable.
@@ -150,7 +151,7 @@ pub mod __private {
 pub(crate) fn export_type_to_string<T: TS + ?Sized + 'static>() -> Result<String, ExportError> {
     let mut buffer = String::with_capacity(1024);
     buffer.push_str(NOTE);
-    generate_imports::<T>(&mut buffer)?;
+    generate_imports::<T::WithoutGenerics>(&mut buffer)?;
     generate_decl::<T>(&mut buffer);
     Ok(buffer)
 }
@@ -276,4 +277,3 @@ where
         Some(comps.iter().map(|c| c.as_os_str()).collect())
     }
 }
-
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index 9146b28..2c2e040 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -164,10 +164,10 @@ use std::{
 
 pub use ts_rs_macros::TS;
 
+pub use crate::export::ExportError;
 // Used in generated code. Not public API
 #[doc(hidden)]
 pub use crate::export::__private;
-pub use crate::export::ExportError;
 use crate::typelist::TypeList;
 
 #[cfg(feature = "chrono-impl")]
@@ -275,10 +275,26 @@ pub mod typelist;
 /// - `#[ts(skip)]`:  
 ///   Skip this variant  
 pub trait TS {
+    /// If this type does not have generic parameters, then `WithoutGenerics` should just be `Self`.
+    /// If the type does have generic parameters, then all generic parameters must be replaced with
+    /// a dummy type, e.g `ts_rs::Dummy` or `()`.
+    /// The only requirement for these dummy types is that `EXPORT_TO` must be `None`.
+    /// Example:
+    /// ```ignore
+    /// struct GenericType<A, B>(A, B);
+    /// impl<A, B> TS for GenericType<A, B> {
+    ///     type WithoutGenerics = GenericType<ts_rs::Dummy, ts_rs::Dummy>;
+    ///     // ...
+    /// }
+    /// ```
+    type WithoutGenerics: TS + ?Sized;
+
     const EXPORT_TO: Option<&'static str> = None;
     const DOCS: Option<&'static str> = None;
 
+    /// Identifier of this type, excluding generic parameters.
     fn ident() -> String {
+        // by default, fall back to `TS::name()`.
         let name = Self::name();
 
         if name.contains('<') {
@@ -310,7 +326,7 @@ pub trait TS {
         panic!("{} cannot be declared", Self::name());
     }
 
-    /// Name of this type in TypeScript.
+    /// Name of this type in TypeScript, including generic parameters
     fn name() -> String;
 
     /// Formats this types definition in TypeScript, e.g `{ user_id: number }`.
@@ -332,6 +348,14 @@ pub trait TS {
     {
     }
 
+    /// Returns a `TypeList` containing all generic parameters of this type.
+    /// If this type is not generic, this will return an empty `TypeList`.
+    fn generics() -> impl TypeList
+    where
+        Self: 'static,
+    {
+    }
+
     /// Resolves all dependencies of this type recursively.
     fn dependencies() -> Vec<Dependency>
     where
@@ -426,6 +450,7 @@ impl Dependency {
 macro_rules! impl_primitives {
     ($($($ty:ty),* => $l:literal),*) => { $($(
         impl TS for $ty {
+            type WithoutGenerics = Self;
             fn name() -> String { $l.to_owned() }
             fn inline() -> String { <Self as $crate::TS>::name() }
             fn transparent() -> bool { false }
@@ -436,6 +461,7 @@ macro_rules! impl_primitives {
 macro_rules! impl_tuples {
     ( impl $($i:ident),* ) => {
         impl<$($i: TS),*> TS for ($($i,)*) {
+            type WithoutGenerics = (Dummy, );
             fn name() -> String {
                 format!("[{}]", [$($i::name()),*].join(", "))
             }
@@ -462,15 +488,22 @@ macro_rules! impl_tuples {
 macro_rules! impl_wrapper {
     ($($t:tt)*) => {
         $($t)* {
+            type WithoutGenerics = Self;
             fn name() -> String { T::name() }
             fn inline() -> String { T::inline() }
             fn inline_flattened() -> String { T::inline_flattened() }
-            fn dependency_types() -> impl TypeList
+            fn dependency_types() -> impl $crate::typelist::TypeList
             where
                 Self: 'static
             {
                 T::dependency_types()
             }
+            fn generics() -> impl $crate::typelist::TypeList
+            where
+                Self: 'static
+            {
+                ((std::marker::PhantomData::<T>,), T::generics())
+            }
             fn transparent() -> bool { T::transparent() }
         }
     };
@@ -480,6 +513,7 @@ macro_rules! impl_wrapper {
 macro_rules! impl_shadow {
     (as $s:ty: $($impl:tt)*) => {
         $($impl)* {
+            type WithoutGenerics = <$s as TS>::WithoutGenerics;
             fn name() -> String { <$s>::name() }
             fn inline() -> String { <$s>::inline() }
             fn inline_flattened() -> String { <$s>::inline_flattened() }
@@ -489,33 +523,44 @@ macro_rules! impl_shadow {
             {
                 <$s>::dependency_types()
             }
+            fn generics() -> impl $crate::typelist::TypeList
+            where
+                Self: 'static
+            {
+                <$s>::generics()
+            }
             fn transparent() -> bool { <$s>::transparent() }
         }
     };
 }
 
 impl<T: TS> TS for Option<T> {
+    type WithoutGenerics = Self;
     fn name() -> String {
         format!("{} | null", T::name())
     }
-
     fn inline() -> String {
         format!("{} | null", T::inline())
     }
-
     fn dependency_types() -> impl TypeList
     where
         Self: 'static,
     {
-        ().push::<T>()
+        T::dependency_types()
+    }
+    fn generics() -> impl TypeList
+    where
+        Self: 'static,
+    {
+        ((std::marker::PhantomData::<T>,), T::generics())
     }
-
     fn transparent() -> bool {
-        true
+        T::transparent()
     }
 }
 
 impl<T: TS, E: TS> TS for Result<T, E> {
+    type WithoutGenerics = Result<Dummy, Dummy>;
     fn name() -> String {
         format!("{{ Ok : {} }} | {{ Err : {} }}", T::name(), E::name())
     }
@@ -526,39 +571,55 @@ impl<T: TS, E: TS> TS for Result<T, E> {
     where
         Self: 'static,
     {
-        ().push::<T>().push::<E>()
+        T::dependency_types().extend(E::dependency_types())
+    }
+    fn generics() -> impl TypeList
+    where
+        Self: 'static,
+    {
+        use std::marker::PhantomData;
+        (
+            ((PhantomData::<T>,), T::generics()),
+            ((PhantomData::<E>,), E::generics()),
+        )
     }
     fn transparent() -> bool {
-        true
+        false
     }
 }
 
 impl<T: TS> TS for Vec<T> {
+    type WithoutGenerics = Vec<Dummy>;
     fn ident() -> String {
         "Array".to_owned()
     }
     fn name() -> String {
         format!("Array<{}>", T::name())
     }
-
     fn inline() -> String {
         format!("Array<{}>", T::inline())
     }
-
     fn dependency_types() -> impl TypeList
     where
         Self: 'static,
     {
-        ().push::<T>()
+        T::dependency_types()
+    }
+    fn generics() -> impl TypeList
+    where
+        Self: 'static,
+    {
+        ((std::marker::PhantomData::<T>,), T::generics())
     }
     fn transparent() -> bool {
-        true
+        false
     }
 }
 
 // Arrays longer than this limit will be emitted as Array<T>
 const ARRAY_TUPLE_LIMIT: usize = 64;
 impl<T: TS, const N: usize> TS for [T; N] {
+    type WithoutGenerics = [Dummy; N];
     fn name() -> String {
         if N > ARRAY_TUPLE_LIMIT {
             return Vec::<T>::name();
@@ -566,10 +627,7 @@ impl<T: TS, const N: usize> TS for [T; N] {
 
         format!(
             "[{}]",
-            (0..N)
-                .map(|_| T::name())
-                .collect::<Box<[_]>>()
-                .join(", ")
+            (0..N).map(|_| T::name()).collect::<Box<[_]>>().join(", ")
         )
     }
 
@@ -580,10 +638,7 @@ impl<T: TS, const N: usize> TS for [T; N] {
 
         format!(
             "[{}]",
-            (0..N)
-                .map(|_| T::inline())
-                .collect::<Box<[_]>>()
-                .join(", ")
+            (0..N).map(|_| T::inline()).collect::<Box<[_]>>().join(", ")
         )
     }
 
@@ -591,15 +646,23 @@ impl<T: TS, const N: usize> TS for [T; N] {
     where
         Self: 'static,
     {
-        ().push::<T>()
+        T::dependency_types()
+    }
+
+    fn generics() -> impl TypeList
+    where
+        Self: 'static,
+    {
+        ((std::marker::PhantomData::<T>,), T::generics())
     }
 
     fn transparent() -> bool {
-        true
+        false
     }
 }
 
 impl<K: TS, V: TS, H> TS for HashMap<K, V, H> {
+    type WithoutGenerics = HashMap<Dummy, Dummy>;
     fn ident() -> String {
         "Record".to_owned()
     }
@@ -615,15 +678,25 @@ impl<K: TS, V: TS, H> TS for HashMap<K, V, H> {
     where
         Self: 'static,
     {
-        ().push::<K>().push::<V>()
+        K::dependency_types().extend(V::dependency_types())
+    }
+    fn generics() -> impl TypeList
+    where
+        Self: 'static,
+    {
+        use std::marker::PhantomData;
+        (
+            ((PhantomData::<K>,), K::generics()),
+            ((PhantomData::<V>,), V::generics()),
+        )
     }
-
     fn transparent() -> bool {
-        true
+        false
     }
 }
 
 impl<I: TS> TS for Range<I> {
+    type WithoutGenerics = Range<Dummy>;
     fn name() -> String {
         format!("{{ start: {}, end: {}, }}", I::name(), I::name())
     }
@@ -641,6 +714,7 @@ impl<I: TS> TS for Range<I> {
 }
 
 impl<I: TS> TS for RangeInclusive<I> {
+    type WithoutGenerics = RangeInclusive<Dummy>;
     fn name() -> String {
         format!("{{ start: {}, end: {}, }}", I::name(), I::name())
     }
@@ -657,12 +731,12 @@ impl<I: TS> TS for RangeInclusive<I> {
     }
 }
 
-impl_shadow!(as T: impl<T: TS + ?Sized> TS for &T);
 impl_shadow!(as Vec<T>: impl<T: TS, H> TS for HashSet<T, H>);
 impl_shadow!(as Vec<T>: impl<T: TS> TS for BTreeSet<T>);
 impl_shadow!(as HashMap<K, V>: impl<K: TS, V: TS> TS for BTreeMap<K, V>);
 impl_shadow!(as Vec<T>: impl<T: TS> TS for [T]);
 
+impl_wrapper!(impl<T: TS + ?Sized> TS for &T);
 impl_wrapper!(impl<T: TS + ?Sized> TS for Box<T>);
 impl_wrapper!(impl<T: TS + ?Sized> TS for std::sync::Arc<T>);
 impl_wrapper!(impl<T: TS + ?Sized> TS for std::rc::Rc<T>);
@@ -728,7 +802,6 @@ impl_primitives! {
 #[rustfmt::skip]
 pub(crate) use impl_primitives;
 
-
 #[doc(hidden)]
 #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
 pub struct Dummy;
@@ -740,7 +813,11 @@ impl std::fmt::Display for Dummy {
 }
 
 impl TS for Dummy {
-    fn name() -> String { "Dummy".to_owned() }
-    fn transparent() -> bool { false }
+    type WithoutGenerics = Self;
+    fn name() -> String {
+        "Dummy".to_owned()
+    }
+    fn transparent() -> bool {
+        false
+    }
 }
-


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 94457a0..39b0e76 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,12 +13,12 @@ jobs:
         working-directory: e2e/dependencies/consumer
         run: |
           cargo t 
-          tsc bindings/* --noEmit
+          tsc bindings/* --noEmit --noUnusedLocals --strict
       - name: dependencies e2e test with default export env
         working-directory: e2e/dependencies/consumer
         run: |
           TS_RS_EXPORT_DIR=custom-bindings cargo t 
-          tsc custom-bindings/* --noEmit
+          tsc custom-bindings/* --noEmit --noUnusedLocals --strict
   e2e-workspace:
     name: Run 'workspace' end-to-end test
     runs-on: ubuntu-latest
@@ -31,12 +31,30 @@ jobs:
         working-directory: e2e/workspace
         run: |
           cargo t 
-          tsc parent/bindings/* --noEmit
+          tsc parent/bindings/* --noEmit --noUnusedLocals --strict
       - name: workspace e2e with default export env
         working-directory: e2e/workspace
         run: |
           TS_RS_EXPORT_DIR=custom-bindings cargo t 
-          tsc parent/custom-bindings/* --noEmit
+          tsc parent/custom-bindings/* --noEmit --noUnusedLocals --strict
+  e2e-example:
+    name: End-to-end test example
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions-rs/toolchain@v1
+        with:
+          toolchain: stable
+      - name: example e2e test
+        working-directory: example
+        run: |
+          cargo t 
+          tsc bindings/* --noEmit
+      - name: example e2e with default export env
+        working-directory: example
+        run: |
+          TS_RS_EXPORT_DIR=custom-bindings cargo t 
+          tsc custom-bindings/* --noEmit --noUnusedLocals --strict
 
   readme-up-to-date:
     name: Check that README.md is up-to-date
@@ -60,11 +78,13 @@ jobs:
       - uses: actions-rs/toolchain@v1
         with:
           toolchain: stable
-      - uses: actions-rs/cargo@v1
-        with:
-          command: test
-          args: --no-default-features --manifest-path ts-rs/Cargo.toml
-      - uses: actions-rs/cargo@v1
-        with:
-          command: test
-          args: --all-features --manifest-path ts-rs/Cargo.toml
\ No newline at end of file
+      - name: No features
+        run: |
+          cargo test --no-default-features
+          shopt -s globstar
+          tsc ts-rs/tests-out/**/*.ts --noEmit --noUnusedLocals
+      - name: All features
+        run: |
+          cargo test --all-features
+          shopt -s globstar
+          tsc ts-rs/tests-out/**/*.ts --noEmit --noUnusedLocals --strict
diff --git a/e2e/dependencies/consumer/src/main.rs b/e2e/dependencies/consumer/src/main.rs
index 01fadc8..ea294c2 100644
--- a/e2e/dependencies/consumer/src/main.rs
+++ b/e2e/dependencies/consumer/src/main.rs
@@ -9,6 +9,18 @@ struct ConsumerType {
     pub ty2_2: LibraryType2<&'static Self>,
     pub ty2_3: LibraryType2<LibraryType2<Box<ConsumerType>>>,
     pub ty2_4: LibraryType2<LibraryType2<LibraryType1>>,
+    pub ty2_5: LibraryType2<LibraryType3>,
 }
 
+#[derive(TS)]
+#[ts(export)]
+struct T0;
+
+#[derive(TS)]
+#[ts(export)]
+struct T1 {
+    t0: Option<T0>
+}
+
+
 fn main() {}
\ No newline at end of file
diff --git a/e2e/dependencies/dependency1/src/lib.rs b/e2e/dependencies/dependency1/src/lib.rs
index 5a635f1..7fa7cc4 100644
--- a/e2e/dependencies/dependency1/src/lib.rs
+++ b/e2e/dependencies/dependency1/src/lib.rs
@@ -9,3 +9,6 @@ pub struct LibraryType1 {
 pub struct LibraryType2<T> {
     pub t: T
 }
+
+#[derive(TS)]
+pub struct LibraryType3;
\ No newline at end of file
diff --git a/ts-rs/tests/arrays.rs b/ts-rs/tests/arrays.rs
index 4753b7e..0653dc1 100644
--- a/ts-rs/tests/arrays.rs
+++ b/ts-rs/tests/arrays.rs
@@ -1,5 +1,13 @@
+#![allow(dead_code)]
+
 use ts_rs::TS;
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/arrays/")]
+struct Interface {
+    a: [i32; 4],
+}
+
 #[test]
 fn free() {
     assert_eq!(<[String; 4]>::inline(), "[string, string, string, string]")
@@ -7,12 +15,6 @@ fn free() {
 
 #[test]
 fn interface() {
-    #[derive(TS)]
-    struct Interface {
-        #[allow(dead_code)]
-        a: [i32; 4],
-    }
-
     assert_eq!(
         Interface::inline(),
         "{ a: [number, number, number, number], }"
diff --git a/ts-rs/tests/chrono.rs b/ts-rs/tests/chrono.rs
index f4091be..586ac6a 100644
--- a/ts-rs/tests/chrono.rs
+++ b/ts-rs/tests/chrono.rs
@@ -1,4 +1,4 @@
-#![allow(deprecated)]
+#![allow(deprecated, dead_code)]
 #![cfg(feature = "chrono-impl")]
 
 use chrono::{
@@ -7,24 +7,24 @@ use chrono::{
 };
 use ts_rs::TS;
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/chrono/")]
+struct Chrono {
+    date: (NaiveDate, Date<Utc>, Date<Local>, Date<FixedOffset>),
+    time: NaiveTime,
+    date_time: (
+        NaiveDateTime,
+        DateTime<Utc>,
+        DateTime<Local>,
+        DateTime<FixedOffset>,
+    ),
+    duration: Duration,
+    month: Month,
+    weekday: Weekday,
+}
+
 #[test]
 fn chrono() {
-    #[derive(TS)]
-    #[allow(dead_code)]
-    struct Chrono {
-        date: (NaiveDate, Date<Utc>, Date<Local>, Date<FixedOffset>),
-        time: NaiveTime,
-        date_time: (
-            NaiveDateTime,
-            DateTime<Utc>,
-            DateTime<Local>,
-            DateTime<FixedOffset>,
-        ),
-        duration: Duration,
-        month: Month,
-        weekday: Weekday,
-    }
-
     assert_eq!(
         Chrono::decl(),
         "type Chrono = { date: [string, string, string, string], time: string, date_time: [string, string, string, string], duration: string, month: string, weekday: string, };"
diff --git a/ts-rs/tests/enum_flattening.rs b/ts-rs/tests/enum_flattening.rs
index b9d9de9..70c6713 100644
--- a/ts-rs/tests/enum_flattening.rs
+++ b/ts-rs/tests/enum_flattening.rs
@@ -4,125 +4,129 @@
 use serde::Serialize;
 use ts_rs::TS;
 
-#[test]
-fn externally_tagged() {
-    #[allow(dead_code)]
-    #[cfg_attr(feature = "serde-compat", derive(Serialize, TS))]
-    #[cfg_attr(not(feature = "serde-compat"), derive(TS))]
-    struct Foo {
-        qux: i32,
-        #[cfg_attr(feature = "serde-compat", serde(flatten))]
-        #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
-        baz: Bar,
-        biz: Option<String>,
-    }
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "tests-out/enum_flattening/externally_tagged/")]
+struct FooExternally {
+    qux: i32,
+    #[cfg_attr(feature = "serde-compat", serde(flatten))]
+    #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+    baz: BarExternally,
+    biz: Option<String>,
+}
 
-    #[cfg_attr(feature = "serde-compat", derive(Serialize, TS))]
-    #[cfg_attr(not(feature = "serde-compat"), derive(TS))]
-    #[allow(dead_code)]
-    enum Bar {
-        Baz { a: i32, a2: String },
-        Biz { b: bool },
-        Buz { c: String, d: Option<i32> },
-    }
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "tests-out/enum_flattening/externally_tagged/")]
+enum BarExternally {
+    Baz { a: i32, a2: String },
+    Biz { b: bool },
+    Buz { c: String, d: Option<i32> },
+}
 
+#[test]
+fn externally_tagged() {
     assert_eq!(
-        Foo::inline(),
+        FooExternally::inline(),
         r#"{ qux: number, biz: string | null, } & ({ "Baz": { a: number, a2: string, } } | { "Biz": { b: boolean, } } | { "Buz": { c: string, d: number | null, } })"#
     )
 }
 
-#[test]
-fn adjacently_tagged() {
-    #[cfg_attr(feature = "serde-compat", derive(Serialize))]
-    #[derive(TS)]
-    struct Foo {
-        one: i32,
-        #[cfg_attr(feature = "serde-compat", serde(flatten))]
-        #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
-        baz: Bar,
-        qux: Option<String>,
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/enum_flattening/adjacently_tagged/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+struct FooAdjecently {
+    one: i32,
+    #[cfg_attr(feature = "serde-compat", serde(flatten))]
+    #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+    baz: BarAdjecently,
+    qux: Option<String>,
+}
 
-    #[cfg_attr(feature = "serde-compat", derive(Serialize))]
-    #[derive(TS)]
-    #[allow(dead_code)]
-    #[cfg_attr(feature = "serde-compat", serde(tag = "type", content = "stuff"))]
-    #[cfg_attr(not(feature = "serde-compat"), ts(tag = "type", content = "stuff"))]
-    enum Bar {
-        Baz {
-            a: i32,
-            a2: String,
-        },
-        Biz {
-            b: bool,
-        },
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/enum_flattening/adjacently_tagged/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "type", content = "stuff"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "type", content = "stuff"))]
+enum BarAdjecently {
+    Baz {
+        a: i32,
+        a2: String,
+    },
+    Biz {
+        b: bool,
+    },
 
-        #[cfg_attr(feature = "serde-compat", serde(untagged))]
-        #[cfg_attr(not(feature = "serde-compat"), ts(untagged))]
-        Buz {
-            c: String,
-            d: Option<i32>,
-        },
-    }
+    #[cfg_attr(feature = "serde-compat", serde(untagged))]
+    #[cfg_attr(not(feature = "serde-compat"), ts(untagged))]
+    Buz {
+        c: String,
+        d: Option<i32>,
+    },
+}
 
+#[test]
+fn adjacently_tagged() {
     assert_eq!(
-        Foo::inline(),
+        FooAdjecently::inline(),
         r#"{ one: number, qux: string | null, } & ({ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { c: string, d: number | null, })"#
     )
 }
 
-#[test]
-fn internally_tagged() {
-    #[cfg_attr(feature = "serde-compat", derive(Serialize))]
-    #[derive(TS)]
-    struct Foo {
-        qux: Option<String>,
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/enum_flattening/internally_tagged/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+struct FooInternally {
+    qux: Option<String>,
 
-        #[cfg_attr(feature = "serde-compat", serde(flatten))]
-        #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
-        baz: Bar,
-    }
+    #[cfg_attr(feature = "serde-compat", serde(flatten))]
+    #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+    baz: BarInternally,
+}
 
-    #[cfg_attr(feature = "serde-compat", derive(Serialize))]
-    #[derive(TS)]
-    #[allow(dead_code)]
-    #[cfg_attr(feature = "serde-compat", serde(tag = "type"))]
-    #[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))]
-    enum Bar {
-        Baz { a: i32, a2: String },
-        Biz { b: bool },
-        Buz { c: String, d: Option<i32> },
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/enum_flattening/internally_tagged/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "type"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))]
+enum BarInternally {
+    Baz { a: i32, a2: String },
+    Biz { b: bool },
+    Buz { c: String, d: Option<i32> },
+}
 
+#[test]
+fn internally_tagged() {
     assert_eq!(
-        Foo::inline(),
+        FooInternally::inline(),
         r#"{ qux: string | null, } & ({ "type": "Baz", a: number, a2: string, } | { "type": "Biz", b: boolean, } | { "type": "Buz", c: string, d: number | null, })"#
     )
 }
 
-#[test]
-fn untagged() {
-    #[cfg_attr(feature = "serde-compat", derive(Serialize))]
-    #[derive(TS)]
-    struct Foo {
-        #[cfg_attr(feature = "serde-compat", serde(flatten))]
-        #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
-        baz: Bar,
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/enum_flattening/untagged/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+struct FooUntagged {
+    #[cfg_attr(feature = "serde-compat", serde(flatten))]
+    #[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
+    baz: BarUntagged,
+}
 
-    #[derive(TS)]
-    #[cfg_attr(feature = "serde-compat", derive(Serialize))]
-    #[cfg_attr(feature = "serde-compat", serde(untagged))]
-    #[cfg_attr(not(feature = "serde-compat"), ts(untagged))]
-    enum Bar {
-        Baz { a: i32, a2: String },
-        Biz { b: bool },
-        Buz { c: String },
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/enum_flattening/untagged/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[cfg_attr(feature = "serde-compat", serde(untagged))]
+#[cfg_attr(not(feature = "serde-compat"), ts(untagged))]
+enum BarUntagged {
+    Baz { a: i32, a2: String },
+    Biz { b: bool },
+    Buz { c: String },
+}
 
+#[test]
+fn untagged() {
     assert_eq!(
-        Foo::inline(),
+        FooUntagged::inline(),
         r#"{ a: number, a2: string, } | { b: boolean, } | { c: string, }"#
     )
 }
diff --git a/ts-rs/tests/enum_struct_rename_all.rs b/ts-rs/tests/enum_struct_rename_all.rs
index 59254b6..e8ad6e9 100644
--- a/ts-rs/tests/enum_struct_rename_all.rs
+++ b/ts-rs/tests/enum_struct_rename_all.rs
@@ -3,7 +3,7 @@ use serde::Serialize;
 use ts_rs::TS;
 
 #[derive(TS)]
-#[ts(export)]
+#[ts(export, export_to = "tests-out/enum_struct_rename_all/")]
 #[cfg_attr(feature = "serde-compat", derive(Serialize))]
 #[cfg_attr(feature = "serde-compat", serde(rename_all = "camelCase"))]
 #[cfg_attr(not(feature = "serde-compat"), ts(rename_all = "camelCase"))]
@@ -29,9 +29,9 @@ pub fn enum_struct_rename_all() {
     )
 }
 
-#[cfg_attr(feature = "serde-compat", derive(Serialize))]
 #[derive(TS, Clone)]
-#[ts(export)]
+#[ts(export, export_to = "tests-out/enum_struct_rename_all/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
 #[cfg_attr(feature = "serde-compat", serde(rename_all_fields = "kebab-case"))]
 #[cfg_attr(not(feature = "serde-compat"), ts(rename_all_fields = "kebab-case"))]
 pub enum TaskStatus2 {
diff --git a/ts-rs/tests/enum_variant_annotation.rs b/ts-rs/tests/enum_variant_annotation.rs
index e1306cb..f9309c3 100644
--- a/ts-rs/tests/enum_variant_annotation.rs
+++ b/ts-rs/tests/enum_variant_annotation.rs
@@ -4,11 +4,11 @@
 use serde::Serialize;
 use ts_rs::TS;
 
-#[cfg_attr(feature = "serde-compat", derive(Serialize))]
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/enum_variant_anotation/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
 #[cfg_attr(feature = "serde-compat", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
 #[cfg_attr(not(feature = "serde-compat"), ts(rename_all = "SCREAMING_SNAKE_CASE"))]
-#[ts(export)]
 enum A {
     MessageOne {
         sender_id: String,
@@ -30,9 +30,9 @@ fn test_enum_variant_rename_all() {
     );
 }
 
-#[cfg_attr(feature = "serde-compat", derive(Serialize))]
 #[derive(TS)]
-#[ts(export)]
+#[ts(export, export_to = "tests-out/enum_variant_anotation/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
 enum B {
     #[cfg_attr(feature = "serde-compat", serde(rename = "SnakeMessage"))]
     #[cfg_attr(not(feature = "serde-compat"), ts(rename = "SnakeMessage"))]
@@ -56,11 +56,11 @@ fn test_enum_variant_rename() {
     );
 }
 
-#[cfg_attr(feature = "serde-compat", derive(Serialize))]
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/enum_variant_anotation/")]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
 #[cfg_attr(feature = "serde-compat", serde(tag = "kind"))]
 #[cfg_attr(not(feature = "serde-compat"), ts(tag = "kind"))]
-#[ts(export)]
 pub enum C {
     #[cfg_attr(feature = "serde-compat", serde(rename = "SQUARE_THING"))]
     #[cfg_attr(not(feature = "serde-compat"), ts(rename = "SQUARE_THING"))]
diff --git a/ts-rs/tests/export_manually.rs b/ts-rs/tests/export_manually.rs
index 1ca37a7..6bf0e3b 100644
--- a/ts-rs/tests/export_manually.rs
+++ b/ts-rs/tests/export_manually.rs
@@ -5,7 +5,7 @@ use std::{concat, fs};
 use ts_rs::TS;
 
 #[derive(TS)]
-#[ts(export_to = "tests-out/export_here_test.ts")]
+#[ts(export_to = "tests-out/export_manually/UserFile.ts")]
 struct User {
     name: String,
     age: i32,
@@ -13,7 +13,7 @@ struct User {
 }
 
 #[derive(TS)]
-#[ts(export_to = "tests-out/export_here_dir_test/")]
+#[ts(export_to = "tests-out/export_manually/dir/")]
 struct UserDir {
     name: String,
     age: i32,
@@ -36,7 +36,7 @@ fn export_manually() {
         )
     };
 
-    let actual_content = fs::read_to_string("tests-out/export_here_test.ts").unwrap();
+    let actual_content = fs::read_to_string("tests-out/export_manually/UserFile.ts").unwrap();
 
     assert_eq!(actual_content, expected_content);
 }
@@ -57,7 +57,7 @@ fn export_manually_dir() {
         )
     };
 
-    let actual_content = fs::read_to_string("tests-out/export_here_dir_test/UserDir.ts").unwrap();
+    let actual_content = fs::read_to_string("tests-out/export_manually/dir/UserDir.ts").unwrap();
 
     assert_eq!(actual_content, expected_content);
 }
diff --git a/ts-rs/tests/flatten.rs b/ts-rs/tests/flatten.rs
index 5621249..9fd19fa 100644
--- a/ts-rs/tests/flatten.rs
+++ b/ts-rs/tests/flatten.rs
@@ -3,12 +3,14 @@
 use ts_rs::TS;
 
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/flatten/")]
 struct A {
     a: i32,
     b: i32,
 }
 
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/flatten/")]
 struct B {
     #[ts(flatten)]
     a: A,
@@ -16,6 +18,7 @@ struct B {
 }
 
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/flatten/")]
 struct C {
     #[ts(inline)]
     b: B,
diff --git a/ts-rs/tests/generic_fields.rs b/ts-rs/tests/generic_fields.rs
index e2100fa..45bae18 100644
--- a/ts-rs/tests/generic_fields.rs
+++ b/ts-rs/tests/generic_fields.rs
@@ -4,18 +4,22 @@ use std::borrow::Cow;
 
 use ts_rs::TS;
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generic_fields/")]
+struct Newtype(Vec<Cow<'static, i32>>);
+
 #[test]
 fn newtype() {
-    #[derive(TS)]
-    struct Newtype(Vec<Cow<'static, i32>>);
     assert_eq!(Newtype::inline(), "Array<number>");
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generic_fields/")]
+struct NewtypeNested(Vec<Vec<i32>>);
+
 #[test]
 fn newtype_nested() {
-    #[derive(TS)]
-    struct Newtype(Vec<Vec<i32>>);
-    assert_eq!(Newtype::inline(), "Array<Array<number>>");
+    assert_eq!(NewtypeNested::inline(), "Array<Array<number>>");
 }
 
 #[test]
@@ -30,51 +34,59 @@ fn alias_nested() {
     assert_eq!(Alias::inline(), "Array<Array<string>>");
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generic_fields/")]
+struct Struct {
+    a: Box<Vec<String>>,
+    b: (Vec<String>, Vec<String>),
+    c: [Vec<String>; 3],
+}
+
 #[test]
 fn named() {
-    #[derive(TS)]
-    struct Struct {
-        a: Box<Vec<String>>,
-        b: (Vec<String>, Vec<String>),
-        c: [Vec<String>; 3],
-    }
     assert_eq!(
         Struct::inline(),
         "{ a: Array<string>, b: [Array<string>, Array<string>], c: [Array<string>, Array<string>, Array<string>], }"
     );
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generic_fields/")]
+struct StructNested {
+    a: Vec<Vec<String>>,
+    b: (Vec<Vec<String>>, Vec<Vec<String>>),
+    c: [Vec<Vec<String>>; 3],
+}
+
 #[test]
 fn named_nested() {
-    #[derive(TS)]
-    struct Struct {
-        a: Vec<Vec<String>>,
-        b: (Vec<Vec<String>>, Vec<Vec<String>>),
-        c: [Vec<Vec<String>>; 3],
-    }
-    assert_eq!(Struct::inline(), "{ a: Array<Array<string>>, b: [Array<Array<string>>, Array<Array<string>>], c: [Array<Array<string>>, Array<Array<string>>, Array<Array<string>>], }");
+    assert_eq!(StructNested::inline(), "{ a: Array<Array<string>>, b: [Array<Array<string>>, Array<Array<string>>], c: [Array<Array<string>>, Array<Array<string>>, Array<Array<string>>], }");
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generic_fields/")]
+struct Tuple(Vec<i32>, (Vec<i32>, Vec<i32>), [Vec<i32>; 3]);
+
 #[test]
 fn tuple() {
-    #[derive(TS)]
-    struct Tuple(Vec<i32>, (Vec<i32>, Vec<i32>), [Vec<i32>; 3]);
     assert_eq!(
         Tuple::inline(),
         "[Array<number>, [Array<number>, Array<number>], [Array<number>, Array<number>, Array<number>]]"
     );
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generic_fields/")]
+struct TupleNested(
+    Vec<Vec<i32>>,
+    (Vec<Vec<i32>>, Vec<Vec<i32>>),
+    [Vec<Vec<i32>>; 3],
+);
+
 #[test]
 fn tuple_nested() {
-    #[derive(TS)]
-    struct Tuple(
-        Vec<Vec<i32>>,
-        (Vec<Vec<i32>>, Vec<Vec<i32>>),
-        [Vec<Vec<i32>>; 3],
-    );
     assert_eq!(
-        Tuple::inline(),
+        TupleNested::inline(),
         "[Array<Array<number>>, [Array<Array<number>>, Array<Array<number>>], [Array<Array<number>>, Array<Array<number>>, Array<Array<number>>]]"
     );
 }
diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs
index de4e5d6..0211e60 100644
--- a/ts-rs/tests/generics.rs
+++ b/ts-rs/tests/generics.rs
@@ -10,7 +10,7 @@ use std::{
 use ts_rs::TS;
 
 #[derive(TS)]
-#[ts(export)]
+#[ts(export, export_to = "tests-out/generics/")]
 struct Generic<T>
 where
     T: TS,
@@ -20,12 +20,14 @@ where
 }
 
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
 struct GenericAutoBound<T> {
     value: T,
     values: Vec<T>,
 }
 
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
 struct GenericAutoBound2<T>
 where
     T: PartialEq,
@@ -35,6 +37,7 @@ where
 }
 
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
 struct Container {
     foo: Generic<u32>,
     bar: Box<HashSet<Generic<u32>>>,
@@ -52,6 +55,7 @@ macro_rules! declare {
 
 declare! {
     #[derive(TS)]
+    #[ts(export, export_to = "tests-out/generics/")]
     TypeGroup {
         foo: Vec<Container>,
     }
@@ -85,244 +89,294 @@ fn test() {
     );
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+enum GenericEnum<A, B, C> {
+    A(A),
+    B(B, B, B),
+    C(Vec<C>),
+    D(Vec<Vec<Vec<A>>>),
+    E { a: A, b: B, c: C },
+    X(Vec<i32>),
+    Y(i32),
+    Z(Vec<Vec<i32>>),
+}
+
 #[test]
 fn generic_enum() {
-    #[derive(TS)]
-    enum Generic<A, B, C> {
-        A(A),
-        B(B, B, B),
-        C(Vec<C>),
-        D(Vec<Vec<Vec<A>>>),
-        E { a: A, b: B, c: C },
-        X(Vec<i32>),
-        Y(i32),
-        Z(Vec<Vec<i32>>),
-    }
-
     assert_eq!(
-        Generic::<(), (), ()>::decl(),
-        r#"type Generic<A, B, C> = { "A": A } | { "B": [B, B, B] } | { "C": Array<C> } | { "D": Array<Array<Array<A>>> } | { "E": { a: A, b: B, c: C, } } | { "X": Array<number> } | { "Y": number } | { "Z": Array<Array<number>> };"#
+        GenericEnum::<(), (), ()>::decl(),
+        r#"type GenericEnum<A, B, C> = { "A": A } | { "B": [B, B, B] } | { "C": Array<C> } | { "D": Array<Array<Array<A>>> } | { "E": { a: A, b: B, c: C, } } | { "X": Array<number> } | { "Y": number } | { "Z": Array<Array<number>> };"#
     )
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct NewType<T>(Vec<Vec<T>>);
+
 #[test]
 fn generic_newtype() {
-    #[derive(TS)]
-    struct NewType<T>(Vec<Vec<T>>);
-
     assert_eq!(
         NewType::<()>::decl(),
         r#"type NewType<T> = Array<Array<T>>;"#
     );
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct Tuple<T>(T, Vec<T>, Vec<Vec<T>>);
+
 #[test]
 fn generic_tuple() {
-    #[derive(TS)]
-    struct Tuple<T>(T, Vec<T>, Vec<Vec<T>>);
-
     assert_eq!(
         Tuple::<()>::decl(),
         r#"type Tuple<T> = [T, Array<T>, Array<Array<T>>];"#
     );
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct Struct<T> {
+    a: T,
+    b: (T, T),
+    c: (T, (T, T)),
+    d: [T; 3],
+    e: [(T, T); 3],
+    f: Vec<T>,
+    g: Vec<Vec<T>>,
+    h: Vec<[(T, T); 3]>,
+}
+
 #[test]
 fn generic_struct() {
-    #[derive(TS)]
-    struct Struct<T> {
-        a: T,
-        b: (T, T),
-        c: (T, (T, T)),
-        d: [T; 3],
-        e: [(T, T); 3],
-        f: Vec<T>,
-        g: Vec<Vec<T>>,
-        h: Vec<[(T, T); 3]>,
-    }
-
     assert_eq!(
         Struct::<()>::decl(),
         "type Struct<T> = { a: T, b: [T, T], c: [T, [T, T]], d: [T, T, T], e: [[T, T], [T, T], [T, T]], f: Array<T>, g: Array<Array<T>>, h: Array<[[T, T], [T, T], [T, T]]>, };"
     )
 }
 
-#[test]
-fn inline() {
-    #[derive(TS)]
-    struct Generic<T> {
-        t: T,
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct GenericInline<T> {
+    t: T,
+}
 
-    #[derive(TS)]
-    struct Container {
-        g: Generic<String>,
-        #[ts(inline)]
-        gi: Generic<String>,
-        #[ts(flatten)]
-        t: Generic<Vec<String>>,
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct ContainerInline {
+    g: GenericInline<String>,
+    #[ts(inline)]
+    gi: GenericInline<String>,
+    #[ts(flatten)]
+    t: GenericInline<Vec<String>>,
+}
 
-    assert_eq!(Generic::<()>::decl(), "type Generic<T> = { t: T, };");
+#[test]
+fn inline() {
     assert_eq!(
-        Container::decl(),
-        "type Container = { g: Generic<string>, gi: { t: string, }, t: Array<string>, };"
+        GenericInline::<()>::decl(),
+        "type GenericInline<T> = { t: T, };"
+    );
+    assert_eq!(
+        ContainerInline::decl(),
+        "type ContainerInline = { g: GenericInline<string>, gi: { t: string, }, t: Array<string>, };"
     );
 }
 
-#[test]
-fn inline_with_bounds() {
-    #[derive(TS)]
-    struct Generic<T: ToString> {
-        t: T,
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct GenericWithBounds<T: ToString> {
+    t: T,
+}
 
-    #[derive(TS)]
-    struct Container {
-        g: Generic<String>,
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct ContainerWithBounds {
+    g: GenericWithBounds<String>,
 
-        #[ts(inline)]
-        gi: Generic<String>,
+    #[ts(inline)]
+    gi: GenericWithBounds<String>,
 
-        #[ts(flatten)]
-        t: Generic<u32>,
-    }
+    #[ts(flatten)]
+    t: GenericWithBounds<u32>,
+}
 
+#[test]
+fn inline_with_bounds() {
     assert_eq!(
-        Generic::<&'static str>::decl(),
-        "type Generic<T> = { t: T, };"
+        GenericWithBounds::<&'static str>::decl(),
+        "type GenericWithBounds<T> = { t: T, };"
     );
     assert_eq!(
-        Container::decl(),
-        "type Container = { g: Generic<string>, gi: { t: string, }, t: number, };"
+        ContainerWithBounds::decl(),
+        "type ContainerWithBounds = { g: GenericWithBounds<string>, gi: { t: string, }, t: number, };"
     );
 }
 
-#[test]
-fn inline_with_default() {
-    #[derive(TS)]
-    struct Generic<T = String> {
-        t: T,
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct GenericWithDefault<T = String> {
+    t: T,
+}
 
-    #[derive(TS)]
-    struct Container {
-        g: Generic<String>,
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct ContainerWithDefault {
+    g: GenericWithDefault<String>,
 
-        #[ts(inline)]
-        gi: Generic<String>,
+    #[ts(inline)]
+    gi: GenericWithDefault<String>,
 
-        #[ts(flatten)]
-        t: Generic<u32>,
-    }
+    #[ts(flatten)]
+    t: GenericWithDefault<u32>,
+}
 
+#[test]
+fn inline_with_default() {
     assert_eq!(
-        Generic::<()>::decl(),
-        "type Generic<T = string> = { t: T, };"
+        GenericWithDefault::<()>::decl(),
+        "type GenericWithDefault<T = string> = { t: T, };"
     );
     assert_eq!(
-        Container::decl(),
-        "type Container = { g: Generic<string>, gi: { t: string, }, t: number, };"
+        ContainerWithDefault::decl(),
+        "type ContainerWithDefault = { g: GenericWithDefault<string>, gi: { t: string, }, t: number, };"
     );
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct ADefault<T = String> {
+    t: T,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct BDefault<U = Option<ADefault<i32>>> {
+    u: U,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct YDefault {
+    a1: ADefault,
+    a2: ADefault<i32>,
+}
+
 #[test]
 fn default() {
-    #[derive(TS)]
-    struct A<T = String> {
-        t: T,
-    }
-    assert_eq!(A::<()>::decl(), "type A<T = string> = { t: T, };");
+    assert_eq!(
+        ADefault::<()>::decl(),
+        "type ADefault<T = string> = { t: T, };"
+    );
 
-    #[derive(TS)]
-    struct B<U = Option<A<i32>>> {
-        u: U,
-    }
-    assert_eq!(B::<()>::decl(), "type B<U = A<number> | null> = { u: U, };");
-    assert!(B::<()>::dependencies().iter().any(|dep| dep.ts_name == "A"));
+    assert_eq!(
+        BDefault::<()>::decl(),
+        "type BDefault<U = ADefault<number> | null> = { u: U, };"
+    );
+    assert!(BDefault::<()>::dependencies()
+        .iter()
+        .any(|dep| dep.ts_name == "ADefault"));
 
-    #[derive(TS)]
-    struct Y {
-        a1: A,
-        a2: A<i32>,
-    }
-    assert_eq!(Y::decl(), "type Y = { a1: A<string>, a2: A<number>, };")
+    assert_eq!(
+        YDefault::decl(),
+        "type YDefault = { a1: ADefault<string>, a2: ADefault<number>, };"
+    )
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct ATraitBounds<T: ToString = i32> {
+    t: T,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct BTraitBounds<T: ToString + Debug + Clone + 'static>(T);
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+enum CTraitBounds<T: Copy + Clone + PartialEq, K: Copy + PartialOrd = i32> {
+    A { t: T },
+    B(T),
+    C,
+    D(T, K),
+}
+
+// Types with const generics can't be exported
+#[derive(TS)]
+struct DTraitBounds<T: ToString, const N: usize> {
+    t: [T; N],
 }
 
 #[test]
 fn trait_bounds() {
-    #[derive(TS)]
-    struct A<T: ToString = i32> {
-        t: T,
-    }
-    assert_eq!(A::<i32>::decl(), "type A<T = number> = { t: T, };");
-
-    #[derive(TS)]
-    struct B<T: ToString + Debug + Clone + 'static>(T);
-    assert_eq!(B::<&'static str>::decl(), "type B<T> = T;");
+    assert_eq!(
+        ATraitBounds::<i32>::decl(),
+        "type ATraitBounds<T = number> = { t: T, };"
+    );
 
-    #[derive(TS)]
-    enum C<T: Copy + Clone + PartialEq, K: Copy + PartialOrd = i32> {
-        A { t: T },
-        B(T),
-        C,
-        D(T, K),
-    }
     assert_eq!(
-        C::<&'static str, i32>::decl(),
-        r#"type C<T, K = number> = { "A": { t: T, } } | { "B": T } | "C" | { "D": [T, K] };"#
+        BTraitBounds::<&'static str>::decl(),
+        "type BTraitBounds<T> = T;"
     );
 
-    #[derive(TS)]
-    struct D<T: ToString, const N: usize> {
-        t: [T; N],
-    }
+    assert_eq!(
+        CTraitBounds::<&'static str, i32>::decl(),
+        r#"type CTraitBounds<T, K = number> = { "A": { t: T, } } | { "B": T } | "C" | { "D": [T, K] };"#
+    );
 
     let ty = format!(
-        "type D<T> = {{ t: [{}], }};",
+        "type DTraitBounds<T> = {{ t: [{}], }};",
         "T, ".repeat(41).trim_end_matches(", ")
     );
-    assert_eq!(D::<&str, 41>::decl(), ty)
+    assert_eq!(DTraitBounds::<&str, 41>::decl(), ty)
 }
 
-#[test]
-fn deeply_nested() {
-    #[derive(TS)]
-    struct T0<T> {
-        t0: T,
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct T0<T> {
+    t0: T,
+}
 
-    #[derive(TS)]
-    struct P0<T> {
-        p0: T,
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct P0<T> {
+    p0: T,
+}
 
-    #[derive(TS)]
-    struct T1<T> {
-        t0: T,
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct T1<T> {
+    t0: T,
+}
 
-    #[derive(TS)]
-    struct P1<T> {
-        p0: T,
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct P1<T> {
+    p0: T,
+}
 
-    #[derive(TS)]
-    struct Parent {
-        a: T1<T0<u32>>,
-        b: T1<P1<T0<P0<u32>>>>,
-        c: T1<P1<()>>,
-    }
-    #[derive(TS)]
-    struct GenericParent<T> {
-        a_t: T1<T0<T>>,
-        b_t: T1<P1<T0<P0<T>>>>,
-        c_t: T1<P1<T>>,
-        a_null: T1<T0<()>>,
-        b_null: T1<P1<T0<P0<()>>>>,
-        c_null: T1<P1<()>>,
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct Parent {
+    a: T1<T0<u32>>,
+    b: T1<P1<T0<P0<u32>>>>,
+    c: T1<P1<()>>,
+}
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct GenericParent<T> {
+    a_t: T1<T0<T>>,
+    b_t: T1<P1<T0<P0<T>>>>,
+    c_t: T1<P1<T>>,
+    a_null: T1<T0<()>>,
+    b_null: T1<P1<T0<P0<()>>>>,
+    c_null: T1<P1<()>>,
+}
+
+#[test]
+fn deeply_nested() {
     assert_eq!(
         Parent::inline(),
         "{ a: T1<T0<number>>, b: T1<P1<T0<P0<number>>>>, c: T1<P1<null>>, }"
@@ -340,30 +394,33 @@ fn deeply_nested() {
     );
 }
 
-#[test]
-fn inline_generic_enum() {
-    #[derive(TS)]
-    struct SomeType(String);
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct SomeType(String);
 
-    #[derive(TS)]
-    enum MyEnum<A, B> {
-        VariantA(A),
-        VariantB(B)
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+enum MyEnum<A, B> {
+    VariantA(A),
+    VariantB(B),
+}
 
-    #[derive(TS)]
-    struct Parent {
-        e: MyEnum<i32, i32>,
-        #[ts(inline)]
-        e1: MyEnum<i32, SomeType>
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/generics/")]
+struct ParentEnum {
+    e: MyEnum<i32, i32>,
+    #[ts(inline)]
+    e1: MyEnum<i32, SomeType>,
+}
 
+#[test]
+fn inline_generic_enum() {
     // This fails!
     // The #[ts(inline)] seems to inline recursively, so not only the definition of `MyEnum`, but
     // also the definition of `SomeType`.
     assert_eq!(
-        Parent::decl(),
-        "type Parent = { \
+        ParentEnum::decl(),
+        "type ParentEnum = { \
             e: MyEnum<number, number>, \
             e1: { \"VariantA\": number } | { \"VariantB\": SomeType }, \
         };"
diff --git a/ts-rs/tests/hashmap.rs b/ts-rs/tests/hashmap.rs
index 8f20512..310a656 100644
--- a/ts-rs/tests/hashmap.rs
+++ b/ts-rs/tests/hashmap.rs
@@ -1,38 +1,72 @@
-use std::collections::{HashMap, HashSet};
+#![allow(dead_code)]
+
+use std::collections::{BTreeMap, HashMap, HashSet};
 
 use ts_rs::TS;
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/hashmap/")]
+struct Hashes {
+    map: HashMap<String, String>,
+    set: HashSet<String>,
+}
+
 #[test]
 fn hashmap() {
-    #[derive(TS)]
-    #[allow(dead_code)]
-    struct Hashes {
-        map: HashMap<String, String>,
-        set: HashSet<String>,
-    }
-
     assert_eq!(
         Hashes::decl(),
         "type Hashes = { map: Record<string, string>, set: Array<string>, };"
     )
 }
 
+struct CustomHasher {}
+
+type CustomHashMap<K, V> = HashMap<K, V, CustomHasher>;
+type CustomHashSet<K> = HashSet<K, CustomHasher>;
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/hashmap/")]
+struct HashesHasher {
+    map: CustomHashMap<String, String>,
+    set: CustomHashSet<String>,
+}
+
 #[test]
 fn hashmap_with_custom_hasher() {
-    struct CustomHasher {}
+    assert_eq!(
+        HashesHasher::decl(),
+        "type HashesHasher = { map: Record<string, string>, set: Array<string>, };"
+    )
+}
+
+#[derive(TS, Eq, PartialEq, Hash)]
+#[ts(export, export_to = "tests-out/hashmap/")]
+struct CustomKey(String);
 
-    type CustomHashMap<K, V> = HashMap<K, V, CustomHasher>;
-    type CustomHashSet<K> = HashSet<K, CustomHasher>;
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/hashmap/")]
+struct CustomValue;
 
-    #[derive(TS)]
-    #[allow(dead_code)]
-    struct Hashes {
-        map: CustomHashMap<String, String>,
-        set: CustomHashSet<String>,
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/hashmap/")]
+struct HashMapWithCustomTypes {
+    map: HashMap<CustomKey, CustomValue>,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/hashmap/")]
+struct BTreeMapWithCustomTypes {
+    map: BTreeMap<CustomKey, CustomValue>,
+}
 
+#[test]
+fn with_custom_types() {
     assert_eq!(
-        Hashes::decl(),
-        "type Hashes = { map: Record<string, string>, set: Array<string>, };"
-    )
+        HashMapWithCustomTypes::inline(),
+        BTreeMapWithCustomTypes::inline()
+    );
+    assert_eq!(
+        HashMapWithCustomTypes::decl(),
+        "type HashMapWithCustomTypes = { map: Record<CustomKey, CustomValue>, };"
+    );
 }
diff --git a/ts-rs/tests/hashset.rs b/ts-rs/tests/hashset.rs
new file mode 100644
index 0000000..feeaa75
--- /dev/null
+++ b/ts-rs/tests/hashset.rs
@@ -0,0 +1,33 @@
+#![allow(dead_code)]
+
+use std::collections::{BTreeSet, HashSet};
+
+use ts_rs::TS;
+
+#[derive(TS, Eq, PartialEq, Hash)]
+#[ts(export, export_to = "tests-out/hashset/")]
+struct CustomValue;
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/hashset/")]
+struct HashSetWithCustomType {
+    set: HashSet<CustomValue>,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/hashset/")]
+struct BTreeSetWithCustomType {
+    set: BTreeSet<CustomValue>,
+}
+
+#[test]
+fn with_custom_types() {
+    assert_eq!(
+        HashSetWithCustomType::inline(),
+        BTreeSetWithCustomType::inline()
+    );
+    assert_eq!(
+        HashSetWithCustomType::decl(),
+        "type HashSetWithCustomType = { set: Array<CustomValue>, };"
+    );
+}
diff --git a/ts-rs/tests/imports.rs b/ts-rs/tests/imports.rs
index bf1688b..0451e17 100644
--- a/ts-rs/tests/imports.rs
+++ b/ts-rs/tests/imports.rs
@@ -2,21 +2,19 @@
 use ts_rs::TS;
 
 #[derive(TS)]
-#[ts(export)]
-#[ts(export_to = "/tmp/ts_rs_test_type_a.ts")]
+#[ts(export, export_to = "tests-out/imports/ts_rs_test_type_a.ts")]
 pub struct TestTypeA<T> {
     value: T,
 }
 
 #[derive(TS)]
-#[ts(export)]
-#[ts(export_to = "/tmp/ts_rs_test_type_b.ts")]
+#[ts(export, export_to = "tests-out/imports/ts_rs_test_type_b.ts")]
 pub struct TestTypeB<T> {
     value: T,
 }
 
 #[derive(TS)]
-#[ts(export_to = "/tmp/ts_rs_test_enum.ts")]
+#[ts(export, export_to = "tests-out/imports/")]
 pub enum TestEnum {
     C { value: TestTypeB<i8> },
     A1 { value: TestTypeA<i32> },
@@ -24,16 +22,13 @@ pub enum TestEnum {
 }
 
 #[test]
-#[cfg(feature = "format")]
 fn test_def() {
     // The only way to get access to how the imports look is to export the type and load the exported file
     TestEnum::export().unwrap();
     let text = std::fs::read_to_string(TestEnum::EXPORT_TO.unwrap()).unwrap();
 
-    // Checks to make sure imports are ordered and deduplicated
-    #[cfg(feature = "import-esm")]
-    assert_eq!(text,
-        concat!(
+    let expected = match (cfg!(feature = "format"), cfg!(feature = "import-esm")) {
+        (true, true) => concat!(
             "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n",
             "import type { TestTypeA } from \"./ts_rs_test_type_a.js\";\n",
             "import type { TestTypeB } from \"./ts_rs_test_type_b.js\";\n",
@@ -41,11 +36,9 @@ fn test_def() {
             "export type TestEnum = { \"C\": { value: TestTypeB<number> } } | {\n",
             "  \"A1\": { value: TestTypeA<number> };\n",
             "} | { \"A2\": { value: TestTypeA<number> } };\n"
-        )
-    );
-    #[cfg(not(feature = "import-esm"))]
-    assert_eq!(text,
-        concat!(
+
+        ),
+        (true, false) => concat!(
             "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n",
             "import type { TestTypeA } from \"./ts_rs_test_type_a\";\n",
             "import type { TestTypeB } from \"./ts_rs_test_type_b\";\n",
@@ -53,8 +46,24 @@ fn test_def() {
             "export type TestEnum = { \"C\": { value: TestTypeB<number> } } | {\n",
             "  \"A1\": { value: TestTypeA<number> };\n",
             "} | { \"A2\": { value: TestTypeA<number> } };\n"
-        )
-    );
 
+        ),
+        (false, true) => concat!(
+            "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n",
+            "import type { TestTypeA } from \"./ts_rs_test_type_a.js\";\n",
+            "import type { TestTypeB } from \"./ts_rs_test_type_b.js\";\n",
+            "\n",
+            "export type TestEnum = { \"C\": { value: TestTypeB<number>, } } | { \"A1\": { value: TestTypeA<number>, } } | { \"A2\": { value: TestTypeA<number>, } };"
+        ),
+        (false, false) => concat!(
+            "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n",
+            "import type { TestTypeA } from \"./ts_rs_test_type_a\";\n",
+            "import type { TestTypeB } from \"./ts_rs_test_type_b\";\n",
+            "\n",
+            "export type TestEnum = { \"C\": { value: TestTypeB<number>, } } | { \"A1\": { value: TestTypeA<number>, } } | { \"A2\": { value: TestTypeA<number>, } };"
+        ),
+    };
+
+    assert_eq!(text, expected);
     std::fs::remove_file(TestEnum::EXPORT_TO.unwrap()).unwrap();
 }
diff --git a/ts-rs/tests/indexmap.rs b/ts-rs/tests/indexmap.rs
index 66319b8..a63305c 100644
--- a/ts-rs/tests/indexmap.rs
+++ b/ts-rs/tests/indexmap.rs
@@ -1,17 +1,18 @@
+#![allow(dead_code)]
 #![cfg(feature = "indexmap-impl")]
 
 use indexmap::{IndexMap, IndexSet};
 use ts_rs::TS;
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/indexmap/")]
+struct Indexes {
+    map: IndexMap<String, String>,
+    set: IndexSet<String>,
+}
+
 #[test]
 fn indexmap() {
-    #[derive(TS)]
-    #[allow(dead_code)]
-    struct Indexes {
-        map: IndexMap<String, String>,
-        set: IndexSet<String>,
-    }
-
     assert_eq!(
         Indexes::decl(),
         "type Indexes = { map: Record<string, string>, set: Array<string>, };"
diff --git a/ts-rs/tests/issue-168.rs b/ts-rs/tests/issue-168.rs
new file mode 100644
index 0000000..1a4c709
--- /dev/null
+++ b/ts-rs/tests/issue-168.rs
@@ -0,0 +1,50 @@
+#![allow(unused)]
+
+use std::collections::HashMap;
+
+use ts_rs::TS;
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/issue_168/")]
+pub struct Foo {
+    map: HashMap<usize, Bar>,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/issue_168/")]
+pub struct FooInlined {
+    #[ts(inline)]
+    map: HashMap<usize, Bar>,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/issue_168/")]
+struct Bar {
+    #[ts(inline)]
+    map: HashMap<usize, Baz>,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/issue_168/")]
+struct Baz {
+    #[ts(inline)]
+    map: HashMap<usize, String>,
+}
+
+#[test]
+#[cfg(not(feature = "import-esm"))]
+fn issue_168() {
+    assert_eq!(
+        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>, }>, }>, };"
+    );
+    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>, };"
+    );
+}
diff --git a/ts-rs/tests/issue-232.rs b/ts-rs/tests/issue-232.rs
new file mode 100644
index 0000000..cf0b841
--- /dev/null
+++ b/ts-rs/tests/issue-232.rs
@@ -0,0 +1,68 @@
+#![allow(unused)]
+
+use ts_rs::TS;
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/issue_232/")]
+struct State {
+    a: Result<EnumWithName, String>,
+    b: Result<EnumWithName, String>,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/issue_232/")]
+struct StateInlined {
+    #[ts(inline)]
+    a: Result<EnumWithName, String>,
+    #[ts(inline)]
+    b: Result<EnumWithName, String>,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/issue_232/")]
+struct StateInlinedVec {
+    #[ts(inline)]
+    a: Vec<Result<EnumWithName, String>>,
+    #[ts(inline)]
+    b: Vec<Result<EnumWithName, String>>,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/issue_232/")]
+struct EnumWithName {
+    name: String,
+    inner: Enum,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/issue_232/")]
+enum Enum {
+    A,
+    B,
+}
+
+#[test]
+#[cfg(not(feature = "import-esm"))]
+fn issue_232() {
+    println!("{}", StateInlinedVec::export_to_string().unwrap());
+    assert_eq!(
+        StateInlined::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 { Enum } from \"./Enum\";\n\
+        \n\
+        export type StateInlined = { \
+            a: { Ok : { name: string, inner: Enum, } } | { Err : string }, \
+            b: { Ok : { name: string, inner: Enum, } } | { Err : string }, \
+        };"
+    );
+    assert_eq!(
+        State::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 { EnumWithName } from \"./EnumWithName\";\n\
+        \n\
+        export type State = { \
+            a: { Ok : EnumWithName } | { Err : string }, \
+            b: { Ok : EnumWithName } | { Err : string }, \
+        };"
+    );
+}
diff --git a/ts-rs/tests/issue-70.rs b/ts-rs/tests/issue-70.rs
index a90bd4d..1c9b71a 100644
--- a/ts-rs/tests/issue-70.rs
+++ b/ts-rs/tests/issue-70.rs
@@ -1,57 +1,77 @@
 #![allow(unused)]
 
 use std::collections::HashMap;
+
 use ts_rs::TS;
 
+type TypeAlias = HashMap<String, String>;
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/issue_70/")]
+enum Enum {
+    A(TypeAlias),
+    B(HashMap<String, String>),
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/issue_70/")]
+struct Struct {
+    a: TypeAlias,
+    b: HashMap<String, String>,
+}
+
 #[test]
 fn issue_70() {
-    type TypeAlias = HashMap<String, String>;
-
-    #[derive(TS)]
-    enum Enum {
-        A(TypeAlias),
-        B(HashMap<String, String>),
-    }
-
-    #[derive(TS)]
-    struct Struct {
-        a: TypeAlias,
-        b: HashMap<String, String>
-    }
-
-    assert_eq!(Enum::decl(), "type Enum = { \"A\": Record<string, string> } | { \"B\": Record<string, string> };");
-    assert_eq!(Struct::decl(), "type Struct = { a: Record<string, string>, b: Record<string, string>, };");
+    assert_eq!(
+        Enum::decl(),
+        "type Enum = { \"A\": Record<string, string> } | { \"B\": Record<string, string> };"
+    );
+    assert_eq!(
+        Struct::decl(),
+        "type Struct = { a: Record<string, string>, b: Record<string, string>, };"
+    );
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/issue_70/")]
+struct GenericType<T, U> {
+    foo: T,
+    bar: U,
+}
+
+type GenericAlias<A = String, B = String> = GenericType<(A, String), Vec<(B, i32)>>;
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/issue_70/")]
+struct Container {
+    a: GenericAlias<Vec<i32>, Vec<String>>,
+    b: GenericAlias,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/issue_70/")]
+struct GenericContainer<A, B = i32> {
+    a: GenericAlias,
+    b: GenericAlias<A, B>,
+    c: GenericAlias<A, GenericAlias<A, B>>,
 }
 
 #[test]
 fn generic() {
-    type GenericAlias<A = String, B = String> = HashMap<(A, String), Vec<(B, i32)>>;
-
-    #[derive(TS)]
-    struct Container {
-        a: GenericAlias<Vec<i32>, Vec<String>>,
-        b: GenericAlias
-    }
     assert_eq!(
         Container::decl(),
         "type Container = { \
-            a: Record<[Array<number>, string], Array<[Array<string>, number]>>, \
-            b: Record<[string, string], Array<[string, number]>>, \
+            a: GenericType<[Array<number>, string], Array<[Array<string>, number]>>, \
+            b: GenericType<[string, string], Array<[string, number]>>, \
         };"
     );
 
-    #[derive(TS)]
-    struct GenericContainer<A, B = i32> {
-        a: GenericAlias,
-        b: GenericAlias<A, B>,
-        c: GenericAlias<A, GenericAlias<A, B>>
-    }
     assert_eq!(
         GenericContainer::<(), ()>::decl(),
         "type GenericContainer<A, B = number> = { \
-            a: Record<[string, string], Array<[string, number]>>, \
-            b: Record<[A, string], Array<[B, number]>>, \
-            c: Record<[A, string], Array<[Record<[A, string], Array<[B, number]>>, number]>>, \
+            a: GenericType<[string, string], Array<[string, number]>>, \
+            b: GenericType<[A, string], Array<[B, number]>>, \
+            c: GenericType<[A, string], Array<[GenericType<[A, string], Array<[B, number]>>, number]>>, \
         };"
     );
-}
\ No newline at end of file
+}
diff --git a/ts-rs/tests/issue-80.rs b/ts-rs/tests/issue-80.rs
index 20c4b07..8a59bd8 100644
--- a/ts-rs/tests/issue-80.rs
+++ b/ts-rs/tests/issue-80.rs
@@ -2,7 +2,7 @@ use serde::Serialize;
 use ts_rs::TS;
 
 #[derive(TS, Serialize)]
-#[ts(export)]
+#[ts(export, export_to = "tests-out/issue_80/")]
 pub enum SomeTypeList {
     Value1 {
         #[serde(skip)]
diff --git a/ts-rs/tests/lifetimes.rs b/ts-rs/tests/lifetimes.rs
index d524cea..f359302 100644
--- a/ts-rs/tests/lifetimes.rs
+++ b/ts-rs/tests/lifetimes.rs
@@ -1,32 +1,34 @@
+#![allow(dead_code)]
+
 use ts_rs::TS;
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/lifetimes/")]
+struct S<'a> {
+    s: &'a str,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/lifetimes/")]
+struct B<'a, T: 'a> {
+    a: &'a T,
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/lifetimes/")]
+struct A<'a> {
+    a: &'a &'a &'a Vec<u32>,                        //Multiple References
+    b: &'a Vec<B<'a, u32>>,                         //Nesting
+    c: &'a std::collections::HashMap<String, bool>, //Multiple type args
+}
+
 #[test]
 fn contains_borrow() {
-    #[derive(TS)]
-    #[allow(dead_code)]
-    struct S<'a> {
-        s: &'a str,
-    }
-
     assert_eq!(S::decl(), "type S = { s: string, };")
 }
 
 #[test]
 fn contains_borrow_type_args() {
-    #[derive(TS)]
-    #[allow(dead_code)]
-    struct B<'a, T: 'a> {
-        a: &'a T,
-    }
-
-    #[derive(TS)]
-    #[allow(dead_code)]
-    struct A<'a> {
-        a: &'a &'a &'a Vec<u32>,                        //Multiple References
-        b: &'a Vec<B<'a, u32>>,                         //Nesting
-        c: &'a std::collections::HashMap<String, bool>, //Multiple type args
-    }
-
     assert_eq!(
         A::decl(),
         "type A = { a: Array<number>, b: Array<B<number>>, c: Record<string, boolean>, };"
diff --git a/ts-rs/tests/list.rs b/ts-rs/tests/list.rs
index f8c131e..f2cf230 100644
--- a/ts-rs/tests/list.rs
+++ b/ts-rs/tests/list.rs
@@ -1,12 +1,13 @@
+#![allow(dead_code)]
 use ts_rs::TS;
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/list/")]
+struct List {
+    data: Option<Vec<u32>>,
+}
+
 #[test]
 fn list() {
-    #[derive(TS)]
-    struct List {
-        #[allow(dead_code)]
-        data: Option<Vec<u32>>,
-    }
-
     assert_eq!(List::decl(), "type List = { data: Array<number> | null, };");
 }
diff --git a/ts-rs/tests/nested.rs b/ts-rs/tests/nested.rs
index 714ef94..778878b 100644
--- a/ts-rs/tests/nested.rs
+++ b/ts-rs/tests/nested.rs
@@ -5,12 +5,14 @@ use std::{cell::Cell, rc::Rc, sync::Arc};
 use ts_rs::TS;
 
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/nested/")]
 struct A {
     x1: Arc<i32>,
     y1: Cell<i32>,
 }
 
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/nested/")]
 struct B {
     a1: Box<A>,
     #[ts(inline)]
@@ -18,6 +20,7 @@ struct B {
 }
 
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/nested/")]
 struct C {
     b1: Rc<B>,
     #[ts(inline)]
diff --git a/ts-rs/tests/optional_field.rs b/ts-rs/tests/optional_field.rs
index 1c7174a..599efef 100644
--- a/ts-rs/tests/optional_field.rs
+++ b/ts-rs/tests/optional_field.rs
@@ -3,79 +3,85 @@
 use serde::Serialize;
 use ts_rs::TS;
 
+#[derive(Serialize, TS)]
+#[ts(export, export_to = "tests-out/optional_field/")]
+struct OptionalInStruct {
+    #[ts(optional)]
+    a: Option<i32>,
+    #[ts(optional = nullable)]
+    b: Option<i32>,
+    c: Option<i32>,
+}
+
 #[test]
 fn in_struct() {
-    #[derive(Serialize, TS)]
-    struct Optional {
-        #[ts(optional)]
-        a: Option<i32>,
-        #[ts(optional = nullable)]
-        b: Option<i32>,
-        c: Option<i32>,
-    }
-
     let a = "a?: number";
     let b = "b?: number | null";
     let c = "c: number | null";
-    assert_eq!(Optional::inline(), format!("{{ {a}, {b}, {c}, }}"));
+    assert_eq!(OptionalInStruct::inline(), format!("{{ {a}, {b}, {c}, }}"));
+}
+
+#[derive(Serialize, TS)]
+#[ts(export, export_to = "tests-out/optional_field/")]
+enum OptionalInEnum {
+    A {
+        #[ts(optional)]
+        a: Option<i32>,
+    },
+    B {
+        b: Option<String>,
+    },
 }
 
 #[test]
 fn in_enum() {
-    #[derive(Serialize, TS)]
-    enum Optional {
-        A {
-            #[ts(optional)]
-            a: Option<i32>,
-        },
-        B {
-            b: Option<String>,
-        },
-    }
-
     assert_eq!(
-        Optional::inline(),
+        OptionalInEnum::inline(),
         r#"{ "A": { a?: number, } } | { "B": { b: string | null, } }"#
     );
 }
 
+#[derive(Serialize, TS)]
+#[ts(export, export_to = "tests-out/optional_field/")]
+struct OptionalFlatten {
+    #[ts(optional)]
+    a: Option<i32>,
+    #[ts(optional = nullable)]
+    b: Option<i32>,
+    c: Option<i32>,
+}
+
+#[derive(Serialize, TS)]
+#[ts(export, export_to = "tests-out/optional_field/")]
+struct Flatten {
+    #[ts(flatten)]
+    x: OptionalFlatten,
+}
+
 #[test]
 fn flatten() {
-    #[derive(Serialize, TS)]
-    struct Optional {
-        #[ts(optional)]
-        a: Option<i32>,
-        #[ts(optional = nullable)]
-        b: Option<i32>,
-        c: Option<i32>,
-    }
+    assert_eq!(Flatten::inline(), OptionalFlatten::inline());
+}
 
-    #[derive(Serialize, TS)]
-    struct Flatten {
-        #[ts(flatten)]
-        x: Optional,
-    }
+#[derive(Serialize, TS)]
+#[ts(export, export_to = "tests-out/optional_field/")]
+struct OptionalInline {
+    #[ts(optional)]
+    a: Option<i32>,
+    #[ts(optional = nullable)]
+    b: Option<i32>,
+    c: Option<i32>,
+}
 
-    assert_eq!(Flatten::inline(), Optional::inline());
+#[derive(Serialize, TS)]
+#[ts(export, export_to = "tests-out/optional_field/")]
+struct Inline {
+    #[ts(inline)]
+    x: OptionalInline,
 }
 
 #[test]
 fn inline() {
-    #[derive(Serialize, TS)]
-    struct Optional {
-        #[ts(optional)]
-        a: Option<i32>,
-        #[ts(optional = nullable)]
-        b: Option<i32>,
-        c: Option<i32>,
-    }
-
-    #[derive(Serialize, TS)]
-    struct Inline {
-        #[ts(inline)]
-        x: Optional,
-    }
-
     let a = "a?: number";
     let b = "b?: number | null";
     let c = "c: number | null";
diff --git a/ts-rs/tests/ranges.rs b/ts-rs/tests/ranges.rs
index f9d50a6..26af887 100644
--- a/ts-rs/tests/ranges.rs
+++ b/ts-rs/tests/ranges.rs
@@ -1,3 +1,5 @@
+#![allow(dead_code)]
+
 use std::{
     collections::BTreeSet,
     ops::{Range, RangeInclusive},
@@ -6,10 +8,11 @@ use std::{
 use ts_rs::{Dependency, TS};
 
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/ranges/")]
 struct Inner(i32);
 
 #[derive(TS)]
-#[allow(dead_code)]
+#[ts(export, export_to = "tests-out/ranges/")]
 struct RangeTest {
     a: Range<u32>,
     b: Range<&'static str>,
diff --git a/ts-rs/tests/raw_idents.rs b/ts-rs/tests/raw_idents.rs
index dc87d95..870fc77 100644
--- a/ts-rs/tests/raw_idents.rs
+++ b/ts-rs/tests/raw_idents.rs
@@ -1,8 +1,10 @@
+#![allow(non_camel_case_types, dead_code)]
+
 use ts_rs::TS;
 
-#[allow(non_camel_case_types, dead_code)]
 #[derive(TS)]
-struct r#enum {
+#[ts(export, export_to = "tests-out/raw_idents/")]
+struct r#struct {
     r#type: i32,
     r#use: i32,
     r#struct: i32,
@@ -12,9 +14,9 @@ struct r#enum {
 
 #[test]
 fn raw_idents() {
-    let out = <r#enum as TS>::decl();
+    let out = <r#struct as TS>::decl();
     assert_eq!(
         out,
-        "type enum = { type: number, use: number, struct: number, let: number, enum: number, };"
+        "type struct = { type: number, use: number, struct: number, let: number, enum: number, };"
     );
 }
diff --git a/ts-rs/tests/references.rs b/ts-rs/tests/references.rs
index e9b2dbb..e9411fc 100644
--- a/ts-rs/tests/references.rs
+++ b/ts-rs/tests/references.rs
@@ -1,15 +1,17 @@
+#![allow(dead_code)]
+
 use ts_rs::TS;
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/references/")]
+struct FullOfRefs<'a> {
+    str_slice: &'a str,
+    ref_slice: &'a [&'a str],
+    num_ref: &'a i32,
+}
+
 #[test]
 fn references() {
-    #[derive(TS)]
-    #[allow(dead_code)]
-    struct FullOfRefs<'a> {
-        str_slice: &'a str,
-        ref_slice: &'a [&'a str],
-        num_ref: &'a i32,
-    }
-
     assert_eq!(
         FullOfRefs::inline(),
         "{ str_slice: string, ref_slice: Array<string>, num_ref: number, }"
diff --git a/ts-rs/tests/self_referential.rs b/ts-rs/tests/self_referential.rs
index 0357a5b..acf1235 100644
--- a/ts-rs/tests/self_referential.rs
+++ b/ts-rs/tests/self_referential.rs
@@ -2,30 +2,34 @@
 
 use std::{collections::HashMap, sync::Arc};
 
+#[cfg(feature = "serde-compat")]
+use serde::Serialize;
 use ts_rs::TS;
 
-#[test]
-fn named() {
-    #[derive(TS)]
-    struct HasT {
-        t: &'static T<'static>,
-    }
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/self_referential/")]
+struct HasT {
+    t: &'static T<'static>,
+}
 
-    #[derive(TS)]
-    struct T<'a> {
-        t_box: Box<T<'a>>,
-        self_box: Box<Self>,
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/self_referential/")]
+struct T<'a> {
+    t_box: Box<T<'a>>,
+    self_box: Box<Self>,
 
-        t_ref: &'a T<'a>,
-        self_ref: &'a Self,
+    t_ref: &'a T<'a>,
+    self_ref: &'a Self,
 
-        t_arc: Arc<T<'a>>,
-        self_arc: Arc<Self>,
+    t_arc: Arc<T<'a>>,
+    self_arc: Arc<Self>,
 
-        #[ts(inline)]
-        has_t: HasT,
-    }
+    #[ts(inline)]
+    has_t: HasT,
+}
 
+#[test]
+fn named() {
     assert_eq!(
         T::decl(),
         "type T = { \
@@ -40,145 +44,145 @@ fn named() {
     );
 }
 
-#[test]
-fn enum_externally_tagged() {
-    #[derive(TS)]
-    #[ts(rename = "T")]
-    enum ExternallyTagged {
-        A(Box<ExternallyTagged>),
-        B(&'static ExternallyTagged),
-        C(Box<Self>),
-        D(&'static Self),
-        E(
-            Box<ExternallyTagged>,
-            Box<Self>,
-            &'static ExternallyTagged,
-            &'static Self,
-        ),
-        F {
-            a: Box<Self>,
-            b: &'static ExternallyTagged,
-            c: HashMap<String, ExternallyTagged>,
-            d: Option<Arc<ExternallyTagged>>,
-            #[ts(optional = nullable)]
-            e: Option<Arc<ExternallyTagged>>,
-            #[ts(optional)]
-            f: Option<Arc<ExternallyTagged>>,
-        },
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/self_referential/", rename = "E")]
+enum ExternallyTagged {
+    A(Box<ExternallyTagged>),
+    B(&'static ExternallyTagged),
+    C(Box<Self>),
+    D(&'static Self),
+    E(
+        Box<ExternallyTagged>,
+        Box<Self>,
+        &'static ExternallyTagged,
+        &'static Self,
+    ),
+    F {
+        a: Box<Self>,
+        b: &'static ExternallyTagged,
+        c: HashMap<String, ExternallyTagged>,
+        d: Option<Arc<ExternallyTagged>>,
+        #[ts(optional = nullable)]
+        e: Option<Arc<ExternallyTagged>>,
+        #[ts(optional)]
+        f: Option<Arc<ExternallyTagged>>,
+    },
 
-        G(
-            Vec<ExternallyTagged>,
-            [&'static ExternallyTagged; 1024],
-            HashMap<String, ExternallyTagged>,
-        ),
-    }
+    G(
+        Vec<ExternallyTagged>,
+        [&'static ExternallyTagged; 1024],
+        HashMap<String, ExternallyTagged>,
+    ),
+}
 
+#[test]
+fn enum_externally_tagged() {
     assert_eq!(
         ExternallyTagged::decl(),
-       "type T = { \"A\": T } | \
-                 { \"B\": T } | \
-                 { \"C\": T } | \
-                 { \"D\": T } | \
-                 { \"E\": [T, T, T, T] } | \
-                 { \"F\": { a: T, b: T, c: Record<string, T>, d: T | null, e?: T | null, f?: T, } } | \
-                 { \"G\": [Array<T>, Array<T>, Record<string, T>] };"
+       "type E = { \"A\": E } | \
+                 { \"B\": E } | \
+                 { \"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>] };"
     );
 }
 
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(rename = "I")]
+#[cfg_attr(feature = "serde-compat", serde(tag = "tag"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "tag"))]
+enum InternallyTagged {
+    A(Box<InternallyTagged>),
+    B(&'static InternallyTagged),
+    C(Box<Self>),
+    D(&'static Self),
+    E(Vec<Self>),
+    F {
+        a: Box<Self>,
+        b: &'static InternallyTagged,
+        c: HashMap<InternallyTagged, InternallyTagged>,
+        d: Option<&'static InternallyTagged>,
+        #[ts(optional = nullable)]
+        e: Option<&'static InternallyTagged>,
+        #[ts(optional)]
+        f: Option<&'static InternallyTagged>,
+    },
+}
+
 // NOTE: The generated type is actually not valid TS here, since the indirections rust enforces for recursive types
 //       gets lost during the translation to TypeScript (e.g "Box<T>" => "T").
 #[test]
-#[cfg(feature = "serde-compat")]
 fn enum_internally_tagged() {
-    use serde::Serialize;
-    #[derive(Serialize, TS)]
-    #[ts(rename = "T")]
-    #[serde(tag = "tag")]
-    enum InternallyTagged {
-        A(Box<InternallyTagged>),
-        B(&'static InternallyTagged),
-        C(Box<Self>),
-        D(&'static Self),
-        E(Vec<Self>),
-        F {
-            a: Box<Self>,
-            b: &'static InternallyTagged,
-            c: HashMap<InternallyTagged, InternallyTagged>,
-            d: Option<&'static InternallyTagged>,
-            #[ts(optional = nullable)]
-            e: Option<&'static InternallyTagged>,
-            #[ts(optional)]
-            f: Option<&'static InternallyTagged>,
-        },
-    }
-
     assert_eq!(
         InternallyTagged::decl(),
-        "type T = { \"tag\": \"A\" } & T | \
-                  { \"tag\": \"B\" } & T | \
-                  { \"tag\": \"C\" } & T | \
-                  { \"tag\": \"D\" } & T | \
-                  { \"tag\": \"E\" } & Array<T> | \
-                  { \"tag\": \"F\", a: T, b: T, c: Record<T, T>, d: T | null, e?: T | null, f?: T, };"
+        "type I = { \"tag\": \"A\" } & I | \
+                  { \"tag\": \"B\" } & I | \
+                  { \"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, };"
     );
 }
 
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "tests-out/self_referential/", rename = "A")]
+#[cfg_attr(feature = "serde-compat", serde(tag = "tag", content = "content"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "tag", content = "content"))]
+enum AdjacentlyTagged {
+    A(Box<AdjacentlyTagged>),
+    B(&'static AdjacentlyTagged),
+    C(Box<Self>),
+    D(&'static Self),
+    E(Vec<Self>),
+    F {
+        a: Box<Self>,
+        b: &'static AdjacentlyTagged,
+        c: HashMap<String, AdjacentlyTagged>,
+        d: Option<&'static AdjacentlyTagged>,
+        #[ts(optional = nullable)]
+        e: Option<&'static AdjacentlyTagged>,
+        #[ts(optional)]
+        f: Option<&'static AdjacentlyTagged>,
+    },
+    G(
+        Vec<Self>,
+        [&'static AdjacentlyTagged; 4],
+        HashMap<String, AdjacentlyTagged>,
+    ),
+}
+
 // NOTE: The generated type is actually not valid TS here, since the indirections rust enforces for recursive types
 //       gets lost during the translation to TypeScript (e.g "Box<T>" => "T").
 #[test]
-#[cfg(feature = "serde-compat")]
 fn enum_adjacently_tagged() {
-    use serde::Serialize;
-    #[derive(Serialize, TS)]
-    #[ts(rename = "T")]
-    #[serde(tag = "tag", content = "content")]
-    enum AdjacentlyTagged {
-        A(Box<AdjacentlyTagged>),
-        B(&'static AdjacentlyTagged),
-        C(Box<Self>),
-        D(&'static Self),
-        E(Vec<Self>),
-        F {
-            a: Box<Self>,
-            b: &'static AdjacentlyTagged,
-            c: HashMap<String, AdjacentlyTagged>,
-            d: Option<&'static AdjacentlyTagged>,
-            #[ts(optional = nullable)]
-            e: Option<&'static AdjacentlyTagged>,
-            #[ts(optional)]
-            f: Option<&'static AdjacentlyTagged>,
-        },
-        G(
-            Vec<Self>,
-            [&'static AdjacentlyTagged; 4],
-            HashMap<String, AdjacentlyTagged>,
-        ),
-    }
-
     assert_eq!(
         AdjacentlyTagged::decl(),
-        "type T = { \"tag\": \"A\", \"content\": T } | \
-                  { \"tag\": \"B\", \"content\": T } | \
-                  { \"tag\": \"C\", \"content\": T } | \
-                  { \"tag\": \"D\", \"content\": T } | \
-                  { \"tag\": \"E\", \"content\": Array<T> } | \
+        "type A = { \"tag\": \"A\", \"content\": A } | \
+                  { \"tag\": \"B\", \"content\": A } | \
+                  { \"tag\": \"C\", \"content\": A } | \
+                  { \"tag\": \"D\", \"content\": A } | \
+                  { \"tag\": \"E\", \"content\": Array<A> } | \
                   { \
                      \"tag\": \"F\", \
                      \"content\": { \
-                         a: T, \
-                         b: T, \
-                         c: Record<string, T>, \
-                         d: T | null, \
-                         e?: T | null, \
-                         f?: T, \
+                         a: A, \
+                         b: A, \
+                         c: Record<string, A>, \
+                         d: A | null, \
+                         e?: A | null, \
+                         f?: A, \
                      } \
                   } | \
                   { \
                      \"tag\": \"G\", \
                      \"content\": [\
-                         Array<T>, \
-                         [T, T, T, T], \
-                         Record<string, T>\
+                         Array<A>, \
+                         [A, A, A, A], \
+                         Record<string, A>\
                      ] \
                   };"
     );
diff --git a/ts-rs/tests/semver.rs b/ts-rs/tests/semver.rs
index 61800ed..07e668e 100644
--- a/ts-rs/tests/semver.rs
+++ b/ts-rs/tests/semver.rs
@@ -1,15 +1,16 @@
+#![allow(dead_code)]
 #![cfg(feature = "semver-impl")]
 
 use semver::Version;
 use ts_rs::TS;
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/semver/")]
+struct Semver {
+    version: Version,
+}
+
 #[test]
 fn semver() {
-    #[derive(TS)]
-    struct Semver {
-        #[allow(dead_code)]
-        version: Version,
-    }
-
     assert_eq!(Semver::decl(), "type Semver = { version: string, };")
 }
diff --git a/ts-rs/tests/serde-skip-with-default.rs b/ts-rs/tests/serde-skip-with-default.rs
index d6a5be9..ee5e280 100644
--- a/ts-rs/tests/serde-skip-with-default.rs
+++ b/ts-rs/tests/serde-skip-with-default.rs
@@ -10,7 +10,7 @@ fn default_http_version() -> String {
 }
 
 #[derive(Debug, Clone, Deserialize, Serialize, TS)]
-#[ts(export)]
+#[ts(export, export_to = "tests-out/serde_skip_with_default/")]
 pub struct Foobar {
     #[ts(skip)]
     #[serde(skip, default = "default_http_version")]
diff --git a/ts-rs/tests/simple.rs b/ts-rs/tests/simple.rs
index 038b584..1ca3b85 100644
--- a/ts-rs/tests/simple.rs
+++ b/ts-rs/tests/simple.rs
@@ -5,6 +5,7 @@ use std::cell::RefCell;
 use ts_rs::TS;
 
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/simple/")]
 struct Simple {
     a: i32,
     b: String,
diff --git a/ts-rs/tests/skip.rs b/ts-rs/tests/skip.rs
index f730760..3373239 100644
--- a/ts-rs/tests/skip.rs
+++ b/ts-rs/tests/skip.rs
@@ -1,55 +1,58 @@
 #![allow(dead_code, unused_imports)]
 
 use std::error::Error;
+
 use serde::Serialize;
 use ts_rs::TS;
 
 struct Unsupported;
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/skip/")]
+struct Skip {
+    a: i32,
+    b: i32,
+    #[ts(skip)]
+    c: String,
+    #[ts(skip)]
+    d: Box<dyn Error>,
+}
+
 #[test]
 fn simple() {
-    #[derive(TS)]
-    struct Skip {
-        a: i32,
-        b: i32,
-        #[ts(skip)]
-        c: String,
-        #[ts(skip)]
-        d: Box<dyn Error>
-    }
-
     assert_eq!(Skip::inline(), "{ a: number, b: number, }");
 }
 
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "tests-out/skip/")]
+enum Externally {
+    A(
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        Unsupported,
+    ),
+    B(
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        Unsupported,
+        i32,
+    ),
+    C {
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        x: Unsupported,
+    },
+    D {
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        x: Unsupported,
+        y: i32,
+    },
+}
+
 #[test]
 fn externally_tagged() {
-    #[cfg_attr(feature = "serde-compat", derive(Serialize, TS))]
-    #[cfg_attr(not(feature = "serde-compat"), derive(TS))]
-    enum Externally {
-        A (
-            #[cfg_attr(feature = "serde-compat", serde(skip))]
-            #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
-            Unsupported,
-        ),
-        B(
-            #[cfg_attr(feature = "serde-compat", serde(skip))]
-            #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
-            Unsupported,
-            i32
-        ),
-        C {
-            #[cfg_attr(feature = "serde-compat", serde(skip))]
-            #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
-            x: Unsupported,
-        },
-        D {
-            #[cfg_attr(feature = "serde-compat", serde(skip))]
-            #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
-            x: Unsupported,
-            y: i32
-        },
-    }
-
     // TODO: variant C should probably not generate `{}`
     assert_eq!(
         Externally::decl(),
@@ -57,69 +60,70 @@ fn externally_tagged() {
     );
 }
 
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "t"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "t"))]
+#[ts(export, export_to = "tests-out/skip/")]
+enum Internally {
+    A(
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        Unsupported,
+    ),
+    B {
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        x: Unsupported,
+    },
+    C {
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        x: Unsupported,
+        y: i32,
+    },
+}
+
 #[test]
 fn internally_tagged() {
-    #[cfg_attr(feature = "serde-compat", derive(Serialize, TS))]
-    #[cfg_attr(not(feature = "serde-compat"), derive(TS))]
-    #[cfg_attr(feature = "serde-compat", serde(tag = "t"))]
-    #[cfg_attr(not(feature = "serde-compat"), ts(tag = "t"))]
-    enum Internally {
-        A (
-            #[cfg_attr(feature = "serde-compat", serde(skip))]
-            #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
-            Unsupported,
-        ),
-        B {
-            #[cfg_attr(feature = "serde-compat", serde(skip))]
-            #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
-            x: Unsupported,
-        },
-        C {
-            #[cfg_attr(feature = "serde-compat", serde(skip))]
-            #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
-            x: Unsupported,
-            y: i32,
-        },
-    }
-
     assert_eq!(
         Internally::decl(),
         r#"type Internally = { "t": "A" } | { "t": "B",  } | { "t": "C", y: number, };"#
     );
 }
 
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "t", content = "c"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "t", content = "c"))]
+#[ts(export, export_to = "tests-out/skip/")]
+enum Adjacently {
+    A(
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        Unsupported,
+    ),
+    B(
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        Unsupported,
+        i32,
+    ),
+    C {
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        x: Unsupported,
+    },
+    D {
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        x: Unsupported,
+        y: i32,
+    },
+}
+
 #[test]
 fn adjacently_tagged() {
-    #[cfg_attr(feature = "serde-compat", derive(Serialize, TS))]
-    #[cfg_attr(not(feature = "serde-compat"), derive(TS))]
-    #[cfg_attr(feature = "serde-compat", serde(tag = "t", content = "c"))]
-    #[cfg_attr(not(feature = "serde-compat"), ts(tag = "t", content = "c"))]
-    enum Adjacently {
-        A (
-            #[cfg_attr(feature = "serde-compat", serde(skip))]
-            #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
-            Unsupported,
-        ),
-        B(
-            #[cfg_attr(feature = "serde-compat", serde(skip))]
-            #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
-            Unsupported,
-            i32
-        ),
-        C {
-            #[cfg_attr(feature = "serde-compat", serde(skip))]
-            #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
-            x: Unsupported,
-        },
-        D {
-            #[cfg_attr(feature = "serde-compat", serde(skip))]
-            #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
-            x: Unsupported,
-            y: i32
-        },
-
-    }
-
     // TODO: variant C should probably not generate `{ .., "c": { } }`
     assert_eq!(
         Adjacently::decl(),
diff --git a/ts-rs/tests/slices.rs b/ts-rs/tests/slices.rs
index 0e396c5..4ad938a 100644
--- a/ts-rs/tests/slices.rs
+++ b/ts-rs/tests/slices.rs
@@ -1,3 +1,5 @@
+#![allow(unused)]
+
 use ts_rs::TS;
 
 #[test]
@@ -5,33 +7,36 @@ fn free() {
     assert_eq!(<[String]>::inline(), "Array<string>")
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/slices/")]
+struct Interface {
+    #[allow(dead_code)]
+    a: [i32],
+}
+
 #[test]
 fn interface() {
-    #[derive(TS)]
-    struct Interface {
-        #[allow(dead_code)]
-        a: [i32],
-    }
-
     assert_eq!(Interface::inline(), "{ a: Array<number>, }")
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/slices/")]
+struct InterfaceRef<'a> {
+    #[allow(dead_code)]
+    a: &'a [&'a str],
+}
+
 #[test]
 fn slice_ref() {
-    #[derive(TS)]
-    struct Interface<'a> {
-        #[allow(dead_code)]
-        a: &'a [&'a str],
-    }
-
-    assert_eq!(Interface::inline(), "{ a: Array<string>, }")
+    assert_eq!(InterfaceRef::inline(), "{ a: Array<string>, }")
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/slices/")]
+struct Newtype(#[allow(dead_code)] [i32]);
+
 #[test]
 fn newtype() {
-    #[derive(TS)]
-    struct Newtype(#[allow(dead_code)] [i32]);
-
     assert_eq!(Newtype::inline(), "Array<number>")
 }
 
@@ -43,21 +48,31 @@ fn boxed_free() {
     assert_eq!(<Box<[String]>>::inline(), "Array<string>")
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/slices/")]
+struct InterfaceBoxed {
+    #[allow(dead_code)]
+    a: Box<[i32]>,
+}
+
 #[test]
 fn boxed_interface() {
-    #[derive(TS)]
-    struct Interface {
-        #[allow(dead_code)]
-        a: Box<[i32]>,
-    }
-
-    assert_eq!(Interface::inline(), "{ a: Array<number>, }")
+    assert_eq!(InterfaceBoxed::inline(), "{ a: Array<number>, }")
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/slices/")]
+struct NewtypeBoxed(#[allow(dead_code)] Box<[i32]>);
+
 #[test]
 fn boxed_newtype() {
-    #[derive(TS)]
-    struct Newtype(#[allow(dead_code)] Box<[i32]>);
-
-    assert_eq!(Newtype::inline(), "Array<number>")
+    assert_eq!(NewtypeBoxed::inline(), "Array<number>")
 }
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/slices/nested/")]
+struct InnerMost;
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/slices/nested/")]
+struct Nested<'a>(&'a [InnerMost]);
diff --git a/ts-rs/tests/struct_rename.rs b/ts-rs/tests/struct_rename.rs
index 274b7a7..b5c281e 100644
--- a/ts-rs/tests/struct_rename.rs
+++ b/ts-rs/tests/struct_rename.rs
@@ -1,56 +1,76 @@
+#![allow(non_snake_case)]
 #![allow(dead_code)]
 
 use ts_rs::TS;
 
+#[derive(TS)]
+#[ts(
+    export,
+    export_to = "tests-out/struct_rename/",
+    rename_all = "UPPERCASE"
+)]
+struct RenameAllUpper {
+    a: i32,
+    b: i32,
+}
+
 #[test]
 fn rename_all() {
-    #[derive(TS)]
-    #[ts(rename_all = "UPPERCASE")]
-    struct Rename {
-        a: i32,
-        b: i32,
-    }
+    assert_eq!(RenameAllUpper::inline(), "{ A: number, B: number, }");
+}
 
-    assert_eq!(Rename::inline(), "{ A: number, B: number, }");
+#[derive(TS)]
+#[ts(
+    export,
+    export_to = "tests-out/struct_rename/",
+    rename_all = "camelCase"
+)]
+struct RenameAllCamel {
+    crc32c_hash: i32,
+    b: i32,
+    alreadyCamelCase: i32,
 }
 
 #[test]
 fn rename_all_camel_case() {
-    #[derive(TS)]
-    #[ts(rename_all = "camelCase")]
-    #[allow(non_snake_case)]
-    struct Rename {
-        crc32c_hash: i32,
-        b: i32,
-        alreadyCamelCase: i32,
-    }
-
     assert_eq!(
-        Rename::inline(),
+        RenameAllCamel::inline(),
         "{ crc32cHash: number, b: number, alreadyCamelCase: number, }"
     );
 }
 
+#[derive(TS)]
+#[ts(
+    export,
+    export_to = "tests-out/struct_rename/",
+    rename_all = "PascalCase"
+)]
+struct RenameAllPascal {
+    crc32c_hash: i32,
+    b: i32,
+}
+
 #[test]
 fn rename_all_pascal_case() {
-    #[derive(TS)]
-    #[ts(rename_all = "PascalCase")]
-    struct Rename {
-        crc32c_hash: i32,
-        b: i32,
-    }
+    assert_eq!(
+        RenameAllPascal::inline(),
+        "{ Crc32cHash: number, B: number, }"
+    );
+}
 
-    assert_eq!(Rename::inline(), "{ Crc32cHash: number, B: number, }");
+#[derive(serde::Serialize, TS)]
+#[ts(
+    export,
+    export_to = "tests-out/struct_rename/",
+    rename_all = "camelCase"
+)]
+struct RenameSerdeSpecialChar {
+    #[serde(rename = "a/b")]
+    b: i32,
 }
 
 #[cfg(feature = "serde-compat")]
 #[test]
 fn serde_rename_special_char() {
-    #[derive(serde::Serialize, TS)]
-    struct RenameSerdeSpecialChar {
-        #[serde(rename = "a/b")]
-        b: i32,
-    }
-
     assert_eq!(RenameSerdeSpecialChar::inline(), r#"{ "a/b": number, }"#);
 }
diff --git a/ts-rs/tests/type_as.rs b/ts-rs/tests/type_as.rs
index ff5bbb5..bf54f27 100644
--- a/ts-rs/tests/type_as.rs
+++ b/ts-rs/tests/type_as.rs
@@ -9,25 +9,27 @@ use ts_rs::TS;
 type Unsupported = UnsafeCell<MaybeUninit<NonNull<AtomicPtr<i32>>>>;
 
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/type_as/")]
 struct ExternalTypeDef {
     a: i32,
     b: i32,
     c: i32,
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/type_as/")]
+struct Override {
+    a: i32,
+    #[ts(as = "ExternalTypeDef")]
+    #[ts(inline)]
+    x: Instant,
+    // here, 'as' just behaves like 'type' (though it adds a dependency!)
+    #[ts(as = "ExternalTypeDef")]
+    y: Unsupported,
+}
+
 #[test]
 fn struct_properties() {
-    #[derive(TS)]
-    struct Override {
-        a: i32,
-        #[ts(as = "ExternalTypeDef")]
-        #[ts(inline)]
-        x: Instant,
-        // here, 'as' just behaves like 'type' (though it adds a dependency!)
-        #[ts(as = "ExternalTypeDef")]
-        y: Unsupported,
-    }
-
     assert_eq!(
         Override::inline(),
         "{ a: number, x: { a: number, b: number, c: number, }, y: ExternalTypeDef, }"
@@ -37,37 +39,39 @@ fn struct_properties() {
         .any(|d| d.ts_name == "ExternalTypeDef"));
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/type_as/")]
+enum OverrideEnum {
+    A(#[ts(as = "ExternalTypeDef")] Instant),
+    B {
+        #[ts(as = "ExternalTypeDef")]
+        x: Unsupported,
+        y: i32,
+        z: i32,
+    },
+}
+
 #[test]
 fn enum_variants() {
-    #[derive(TS)]
-    enum OverrideEnum {
-        A(#[ts(as = "ExternalTypeDef")] Instant),
-        B {
-            #[ts(as = "ExternalTypeDef")]
-            x: Unsupported,
-            y: i32,
-            z: i32,
-        },
-    }
-
     assert_eq!(
         OverrideEnum::inline(),
         r#"{ "A": ExternalTypeDef } | { "B": { x: ExternalTypeDef, y: number, z: number, } }"#
     )
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/type_as/")]
+struct Outer {
+    #[ts(as = "Option<ExternalTypeDef>")]
+    #[ts(optional = nullable, inline)]
+    x: Unsupported,
+    #[ts(as = "Option<ExternalTypeDef>")]
+    #[ts(optional = nullable)]
+    y: Unsupported,
+}
+
 #[test]
 fn complex() {
-    #[derive(TS)]
-    struct Outer {
-        #[ts(as = "Option<ExternalTypeDef>")]
-        #[ts(optional = nullable, inline)]
-        x: Unsupported,
-        #[ts(as = "Option<ExternalTypeDef>")]
-        #[ts(optional = nullable)]
-        y: Unsupported,
-    }
-
     let external = ExternalTypeDef::inline();
     assert_eq!(
         Outer::inline(),
diff --git a/ts-rs/tests/type_override.rs b/ts-rs/tests/type_override.rs
index 41b8459..dcd4da9 100644
--- a/ts-rs/tests/type_override.rs
+++ b/ts-rs/tests/type_override.rs
@@ -2,65 +2,73 @@
 
 use std::time::Instant;
 
+#[cfg(feature = "serde-compat")]
+use serde::Serialize;
 use ts_rs::TS;
 
 struct Unsupported<T>(T);
 struct Unsupported2;
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/type_override/")]
+struct Override {
+    a: i32,
+    #[ts(type = "0 | 1 | 2")]
+    b: i32,
+    #[ts(type = "string")]
+    x: Instant,
+    #[ts(type = "string")]
+    y: Unsupported<Unsupported<Unsupported2>>,
+    #[ts(type = "string | null")]
+    z: Option<Unsupported2>,
+}
+
 #[test]
 fn simple() {
-    #[derive(TS)]
-    struct Override {
-        a: i32,
-        #[ts(type = "0 | 1 | 2")]
-        b: i32,
-        #[ts(type = "string")]
-        x: Instant,
-        #[ts(type = "string")]
-        y: Unsupported<Unsupported<Unsupported2>>,
-        #[ts(type = "string | null")]
-        z: Option<Unsupported2>,
-    }
-
     assert_eq!(
         Override::inline(),
         "{ a: number, b: 0 | 1 | 2, x: string, y: string, z: string | null, }"
     )
 }
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/type_override/")]
+struct New1(#[ts(type = "string")] Unsupported2);
+
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/type_override/")]
+struct New2(#[ts(type = "string | null")] Unsupported<Unsupported2>);
+
 #[test]
 fn newtype() {
-    #[derive(TS)]
-    struct New1(#[ts(type = "string")] Unsupported2);
-    #[derive(TS)]
-    struct New2(#[ts(type = "string | null")] Unsupported<Unsupported2>);
-
     assert_eq!(New1::inline(), r#"string"#);
     assert_eq!(New2::inline(), r#"string | null"#);
 }
 
-#[test]
-#[cfg(feature = "serde-compat")]
-fn enum_newtype_representations() {
-    // regression test for https://github.com/Aleph-Alpha/ts-rs/issues/126
-
-    use serde::Serialize;
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+struct S;
 
-    #[derive(Serialize)]
-    struct S;
-
-    #[derive(TS, Serialize)]
-    #[serde(tag = "t")]
-    enum Internal {
-        Newtype(#[ts(type = "unknown")] S),
-    }
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "t"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "t"))]
+#[ts(export, export_to = "tests-out/type_override/")]
+enum Internal {
+    Newtype(#[ts(type = "unknown")] S),
+}
 
-    #[derive(TS, Serialize)]
-    #[serde(tag = "t", content = "c")]
-    enum Adjacent {
-        Newtype(#[ts(type = "unknown")] S),
-    }
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "t", content = "c"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "t", content = "c"))]
+#[ts(export, export_to = "tests-out/type_override/")]
+enum Adjacent {
+    Newtype(#[ts(type = "unknown")] S),
+}
 
+#[test]
+fn enum_newtype_representations() {
+    // regression test for https://github.com/Aleph-Alpha/ts-rs/issues/126
     assert_eq!(Internal::inline(), r#"{ "t": "Newtype" } & unknown"#);
     assert_eq!(Adjacent::inline(), r#"{ "t": "Newtype", "c": unknown }"#);
 }
diff --git a/ts-rs/tests/union.rs b/ts-rs/tests/union.rs
index 1f7cf10..e82a430 100644
--- a/ts-rs/tests/union.rs
+++ b/ts-rs/tests/union.rs
@@ -3,6 +3,7 @@
 use ts_rs::TS;
 
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/union/")]
 enum SimpleEnum {
     #[ts(rename = "asdf")]
     A,
diff --git a/ts-rs/tests/union_named_serde_skip.rs b/ts-rs/tests/union_named_serde_skip.rs
index 804e058..5bc0ea7 100644
--- a/ts-rs/tests/union_named_serde_skip.rs
+++ b/ts-rs/tests/union_named_serde_skip.rs
@@ -1,52 +1,67 @@
 #![allow(dead_code)]
 
+#[cfg(feature = "serde-compat")]
 use serde::Deserialize;
 use ts_rs::TS;
 
-#[derive(TS, Deserialize)]
-#[serde(untagged)]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Deserialize))]
+#[cfg_attr(feature = "serde-compat", serde(untagged))]
+#[cfg_attr(not(feature = "serde-compat"), ts(untagged))]
+#[ts(export, export_to = "tests-out/union_named_serde/")]
 enum TestUntagged {
     A,   // serde_json -> `null`
     B(), // serde_json -> `[]`
     C {
-        #[serde(skip)]
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
         val: i32,
     }, // serde_json -> `{}`
 }
 
-#[derive(TS, Deserialize)]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Deserialize))]
+#[ts(export, export_to = "tests-out/union_named_serde/")]
 enum TestExternally {
     A,   // serde_json -> `"A"`
     B(), // serde_json -> `{"B":[]}`
     C {
-        #[serde(skip)]
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
         val: i32,
     }, // serde_json -> `{"C":{}}`
 }
 
-#[derive(TS, Deserialize)]
-#[serde(tag = "type", content = "content")]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Deserialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "type", content = "content"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "type", content = "content"))]
+#[ts(export, export_to = "tests-out/union_named_serde/")]
 enum TestAdjacently {
     A,   // serde_json -> `{"type":"A"}`
     B(), // serde_json -> `{"type":"B","content":[]}`
     C {
-        #[serde(skip)]
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
         val: i32,
     }, // serde_json -> `{"type":"C","content":{}}`
 }
 
-#[derive(TS, Deserialize)]
-#[serde(tag = "type")]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Deserialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "type"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))]
+#[ts(export, export_to = "tests-out/union_named_serde/")]
 enum TestInternally {
     A, // serde_json -> `{"type":"A"}`
     B, // serde_json -> `{"type":"B"}`
     C {
-        #[serde(skip)]
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
         val: i32,
     }, // serde_json -> `{"type":"C"}`
 }
 
-#[cfg(feature = "serde-compat")]
 #[test]
 fn test() {
     assert_eq!(
diff --git a/ts-rs/tests/union_rename.rs b/ts-rs/tests/union_rename.rs
index abc714c..e6ef007 100644
--- a/ts-rs/tests/union_rename.rs
+++ b/ts-rs/tests/union_rename.rs
@@ -3,8 +3,8 @@
 use ts_rs::TS;
 
 #[derive(TS)]
-#[ts(rename_all = "lowercase")]
-#[ts(rename = "SimpleEnum")]
+#[ts(export, export_to = "tests-out/union_rename/")]
+#[ts(rename_all = "lowercase", rename = "SimpleEnum")]
 enum RenamedEnum {
     #[ts(rename = "ASDF")]
     A,
diff --git a/ts-rs/tests/union_serde.rs b/ts-rs/tests/union_serde.rs
index 6996e31..c0ae613 100644
--- a/ts-rs/tests/union_serde.rs
+++ b/ts-rs/tests/union_serde.rs
@@ -1,17 +1,24 @@
 #![allow(dead_code)]
 
+#[cfg(feature = "serde-compat")]
 use serde::Deserialize;
 use ts_rs::TS;
 
-#[derive(TS, Deserialize)]
-#[serde(tag = "kind", content = "d")]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Deserialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "kind", content = "d"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "kind", content = "d"))]
+#[ts(export, export_to = "tests-out/union_serde/")]
 enum SimpleEnum {
     A,
     B,
 }
 
-#[derive(TS, Deserialize)]
-#[serde(tag = "kind", content = "data")]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Deserialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "kind", content = "data"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "kind", content = "data"))]
+#[ts(export, export_to = "tests-out/union_serde/")]
 enum ComplexEnum {
     A,
     B { foo: String, bar: f64 },
@@ -20,15 +27,17 @@ enum ComplexEnum {
     T(i32, SimpleEnum),
 }
 
-#[derive(TS, Deserialize)]
-#[serde(untagged)]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Deserialize))]
+#[cfg_attr(feature = "serde-compat", serde(untagged))]
+#[cfg_attr(not(feature = "serde-compat"), ts(untagged))]
+#[ts(export, export_to = "tests-out/union_serde/")]
 enum Untagged {
     Foo(String),
     Bar(i32),
     None,
 }
 
-#[cfg(feature = "serde-compat")]
 #[test]
 fn test_serde_enum() {
     assert_eq!(
diff --git a/ts-rs/tests/union_unnamed_serde_skip.rs b/ts-rs/tests/union_unnamed_serde_skip.rs
index 1dfae1c..b022c26 100644
--- a/ts-rs/tests/union_unnamed_serde_skip.rs
+++ b/ts-rs/tests/union_unnamed_serde_skip.rs
@@ -1,40 +1,67 @@
 #![allow(dead_code)]
 
+#[cfg(feature = "serde-compat")]
 use serde::Deserialize;
 use ts_rs::TS;
 
-#[derive(TS, Deserialize)]
-#[serde(untagged)]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Deserialize))]
+#[cfg_attr(feature = "serde-compat", serde(untagged))]
+#[cfg_attr(not(feature = "serde-compat"), ts(untagged))]
+#[ts(export, export_to = "tests-out/union_unnamed_serde/")]
 enum TestUntagged {
-    A,                     // serde_json -> `null`
-    B(),                   // serde_json -> `[]`
-    C(#[serde(skip)] i32), // serde_json -> `null`
+    A,   // serde_json -> `null`
+    B(), // serde_json -> `[]`
+    C(
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        i32,
+    ), // serde_json -> `null`
 }
 
-#[derive(TS, Deserialize)]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Deserialize))]
+#[ts(export, export_to = "tests-out/union_unnamed_serde/")]
 enum TestExternally {
-    A,                     // serde_json -> `"A"`
-    B(),                   // serde_json -> `{"B":[]}`
-    C(#[serde(skip)] i32), // serde_json -> `"C"`
+    A,   // serde_json -> `"A"`
+    B(), // serde_json -> `{"B":[]}`
+    C(
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        i32,
+    ), // serde_json -> `"C"`
 }
 
-#[derive(TS, Deserialize)]
-#[serde(tag = "type", content = "content")]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Deserialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "type", content = "content"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "type", content = "content"))]
+#[ts(export, export_to = "tests-out/union_unnamed_serde/")]
 enum TestAdjacently {
-    A,                     // serde_json -> `{"type":"A"}`
-    B(),                   // serde_json -> `{"type":"B","content":[]}`
-    C(#[serde(skip)] i32), // serde_json -> `{"type":"C"}`
+    A,   // serde_json -> `{"type":"A"}`
+    B(), // serde_json -> `{"type":"B","content":[]}`
+    C(
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        i32,
+    ), // serde_json -> `{"type":"C"}`
 }
 
-#[derive(TS, Deserialize)]
-#[serde(tag = "type")]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Deserialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "type"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))]
+#[ts(export, export_to = "tests-out/union_unnamed_serde/")]
 enum TestInternally {
-    A,                     // serde_json -> `{"type":"A"}`
-    B,                     // serde_json -> `{"type":"B"}`
-    C(#[serde(skip)] i32), // serde_json -> `{"type":"C"}`
+    A, // serde_json -> `{"type":"A"}`
+    B, // serde_json -> `{"type":"B"}`
+    C(
+        #[cfg_attr(feature = "serde-compat", serde(skip))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(skip))]
+        i32,
+    ), // serde_json -> `{"type":"C"}`
 }
 
-#[cfg(feature = "serde-compat")]
 #[test]
 fn test() {
     assert_eq!(
diff --git a/ts-rs/tests/union_with_data.rs b/ts-rs/tests/union_with_data.rs
index 43ff1a9..aeca5f2 100644
--- a/ts-rs/tests/union_with_data.rs
+++ b/ts-rs/tests/union_with_data.rs
@@ -1,18 +1,26 @@
 #![allow(dead_code)]
+
+#[cfg(feature = "serde-compat")]
 use serde::Serialize;
 use ts_rs::{Dependency, TS};
 
-#[derive(TS, Serialize)]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "tests-out/union_with_data/")]
 struct Bar {
     field: i32,
 }
 
-#[derive(TS, Serialize)]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "tests-out/union_with_data/")]
 struct Foo {
     bar: Bar,
 }
 
-#[derive(TS, Serialize)]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "tests-out/union_with_data/")]
 enum SimpleEnum {
     A(String),
     B(i32),
diff --git a/ts-rs/tests/union_with_internal_tag.rs b/ts-rs/tests/union_with_internal_tag.rs
index e7382ee..d330e51 100644
--- a/ts-rs/tests/union_with_internal_tag.rs
+++ b/ts-rs/tests/union_with_internal_tag.rs
@@ -1,34 +1,44 @@
 #![allow(dead_code, clippy::disallowed_names)]
 
+#[cfg(feature = "serde-compat")]
 use serde::Serialize;
 use ts_rs::TS;
 
-#[derive(Serialize, TS)]
-#[serde(tag = "type")]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "type"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))]
+#[ts(export, export_to = "tests-out/union_with_internal_tag/")]
 enum EnumWithInternalTag {
     A { foo: String },
     B { bar: i32 },
 }
 
-#[derive(Serialize, TS)]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "tests-out/union_with_internal_tag/")]
 struct InnerA {
     foo: String,
 }
 
-#[derive(Serialize, TS)]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[ts(export, export_to = "tests-out/union_with_internal_tag/")]
 struct InnerB {
     bar: i32,
 }
 
-#[derive(Serialize, TS)]
-#[serde(tag = "type")]
+#[derive(TS)]
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[cfg_attr(feature = "serde-compat", serde(tag = "type"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))]
+#[ts(export, export_to = "tests-out/union_with_internal_tag/")]
 enum EnumWithInternalTag2 {
     A(InnerA),
     B(InnerB),
 }
 
 #[test]
-#[cfg(feature = "serde-compat")]
 fn test_enums_with_internal_tags() {
     assert_eq!(
         EnumWithInternalTag::decl(),
diff --git a/ts-rs/tests/unit.rs b/ts-rs/tests/unit.rs
index c6fdcf5..f415c2d 100644
--- a/ts-rs/tests/unit.rs
+++ b/ts-rs/tests/unit.rs
@@ -2,20 +2,24 @@ use ts_rs::TS;
 
 // serde_json serializes this to `null`, so it's TS type is `null` as well.
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/unit/")]
 struct Unit;
 
 // serde_json serializes this to `{}`.
 // The TS type best describing an empty object is `Record<string, never>`.
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/unit/")]
 struct Unit2 {}
 
 // serde_json serializes this to `[]`.
 // The TS type best describing an empty array is `never[]`.
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/unit/")]
 struct Unit3();
 
 // serde_json serializes this to `null`, so it's TS type is `null` as well.
 #[derive(TS)]
+#[ts(export, export_to = "tests-out/unit/")]
 struct Unit4(());
 
 #[test]
diff --git a/ts-rs/tests/unsized.rs b/ts-rs/tests/unsized.rs
index 9077665..34450ab 100644
--- a/ts-rs/tests/unsized.rs
+++ b/ts-rs/tests/unsized.rs
@@ -1,18 +1,20 @@
+#![allow(dead_code)]
+
 use std::{borrow::Cow, rc::Rc, sync::Arc};
 
 use ts_rs::TS;
 
+#[derive(TS)]
+#[ts(export, export_to = "tests-out/unsized/")]
+struct S<'a> {
+    b: Box<str>,
+    c: Cow<'a, str>,
+    r: Rc<str>,
+    a: Arc<str>,
+}
+
 #[test]
 fn contains_str() {
-    #[derive(TS)]
-    #[allow(dead_code)]
-    struct S<'a> {
-        b: Box<str>,
-        c: Cow<'a, str>,
-        r: Rc<str>,
-        a: Arc<str>,
-    }
-
     assert_eq!(
         S::decl(),
         "type S = { b: string, c: string, r: string, a: string, };"