Repository: Aleph-Alpha/ts-rs
Pull request #233 (2024-02-12 17:58:41)

Bug: Flattening generic type also flattens all generic parameters
Hey @escritorio-gustavo!
While trying to understand #232, I encountered this issue here. I'm not yet sure if it's the underlying cause, but maybe we should start with this.

Flattening an generic enum currently seems to not only flatten the enum, but also everything inside.  
I've added a test to highlight this behaviour.

Any ideas what's going on here?


Patch:
diff --git a/README.md b/README.md
index 971eeed..9501631 100644
--- a/README.md
+++ b/README.md
@@ -68,10 +68,6 @@ When running `cargo test`, the TypeScript bindings will be exported to the file
 - generic types
 - support for ESM imports
 
-### limitations
-- generic fields cannot be inlined or flattened (#56)
-- type aliases must not alias generic types (#70)
-
 ### cargo features
 - `serde-compat` (default)
 
diff --git a/macros/src/deps.rs b/macros/src/deps.rs
index b996056..735df35 100644
--- a/macros/src/deps.rs
+++ b/macros/src/deps.rs
@@ -12,9 +12,9 @@ impl Dependencies {
             .push(quote![.extend(<#ty as ts_rs::TS>::dependency_types())]);
     }
 
-    /// Adds the given type if it's *not* transparent.
-    /// If it is, all it's child dependencies are added instead.
-    pub fn push_or_append_from(&mut self, ty: &Type) {
+    /// Adds the given type.
+    /// 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>()]);
     }
 
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 11b87a8..7844fa7 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -9,6 +9,7 @@ use syn::{
 };
 
 use crate::deps::Dependencies;
+use crate::utils::format_generics;
 
 #[macro_use]
 mod utils;
@@ -17,10 +18,10 @@ mod deps;
 mod types;
 
 struct DerivedTS {
-    name: String,
+    generics: Generics,
+    ts_name: String,
     docs: String,
     inline: TokenStream,
-    decl: TokenStream,
     inline_flattened: Option<TokenStream>,
     dependencies: Dependencies,
 
@@ -29,29 +30,11 @@ struct DerivedTS {
 }
 
 impl DerivedTS {
-    fn generate_export_test(&self, rust_ty: &Ident, generics: &Generics) -> Option<TokenStream> {
-        let test_fn = format_ident!("export_bindings_{}", &self.name.to_lowercase());
-        let generic_params = generics
-            .params
-            .iter()
-            .filter(|param| matches!(param, GenericParam::Type(_)))
-            .map(|_| quote! { () });
-        let ty = quote!(<#rust_ty<#(#generic_params),*> as ts_rs::TS>);
-
-        Some(quote! {
-            #[cfg(test)]
-            #[test]
-            fn #test_fn() {
-                #ty::export().expect("could not export type");
-            }
-        })
-    }
-
-    fn into_impl(self, rust_ty: Ident, generics: Generics) -> TokenStream {
+    fn into_impl(mut self, rust_ty: Ident, generics: Generics) -> TokenStream {
         let mut get_export_to = quote! {};
         let export_to = match &self.export_to {
             Some(dirname) if dirname.ends_with('/') => {
-                format!("{}{}.ts", dirname, self.name)
+                format!("{}{}.ts", dirname, self.ts_name)
             }
             Some(filename) => filename.clone(),
             None => {
@@ -60,60 +43,39 @@ impl DerivedTS {
                         ts_rs::__private::get_export_to_path::<Self>()
                     }
                 };
-                format!("bindings/{}.ts", self.name)
+                format!("bindings/{}.ts", self.ts_name)
             }
         };
 
-        let export = match self.export {
-            true => Some(self.generate_export_test(&rust_ty, &generics)),
-            false => None,
-        };
-
-        let DerivedTS {
-            name,
-            docs,
-            inline,
-            decl,
-            inline_flattened,
-            dependencies,
-            ..
-        } = self;
+        let export = self.export.then(
+            || self.generate_export_test(&rust_ty, &generics)
+        );
 
-        let docs = match docs.is_empty() {
-            true => None,
-            false => {
-                Some(quote!(const DOCS: Option<&'static str> = Some(#docs);))
-            }
+        let docs = match &*self.docs {
+            "" => None,
+            docs => Some(quote!(const DOCS: Option<&'static str> = Some(#docs);)),
         };
 
-        let inline_flattened = inline_flattened
-            .map(|t| {
-                quote! {
-                    fn inline_flattened() -> String {
-                        #t
-                    }
-                }
-            })
-            .unwrap_or_else(TokenStream::new);
+        let ident = self.ts_name.clone();
+        let impl_start = generate_impl_block_header(&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 impl_start = generate_impl(&rust_ty, &generics);
         quote! {
             #impl_start {
                 const EXPORT_TO: Option<&'static str> = Some(#export_to);
-                #get_export_to
 
-                #docs
-
-                fn decl() -> String {
-                    #decl
-                }
-                fn name() -> String {
-                    #name.to_owned()
+                fn ident() -> String {
+                    #ident.to_owned()
                 }
-                fn inline() -> String {
-                    #inline
-                }
-                #inline_flattened
+                
+                #get_export_to
+                #docs
+                #name
+                #decl
+                #inline
 
                 #[allow(clippy::unused_unit)]
                 fn dependency_types() -> impl ts_rs::typelist::TypeList
@@ -131,26 +93,159 @@ impl DerivedTS {
             #export
         }
     }
+
+    /// Returns an expression which evaluates to the TypeScript name of the type, including generic
+    /// parameters.
+    fn name_with_generics(&self) -> TokenStream {
+        let name = &self.ts_name;
+        let mut generics_ts_names = self
+            .generics
+            .type_params()
+            .map(|ty| &ty.ident)
+            .map(|generic| quote!(<#generic as ts_rs::TS>::name()))
+            .peekable();
+
+        if generics_ts_names.peek().is_some() {
+            quote! {
+                format!("{}<{}>", #name, vec![#(#generics_ts_names),*].join(", "))
+            }
+        } else {
+            quote!(#name.to_owned())
+        }
+    }
+
+    /// Generate a dummy unit struct for every generic type parameter of this type.
+    /// Example:
+    /// ```ignore
+    /// struct Generic<A, B, const C: usize> { /* ... */ }
+    /// ```
+    /// has two generic type parameters, `A` and `B`. This function will therefor generate
+    /// ```ignore
+    /// struct A;
+    /// impl ts_rs::TS for A { /* .. */ }
+    ///
+    /// struct B;
+    /// impl ts_rs::TS for B { /* .. */ }
+    /// ```
+    fn generate_generic_types(&self) -> TokenStream {
+        let generics = self.generics.type_params().map(|ty| ty.ident.clone());
+
+        quote! {
+            #(
+                #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
+                struct #generics;
+                impl std::fmt::Display for #generics {
+                    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                        write!(f, "{:?}", self)
+                    }
+                }
+                impl TS for #generics {
+                    fn name() -> String { stringify!(#generics).to_owned() }
+                    fn transparent() -> bool { false }
+                }
+            )*
+        }
+    }
+
+    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 ty = quote!(<#rust_ty<#(#generic_params),*> as ts_rs::TS>);
+
+        quote! {
+            #[cfg(test)]
+            #[test]
+            fn #test_fn() {
+                #ty::export().expect("could not export type");
+            }
+        }
+    }
+
+    fn generate_name_fn(&self) -> TokenStream {
+        let name = self.name_with_generics();
+        quote! {
+             fn name() -> String {
+                 #name
+             }
+        }
+    }
+
+    fn generate_inline_fn(&self) -> TokenStream {
+        let inline = &self.inline;
+
+        let inline_flattened = self.inline_flattened.as_ref().map(|inline_flattened| {
+            quote! {
+                fn inline_flattened() -> String {
+                    #inline_flattened
+                }
+            }
+        });
+        let inline = quote! {
+            fn inline() -> String {
+                #inline
+            }
+        };
+        quote! {
+            #inline
+            #inline_flattened
+        }
+    }
+
+    /// Generates the `decl()` and `decl_concrete()` methods.
+    /// `decl_concrete()` is simple, and simply defers to `inline()`.
+    /// For `decl()`, however, we need to change out the generic parameters of the type, replacing
+    /// them with the dummy types generated by `generate_generic_types()`.
+    fn generate_decl_fn(&mut self, rust_ty: &Ident) -> TokenStream {
+        let name = &self.ts_name;
+        let generic_types = self.generate_generic_types();
+        let ts_generics = format_generics(&mut self.dependencies, &self.generics);
+
+        use GenericParam as G;
+        // These are the generic parameters we'll be using.
+        let generic_idents = self.generics.params.iter().filter_map(|p| match p {
+            G::Lifetime(_) => None,
+            // Since we named our dummy types the same as the generic parameters, we can just keep
+            // the identifier of the generic parameter - its name is shadowed by the dummy struct.
+            //
+            // 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)),
+        });
+        quote! {
+            fn decl_concrete() -> String {
+                format!("type {} = {};", #name, Self::inline())
+            }
+            fn decl() -> String {
+                #generic_types
+                let inline = <#rust_ty<#(#generic_idents,)*> as ts_rs::TS>::inline();
+                let generics = #ts_generics;
+                format!("type {}{generics} = {inline};", #name)
+            }
+        }
+    }
 }
 
 // generate start of the `impl TS for #ty` block, up to (excluding) the open brace
-fn generate_impl(ty: &Ident, generics: &Generics) -> TokenStream {
-    use GenericParam::*;
+fn generate_impl_block_header(ty: &Ident, generics: &Generics) -> TokenStream {
+    use GenericParam as G;
 
     let bounds = generics.params.iter().map(|param| match param {
-        Type(TypeParam {
+        G::Type(TypeParam {
             ident,
             colon_token,
             bounds,
             ..
         }) => quote!(#ident #colon_token #bounds),
-        Lifetime(LifetimeParam {
+        G::Lifetime(LifetimeParam {
             lifetime,
             colon_token,
             bounds,
             ..
         }) => quote!(#lifetime #colon_token #bounds),
-        Const(ConstParam {
+        G::Const(ConstParam {
             const_token,
             ident,
             colon_token,
@@ -159,8 +254,8 @@ fn generate_impl(ty: &Ident, generics: &Generics) -> TokenStream {
         }) => quote!(#const_token #ident #colon_token #ty),
     });
     let type_args = generics.params.iter().map(|param| match param {
-        Type(TypeParam { ident, .. }) | Const(ConstParam { ident, .. }) => quote!(#ident),
-        Lifetime(LifetimeParam { lifetime, .. }) => quote!(#lifetime),
+        G::Type(TypeParam { ident, .. }) | G::Const(ConstParam { ident, .. }) => quote!(#ident),
+        G::Lifetime(LifetimeParam { lifetime, .. }) => quote!(#lifetime),
     });
 
     let where_bound = add_ts_to_where_clause(generics);
@@ -169,12 +264,8 @@ fn generate_impl(ty: &Ident, generics: &Generics) -> TokenStream {
 
 fn add_ts_to_where_clause(generics: &Generics) -> Option<WhereClause> {
     let generic_types = generics
-        .params
-        .iter()
-        .filter_map(|gp| match gp {
-            GenericParam::Type(ty) => Some(ty.ident.clone()),
-            _ => None,
-        })
+        .type_params()
+        .map(|ty| ty.ident.clone())
         .collect::<Vec<_>>();
     if generic_types.is_empty() {
         return generics.where_clause.clone();
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index 4fb0159..f99a4f0 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -5,9 +5,8 @@ use syn::{Fields, Generics, ItemEnum, Type, Variant};
 use crate::{
     attr::{EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr},
     deps::Dependencies,
-    types,
-    types::generics::{format_generics, format_type},
     DerivedTS,
+    types,
 };
 
 pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
@@ -19,15 +18,15 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
     };
 
     if s.variants.is_empty() {
-        return Ok(empty_enum(name, enum_attr));
+        return Ok(empty_enum(name, enum_attr, s.generics.clone()));
     }
 
     if s.variants.is_empty() {
         return Ok(DerivedTS {
-            name,
+            generics: s.generics.clone(),
+            ts_name: name,
             docs: enum_attr.docs,
             inline: quote!("never".to_owned()),
-            decl: quote!("type {} = never;"),
             inline_flattened: None,
             dependencies: Dependencies::default(),
             export: enum_attr.export,
@@ -47,18 +46,17 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
         )?;
     }
 
-    let generic_args = format_generics(&mut dependencies, &s.generics);
     Ok(DerivedTS {
+        generics: s.generics.clone(),
         inline: quote!([#(#formatted_variants),*].join(" | ")),
-        decl: quote!(format!("type {}{} = {};", #name, #generic_args, Self::inline())),
         inline_flattened: Some(quote!(
             format!("({})", [#(#formatted_variants),*].join(" | "))
         )),
         dependencies,
-        name,
         docs: enum_attr.docs,
         export: enum_attr.export,
         export_to: enum_attr.export_to,
+        ts_name: name,
     })
 }
 
@@ -124,9 +122,13 @@ fn format_variant(
                         (Some(_), Some(_)) => syn_err!("`type` is not compatible with `as`"),
                         (Some(type_override), None) => quote! { #type_override },
                         (None, Some(type_as)) => {
-                            format_type(&syn::parse_str::<Type>(&type_as)?, dependencies, generics)
+                            let ty = syn::parse_str::<Type>(&type_as)?;
+                            quote!(<#ty as ts_rs::TS>::name())
                         }
-                        (None, None) => format_type(&unnamed.unnamed[0].ty, dependencies, generics),
+                        (None, None) => {
+                            let ty = &unnamed.unnamed[0].ty;
+                            quote!(<#ty as ts_rs::TS>::name())
+                        },
                     };
 
                     quote!(format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #ty))
@@ -170,9 +172,13 @@ fn format_variant(
                             (Some(_), Some(_)) => syn_err!("`type` is not compatible with `as`"),
                             (Some(type_override), None) => quote! { #type_override },
                             (None, Some(type_as)) => {
-                                format_type(&syn::parse_str::<Type>(&type_as)?, dependencies, generics)
+                                let ty = syn::parse_str::<Type>(&type_as)?;
+                                quote!(<#ty as ts_rs::TS>::name())
                             }
-                            (None, None) => format_type(&unnamed.unnamed[0].ty, dependencies, generics),
+                            (None, None) => {
+                                let ty = &unnamed.unnamed[0].ty;
+                                quote!(<#ty as ts_rs::TS>::name())
+                            },
                         };
 
                         quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #name, #ty))
@@ -192,16 +198,16 @@ fn format_variant(
 }
 
 // bindings for an empty enum (`never` in TS)
-fn empty_enum(name: impl Into<String>, enum_attr: EnumAttr) -> DerivedTS {
+fn empty_enum(name: impl Into<String>, enum_attr: EnumAttr, generics: Generics) -> DerivedTS {
     let name = name.into();
     DerivedTS {
+        generics,
         inline: quote!("never".to_owned()),
-        decl: quote!(format!("type {} = never;", #name)),
-        name,
         docs: enum_attr.docs,
         inline_flattened: None,
         dependencies: Dependencies::default(),
         export: enum_attr.export,
         export_to: enum_attr.export_to,
+        ts_name: name,
     }
 }
diff --git a/macros/src/types/generics.rs b/macros/src/types/generics.rs
deleted file mode 100644
index 7da2c48..0000000
--- a/macros/src/types/generics.rs
+++ /dev/null
@@ -1,172 +0,0 @@
-use proc_macro2::TokenStream;
-use quote::{format_ident, quote};
-use syn::{
-    GenericArgument, GenericParam, Generics, ItemStruct, PathArguments, Type, TypeGroup,
-    TypeReference, TypeSlice, TypeTuple,
-};
-
-use crate::{attr::StructAttr, deps::Dependencies};
-
-/// formats the generic arguments (like A, B in struct X<A, B>{..}) as "<X>" where x is a comma
-/// seperated list of generic arguments, or an empty string if there are no type generics (lifetime/const generics are ignored).
-/// this expands to an expression which evaluates to a `String`.
-///
-/// If a default type arg is encountered, it will be added to the dependencies.
-pub fn format_generics(deps: &mut Dependencies, generics: &Generics) -> TokenStream {
-    let mut expanded_params = generics
-        .params
-        .iter()
-        .filter_map(|param| match param {
-            GenericParam::Type(type_param) => Some({
-                let ty = type_param.ident.to_string();
-                if let Some(default) = &type_param.default {
-                    let default = format_type(default, deps, generics);
-                    quote!(format!("{} = {}", #ty, #default))
-                } else {
-                    quote!(#ty.to_owned())
-                }
-            }),
-            _ => None,
-        })
-        .peekable();
-
-    if expanded_params.peek().is_none() {
-        return quote!("");
-    }
-
-    let comma_separated = quote!([#(#expanded_params),*].join(", "));
-    quote!(format!("<{}>", #comma_separated))
-}
-
-pub fn format_type(ty: &Type, dependencies: &mut Dependencies, generics: &Generics) -> TokenStream {
-    // If the type matches one of the generic parameters, just pass the identifier:
-    if let Some(generic) = generics
-        .params
-        .iter()
-        .filter_map(|param| match param {
-            GenericParam::Type(type_param) => Some(type_param),
-            _ => None,
-        })
-        .find(|type_param| {
-            matches!(
-                ty,
-                Type::Path(type_path)
-                    if type_path.qself.is_none()
-                    && type_path.path.is_ident(&type_param.ident)
-            )
-        })
-    {
-        let generic_ident = generic.ident.clone();
-        let generic_ident_str = generic_ident.to_string();
-
-        if !generic.bounds.is_empty() {
-            return quote!(#generic_ident_str.to_owned());
-        }
-
-        return quote!(
-            match <#generic_ident>::inline().as_str() {
-                // When exporting a generic, the default type used is `()`,
-                // which gives "null" when calling `.name()`. In this case, we
-                // want to preserve the type param's identifier as the name used
-                "null" => #generic_ident_str.to_owned(),
-
-                // If name is not "null", a type has been provided, so we use its
-                // name instead
-                x => x.to_owned()
-            }
-        );
-    }
-
-    // special treatment for arrays and tuples
-    match ty {
-        // Arrays have their own implementation that needs to be handle separetly
-        // be cause the T in `[T; N]` is technically not a generic
-        Type::Array(type_array) => {
-            let formatted = format_type(&type_array.elem, dependencies, generics);
-            return quote!(<#type_array>::name_with_type_args(vec![#formatted]));
-        }
-        // The field is a slice (`[T]`) so it technically doesn't have a
-        // generic argument. Therefore, we handle it explicitly here like a `Vec<T>`
-        Type::Slice(TypeSlice { ref elem, .. }) => {
-            let inner_ty = elem;
-            let vec_ty = syn::parse2::<Type>(quote!(Vec::<#inner_ty>)).unwrap();
-            return format_type(&vec_ty, dependencies, generics);
-        }
-        // same goes for a tuple (`(A, B, C)`) - it doesn't have a type arg, so we handle it
-        // explicitly here.
-        Type::Tuple(tuple) => {
-            if tuple.elems.is_empty() {
-                // empty tuples `()` should be treated as `null`
-                return super::unit::null(&StructAttr::default(), "")
-                    .unwrap()
-                    .inline;
-            }
-
-            // we convert the tuple field to a struct: `(A, B, C)` => `struct A(A, B, C)`
-            let tuple_struct = super::type_def(
-                &StructAttr::default(),
-                &format_ident!("_"),
-                &tuple_type_to_tuple_struct(tuple).fields,
-                generics,
-            )
-            .unwrap();
-            // now, we return the inline definition
-            dependencies.append(tuple_struct.dependencies);
-            return tuple_struct.inline;
-        }
-        Type::Reference(syn::TypeReference { ref elem, .. }) => {
-            return format_type(elem, dependencies, generics)
-        }
-        _ => (),
-    };
-
-    dependencies.push_or_append_from(ty);
-    match extract_type_args(ty) {
-        None => quote!(<#ty as ts_rs::TS>::name()),
-        Some(type_args) => {
-            let args = type_args
-                .iter()
-                .map(|ty| format_type(ty, dependencies, generics))
-                .collect::<Vec<_>>();
-            let args = quote!(vec![#(#args),*]);
-            quote!(<#ty as ts_rs::TS>::name_with_type_args(#args))
-        }
-    }
-}
-
-fn extract_type_args(ty: &Type) -> Option<Vec<&Type>> {
-    let last_segment = match ty {
-        Type::Group(TypeGroup { elem, .. }) | Type::Reference(TypeReference { elem, .. }) => {
-            return extract_type_args(elem)
-        }
-        Type::Path(type_path) => type_path.path.segments.last(),
-        _ => None,
-    }?;
-
-    let segment_arguments = match &last_segment.arguments {
-        PathArguments::AngleBracketed(generic_arguments) => Some(generic_arguments),
-        _ => None,
-    }?;
-
-    let type_args: Vec<_> = segment_arguments
-        .args
-        .iter()
-        .filter_map(|arg| match arg {
-            GenericArgument::Type(ty) => Some(ty),
-            _ => None,
-        })
-        .collect();
-    if type_args.is_empty() {
-        return None;
-    }
-
-    Some(type_args)
-}
-
-// convert a [`TypeTuple`],  e.g `(A, B, C)`
-//      to a [`ItemStruct`], e.g `struct A(A, B, C)`
-fn tuple_type_to_tuple_struct(tuple: &TypeTuple) -> ItemStruct {
-    let elements = tuple.elems.iter();
-    syn::parse2(quote!(struct A( #(#elements),* );))
-        .expect("could not convert tuple to tuple struct")
-}
diff --git a/macros/src/types/mod.rs b/macros/src/types/mod.rs
index 7a94e21..1bdd276 100644
--- a/macros/src/types/mod.rs
+++ b/macros/src/types/mod.rs
@@ -3,7 +3,6 @@ use syn::{Fields, Generics, Ident, ItemStruct, Result};
 use crate::{attr::StructAttr, utils::to_ts_ident, DerivedTS};
 
 mod r#enum;
-mod generics;
 mod named;
 mod newtype;
 mod tuple;
@@ -26,14 +25,14 @@ fn type_def(
     let name = attr.rename.clone().unwrap_or_else(|| to_ts_ident(ident));
     match fields {
         Fields::Named(named) => match named.named.len() {
-            0 => unit::empty_object(attr, &name),
+            0 => unit::empty_object(attr, &name, generics.clone()),
             _ => named::named(attr, &name, named, generics),
         },
         Fields::Unnamed(unnamed) => match unnamed.unnamed.len() {
-            0 => unit::empty_array(attr, &name),
+            0 => unit::empty_array(attr, &name, generics.clone()),
             1 => newtype::newtype(attr, &name, unnamed, generics),
             _ => tuple::tuple(attr, &name, unnamed, generics),
         },
-        Fields::Unit => unit::null(attr, &name),
+        Fields::Unit => unit::null(attr, &name, generics.clone()),
     }
 }
diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs
index 10080d3..f3cc0dc 100644
--- a/macros/src/types/named.rs
+++ b/macros/src/types/named.rs
@@ -5,9 +5,8 @@ use syn::{Field, FieldsNamed, GenericArgument, Generics, PathArguments, Result,
 use crate::{
     attr::{FieldAttr, Inflection, Optional, StructAttr},
     deps::Dependencies,
-    types::generics::{format_generics, format_type},
-    utils::{raw_name_to_ts_field, to_ts_ident},
     DerivedTS,
+    utils::{raw_name_to_ts_field, to_ts_ident},
 };
 
 pub(crate) fn named(
@@ -39,7 +38,6 @@ pub(crate) fn named(
 
     let fields = quote!(<[String]>::join(&[#(#formatted_fields),*], " "));
     let flattened = quote!(<[String]>::join(&[#(#flattened_fields),*], " & "));
-    let generic_args = format_generics(&mut dependencies, generics);
 
     let inline = match (formatted_fields.len(), flattened_fields.len()) {
         (0, 0) => quote!("{  }".to_owned()),
@@ -50,14 +48,14 @@ pub(crate) fn named(
     };
 
     Ok(DerivedTS {
+        generics: generics.clone(),
         inline: quote!(#inline.replace(" } & { ", " ")),
-        decl: quote!(format!("type {}{} = {}", #name, #generic_args, Self::inline())),
         inline_flattened: Some(quote!(format!("{{ {} }}", #fields))),
-        name: name.to_owned(),
         docs: attr.docs.clone(),
         dependencies,
         export: attr.export,
         export_to: attr.export_to.clone(),
+        ts_name: name.to_owned(),
     })
 }
 
@@ -77,7 +75,7 @@ fn format_field(
     dependencies: &mut Dependencies,
     field: &Field,
     rename_all: &Option<Inflection>,
-    generics: &Generics,
+    _generics: &Generics,
 ) -> Result<()> {
     let FieldAttr {
         type_as,
@@ -139,7 +137,8 @@ fn format_field(
             dependencies.append_from(ty);
             quote!(<#ty as ts_rs::TS>::inline())
         } else {
-            format_type(ty, dependencies, generics)
+            dependencies.push(ty);
+            quote!(<#ty as ts_rs::TS>::name())
         }
     });
     let field_name = to_ts_ident(field.ident.as_ref().unwrap());
diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs
index e1d0463..1b53e6c 100644
--- a/macros/src/types/newtype.rs
+++ b/macros/src/types/newtype.rs
@@ -4,7 +4,6 @@ use syn::{FieldsUnnamed, Generics, Result, Type};
 use crate::{
     attr::{FieldAttr, StructAttr},
     deps::Dependencies,
-    types::generics::{format_generics, format_type},
     DerivedTS,
 };
 
@@ -34,7 +33,7 @@ pub(crate) fn newtype(
 
     match (&rename_inner, skip, optional.optional, flatten) {
         (Some(_), ..) => syn_err!("`rename` is not applicable to newtype fields"),
-        (_, true, ..) => return super::unit::null(attr, name),
+        (_, true, ..) => return super::unit::null(attr, name, generics.clone()),
         (_, _, true, ..) => syn_err!("`optional` is not applicable to newtype fields"),
         (_, _, _, true) => syn_err!("`flatten` is not applicable to newtype fields"),
         _ => {}
@@ -55,24 +54,23 @@ pub(crate) fn newtype(
     match (type_override.is_none(), inline) {
         (false, _) => (),
         (true, true) => dependencies.append_from(&inner_ty),
-        (true, false) => dependencies.push_or_append_from(&inner_ty),
+        (true, false) => dependencies.push(&inner_ty),
     };
 
     let inline_def = match type_override {
         Some(ref o) => quote!(#o.to_owned()),
         None if inline => quote!(<#inner_ty as ts_rs::TS>::inline()),
-        None => format_type(&inner_ty, &mut dependencies, generics),
+        None => quote!(<#inner_ty as ts_rs::TS>::name()),
     };
 
-    let generic_args = format_generics(&mut dependencies, generics);
     Ok(DerivedTS {
-        decl: quote!(format!("type {}{} = {};", #name, #generic_args, #inline_def)),
+        generics: generics.clone(),
         inline: inline_def,
         inline_flattened: None,
-        name: name.to_owned(),
         docs: attr.docs.clone(),
         dependencies,
         export: attr.export,
         export_to: attr.export_to.clone(),
+        ts_name: name.to_owned(),
     })
 }
diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs
index 0459c4a..b72cadd 100644
--- a/macros/src/types/tuple.rs
+++ b/macros/src/types/tuple.rs
@@ -5,7 +5,6 @@ use syn::{Field, FieldsUnnamed, Generics, Result, Type};
 use crate::{
     attr::{FieldAttr, StructAttr},
     deps::Dependencies,
-    types::generics::{format_generics, format_type},
     DerivedTS,
 };
 
@@ -28,28 +27,20 @@ pub(crate) fn tuple(
         format_field(&mut formatted_fields, &mut dependencies, field, generics)?;
     }
 
-    let generic_args = format_generics(&mut dependencies, generics);
     Ok(DerivedTS {
+        generics: generics.clone(),
         inline: quote! {
             format!(
                 "[{}]",
                 [#(#formatted_fields),*].join(", ")
             )
         },
-        decl: quote! {
-            format!(
-                "type {}{} = {};",
-                #name,
-                #generic_args,
-                Self::inline()
-            )
-        },
         inline_flattened: None,
-        name: name.to_owned(),
         docs: attr.docs.clone(),
         dependencies,
         export: attr.export,
         export_to: attr.export_to.clone(),
+        ts_name: name.to_owned(),
     })
 }
 
@@ -57,7 +48,7 @@ fn format_field(
     formatted_fields: &mut Vec<TokenStream>,
     dependencies: &mut Dependencies,
     field: &Field,
-    generics: &Generics,
+    _generics: &Generics,
 ) -> Result<()> {
     let FieldAttr {
         type_as,
@@ -99,17 +90,13 @@ fn format_field(
     formatted_fields.push(match type_override {
         Some(ref o) => quote!(#o.to_owned()),
         None if inline => quote!(<#ty as ts_rs::TS>::inline()),
-        None => format_type(&ty, dependencies, generics),
+        None => quote!(<#ty as ts_rs::TS>::name()),
     });
 
     match (inline, type_override) {
         (_, Some(_)) => (),
-        (false, _) => {
-            dependencies.push_or_append_from(&ty);
-        }
-        (true, _) => {
-            dependencies.append_from(&ty);
-        }
+        (false, _) => dependencies.push(&ty),
+        (true, _) => dependencies.append_from(&ty),
     };
 
     Ok(())
diff --git a/macros/src/types/unit.rs b/macros/src/types/unit.rs
index 64ce68c..5d50a5c 100644
--- a/macros/src/types/unit.rs
+++ b/macros/src/types/unit.rs
@@ -1,50 +1,50 @@
 use quote::quote;
-use syn::Result;
+use syn::{Generics, Result};
 
 use crate::{attr::StructAttr, deps::Dependencies, DerivedTS};
 
-pub(crate) fn empty_object(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
+pub(crate) fn empty_object(attr: &StructAttr, name: &str, generics: Generics) -> Result<DerivedTS> {
     check_attributes(attr)?;
 
     Ok(DerivedTS {
+        generics: generics.clone(),
         inline: quote!("Record<string, never>".to_owned()),
-        decl: quote!(format!("type {} = Record<string, never>;", #name)),
         inline_flattened: None,
-        name: name.to_owned(),
         docs: attr.docs.clone(),
         dependencies: Dependencies::default(),
         export: attr.export,
         export_to: attr.export_to.clone(),
+        ts_name: name.to_owned(),
     })
 }
 
-pub(crate) fn empty_array(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
+pub(crate) fn empty_array(attr: &StructAttr, name: &str, generics: Generics) -> Result<DerivedTS> {
     check_attributes(attr)?;
 
     Ok(DerivedTS {
+        generics: generics.clone(),
         inline: quote!("never[]".to_owned()),
-        decl: quote!(format!("type {} = never[];", #name)),
         inline_flattened: None,
-        name: name.to_owned(),
         docs: attr.docs.clone(),
         dependencies: Dependencies::default(),
         export: attr.export,
         export_to: attr.export_to.clone(),
+        ts_name: name.to_owned(),
     })
 }
 
-pub(crate) fn null(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
+pub(crate) fn null(attr: &StructAttr, name: &str, generics: Generics) -> Result<DerivedTS> {
     check_attributes(attr)?;
 
     Ok(DerivedTS {
+        generics,
         inline: quote!("null".to_owned()),
-        decl: quote!(format!("type {} = null;", #name)),
         inline_flattened: None,
-        name: name.to_owned(),
         docs: attr.docs.clone(),
         dependencies: Dependencies::default(),
         export: attr.export,
         export_to: attr.export_to.clone(),
+        ts_name: name.to_owned(),
     })
 }
 
diff --git a/macros/src/utils.rs b/macros/src/utils.rs
index 93552b7..234d29d 100644
--- a/macros/src/utils.rs
+++ b/macros/src/utils.rs
@@ -1,7 +1,9 @@
 use std::convert::TryFrom;
 
-use proc_macro2::Ident;
-use syn::{spanned::Spanned, Attribute, Error, Expr, ExprLit, Lit, Meta, Result};
+use proc_macro2::{Ident, TokenStream};
+use syn::{Attribute, Error, Expr, ExprLit, GenericParam, Generics, Lit, Meta, Result, spanned::Spanned};
+use quote::quote;
+use crate::deps::Dependencies;
 
 macro_rules! syn_err {
     ($l:literal $(, $a:expr)*) => {
@@ -57,20 +59,24 @@ pub fn to_ts_ident(ident: &Ident) -> String {
 
 /// Convert an arbitrary name to a valid Typescript field name.
 ///
-/// If the name contains special characters it will be wrapped in quotes.
+/// If the name contains special characters or if its first character
+/// is a number it will be wrapped in quotes.
 pub fn raw_name_to_ts_field(value: String) -> String {
-    let valid = value
+    let valid_chars = value
         .chars()
-        .all(|c| c.is_alphanumeric() || c == '_' || c == '$')
-        && value
-            .chars()
-            .next()
-            .map(|first| !first.is_numeric())
-            .unwrap_or(true);
-    if !valid {
-        format!(r#""{value}""#)
-    } else {
+        .all(|c| c.is_alphanumeric() || c == '_' || c == '$');
+    
+    let does_not_start_with_digit = value
+        .chars()
+        .next()
+        .map_or(true, |first| !first.is_numeric());
+
+    let valid = valid_chars && does_not_start_with_digit;
+
+    if valid {
         value
+    } else {
+        format!(r#""{value}""#)
     }
 }
 
@@ -197,3 +203,34 @@ mod warning {
         writer.print(&buffer)
     }
 }
+
+/// formats the generic arguments (like A, B in struct X<A, B>{..}) as "<X>" where x is a comma
+/// seperated list of generic arguments, or an empty string if there are no type generics (lifetime/const generics are ignored).
+/// this expands to an expression which evaluates to a `String`.
+///
+/// If a default type arg is encountered, it will be added to the dependencies.
+pub fn format_generics(deps: &mut Dependencies, generics: &Generics) -> TokenStream {
+    let mut expanded_params = generics
+        .params
+        .iter()
+        .filter_map(|param| match param {
+            GenericParam::Type(type_param) => Some({
+                let ty = type_param.ident.to_string();
+                if let Some(default) = &type_param.default {
+                    deps.push(default);
+                    quote!(format!("{} = {}", #ty, <#default as ts_rs::TS>::name()))
+                } else {
+                    quote!(#ty.to_owned())
+                }
+            }),
+            _ => None,
+        })
+        .peekable();
+
+    if expanded_params.peek().is_none() {
+        return quote!("");
+    }
+
+    let comma_separated = quote!([#(#expanded_params),*].join(", "));
+    quote!(format!("<{}>", #comma_separated))
+}
diff --git a/ts-rs/src/chrono.rs b/ts-rs/src/chrono.rs
index 7aee2a9..797289e 100644
--- a/ts-rs/src/chrono.rs
+++ b/ts-rs/src/chrono.rs
@@ -22,11 +22,11 @@ impl_primitives!(NaiveDateTime, NaiveDate, NaiveTime, Month, Weekday, Duration =
 impl_dummy!(Utc, Local, FixedOffset);
 
 impl<T: TimeZone + 'static> TS for DateTime<T> {
-    fn name() -> String {
+    fn ident() -> String {
         "string".to_owned()
     }
-    fn name_with_type_args(_: Vec<String>) -> String {
-        Self::name()
+    fn name() -> String {
+        "string".to_owned()
     }
     fn inline() -> String {
         "string".to_owned()
@@ -37,11 +37,11 @@ impl<T: TimeZone + 'static> TS for DateTime<T> {
 }
 
 impl<T: TimeZone + 'static> TS for Date<T> {
-    fn name() -> String {
+    fn ident() -> String {
         "string".to_owned()
     }
-    fn name_with_type_args(_: Vec<String>) -> String {
-        Self::name()
+    fn name() -> String {
+        "string".to_owned()
     }
     fn inline() -> String {
         "string".to_owned()
diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs
index 724129b..7ecccc9 100644
--- a/ts-rs/src/export.rs
+++ b/ts-rs/src/export.rs
@@ -57,11 +57,6 @@ mod recursive_export {
 
     /// Exports `T` to the file specified by the `#[ts(export_to = ..)]` attribute.
     /// Additionally, all dependencies of `T` will be exported as well.
-    ///
-    /// TODO: This might cause a race condition:
-    ///       If two types `A` and `B` are `#[ts(export)]` and depend on type `C`,
-    ///       then both tests for exporting `A` and `B` will try to write `C` to `C.ts`.
-    ///       Since rust, by default, executes tests in paralell, this might cause `C.ts` to be corrupted.
     pub(crate) fn export_type_with_dependencies<T: TS + ?Sized + 'static>(
     ) -> Result<(), ExportError> {
         let mut seen = HashSet::new();
@@ -146,7 +141,7 @@ pub mod __private {
     pub fn get_export_to_path<T: TS + ?Sized>() -> Option<String> {
         provided_default_dir().map_or_else(
             || T::EXPORT_TO.map(ToString::to_string),
-            |path| Some(format!("{path}/{}.ts", T::name())),
+            |path| Some(format!("{path}/{}.ts", T::ident())),
         )
     }
 }
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index efd26d5..9146b28 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -66,10 +66,6 @@
 //! - generic types
 //! - support for ESM imports
 //!
-//! ## limitations
-//! - generic fields cannot be inlined or flattened (#56)
-//! - type aliases must not alias generic types (#70)
-//!
 //! ## cargo features
 //! - `serde-compat` (default)  
 //!
@@ -168,12 +164,11 @@ use std::{
 
 pub use ts_rs_macros::TS;
 
-pub use crate::export::ExportError;
-use crate::typelist::TypeList;
-
 // 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")]
 mod chrono;
@@ -283,24 +278,41 @@ pub trait TS {
     const EXPORT_TO: Option<&'static str> = None;
     const DOCS: Option<&'static str> = None;
 
+    fn ident() -> String {
+        let name = Self::name();
+
+        if name.contains('<') {
+            panic!("generic types must implement ident")
+        } else {
+            name
+        }
+    }
+
     fn get_export_to() -> Option<String> {
         Self::EXPORT_TO.map(ToString::to_string)
     }
 
     /// Declaration of this type, e.g. `interface User { user_id: number, ... }`.
     /// This function will panic if the type has no declaration.
+    ///
+    /// If this type is generic, then all provided generic parameters will be swapped for
+    /// placeholders, resulting in a generic typescript definition.
+    /// Both `SomeType::<i32>::decl()` and `SomeType::<String>::decl()` will therefore result in
+    /// the same TypeScript declaration `type SomeType<A> = ...`.
     fn decl() -> String {
         panic!("{} cannot be declared", Self::name());
     }
 
+    /// Declaration of this type using the supplied generic arguments.
+    /// The resulting TypeScript definition will not be generic. For that, see `TS::decl()`.
+    /// If this type is not generic, then this function is equivalent to `TS::decl()`.
+    fn decl_concrete() -> String {
+        panic!("{} cannot be declared", Self::name());
+    }
+
     /// Name of this type in TypeScript.
     fn name() -> String;
 
-    /// Name of this type in TypeScript, with type arguments.
-    fn name_with_type_args(args: Vec<String>) -> String {
-        format!("{}<{}>", Self::name(), args.join(", "))
-    }
-
     /// Formats this types definition in TypeScript, e.g `{ user_id: number }`.
     /// This function will panic if the type cannot be inlined.
     fn inline() -> String {
@@ -313,12 +325,14 @@ pub trait TS {
         panic!("{} cannot be flattened", Self::name())
     }
 
+    /// Returns a `TypeList` of all types on which this type depends.
     fn dependency_types() -> impl TypeList
     where
         Self: 'static,
     {
     }
 
+    /// Resolves all dependencies of this type recursively.
     fn dependencies() -> Vec<Dependency>
     where
         Self: 'static,
@@ -329,7 +343,12 @@ pub trait TS {
         struct Visit<'a>(&'a mut Vec<Dependency>);
         impl<'a> TypeVisitor for Visit<'a> {
             fn visit<T: TS + 'static + ?Sized>(&mut self) {
-                if let Some(dep) = Dependency::from_ty::<T>() {
+                if T::transparent() {
+                    // the dependency `T` is "transparent", meaning that our original type depends
+                    // on the dependencies of `T` as well.
+                    T::dependency_types().for_each(self);
+                } else if let Some(dep) = Dependency::from_ty::<T>() {
+                    // the dependency `T` is not transparent, so we just add it to the output
                     self.0.push(dep);
                 }
             }
@@ -397,7 +416,7 @@ impl Dependency {
         let exported_to = T::get_export_to()?;
         Some(Dependency {
             type_id: TypeId::of::<T>(),
-            ts_name: T::name(),
+            ts_name: T::ident(),
             exported_to,
         })
     }
@@ -408,11 +427,7 @@ macro_rules! impl_primitives {
     ($($($ty:ty),* => $l:literal),*) => { $($(
         impl TS for $ty {
             fn name() -> String { $l.to_owned() }
-            fn name_with_type_args(args: Vec<String>) -> String {
-                assert!(args.is_empty(), "called name_with_type_args on primitive");
-                $l.to_owned()
-            }
-            fn inline() -> String { $l.to_owned() }
+            fn inline() -> String { <Self as $crate::TS>::name() }
             fn transparent() -> bool { false }
         }
     )*)* };
@@ -425,7 +440,7 @@ macro_rules! impl_tuples {
                 format!("[{}]", [$($i::name()),*].join(", "))
             }
             fn inline() -> String {
-                format!("[{}]", [$($i::inline()),*].join(", "))
+                panic!("tuple cannot be inlined!");
             }
             fn dependency_types() -> impl TypeList
             where
@@ -448,10 +463,6 @@ macro_rules! impl_wrapper {
     ($($t:tt)*) => {
         $($t)* {
             fn name() -> String { T::name() }
-            fn name_with_type_args(mut args: Vec<String>) -> String {
-                assert_eq!(args.len(), 1);
-                args.remove(0)
-            }
             fn inline() -> String { T::inline() }
             fn inline_flattened() -> String { T::inline_flattened() }
             fn dependency_types() -> impl TypeList
@@ -470,7 +481,6 @@ macro_rules! impl_shadow {
     (as $s:ty: $($impl:tt)*) => {
         $($impl)* {
             fn name() -> String { <$s>::name() }
-            fn name_with_type_args(args: Vec<String>) -> String { <$s>::name_with_type_args(args) }
             fn inline() -> String { <$s>::inline() }
             fn inline_flattened() -> String { <$s>::inline_flattened() }
             fn dependency_types() -> impl $crate::typelist::TypeList
@@ -486,17 +496,7 @@ macro_rules! impl_shadow {
 
 impl<T: TS> TS for Option<T> {
     fn name() -> String {
-        unreachable!();
-    }
-
-    fn name_with_type_args(args: Vec<String>) -> String {
-        assert_eq!(
-            args.len(),
-            1,
-            "called Option::name_with_type_args with {} args",
-            args.len()
-        );
-        format!("{} | null", args[0])
+        format!("{} | null", T::name())
     }
 
     fn inline() -> String {
@@ -517,7 +517,7 @@ impl<T: TS> TS for Option<T> {
 
 impl<T: TS, E: TS> TS for Result<T, E> {
     fn name() -> String {
-        unreachable!();
+        format!("{{ Ok : {} }} | {{ Err : {} }}", T::name(), E::name())
     }
     fn inline() -> String {
         format!("{{ Ok : {} }} | {{ Err : {} }}", T::inline(), E::inline())
@@ -534,9 +534,12 @@ impl<T: TS, E: TS> TS for Result<T, E> {
 }
 
 impl<T: TS> TS for Vec<T> {
-    fn name() -> String {
+    fn ident() -> String {
         "Array".to_owned()
     }
+    fn name() -> String {
+        format!("Array<{}>", T::name())
+    }
 
     fn inline() -> String {
         format!("Array<{}>", T::inline())
@@ -561,25 +564,10 @@ impl<T: TS, const N: usize> TS for [T; N] {
             return Vec::<T>::name();
         }
 
-        "[]".to_owned()
-    }
-
-    fn name_with_type_args(args: Vec<String>) -> String {
-        if N > ARRAY_TUPLE_LIMIT {
-            return Vec::<T>::name_with_type_args(args);
-        }
-
-        assert_eq!(
-            args.len(),
-            1,
-            "called [T; N]::name_with_type_args with {} args",
-            args.len()
-        );
-
         format!(
             "[{}]",
             (0..N)
-                .map(|_| args[0].clone())
+                .map(|_| T::name())
                 .collect::<Box<[_]>>()
                 .join(", ")
         )
@@ -592,7 +580,10 @@ 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(", ")
         )
     }
 
@@ -609,18 +600,11 @@ impl<T: TS, const N: usize> TS for [T; N] {
 }
 
 impl<K: TS, V: TS, H> TS for HashMap<K, V, H> {
-    fn name() -> String {
+    fn ident() -> String {
         "Record".to_owned()
     }
-
-    fn name_with_type_args(args: Vec<String>) -> String {
-        assert_eq!(
-            args.len(),
-            2,
-            "called HashMap::name_with_type_args with {} args",
-            args.len()
-        );
-        format!("Record<{}, {}>", args[0], args[1])
+    fn name() -> String {
+        format!("Record<{}, {}>", K::name(), V::name())
     }
 
     fn inline() -> String {
@@ -641,17 +625,7 @@ impl<K: TS, V: TS, H> TS for HashMap<K, V, H> {
 
 impl<I: TS> TS for Range<I> {
     fn name() -> String {
-        panic!("called Range::name - Did you use a type alias?")
-    }
-
-    fn name_with_type_args(args: Vec<String>) -> String {
-        assert_eq!(
-            args.len(),
-            1,
-            "called Range::name_with_type_args with {} args",
-            args.len()
-        );
-        format!("{{ start: {}, end: {}, }}", &args[0], &args[0])
+        format!("{{ start: {}, end: {}, }}", I::name(), I::name())
     }
 
     fn dependency_types() -> impl TypeList
@@ -668,17 +642,7 @@ impl<I: TS> TS for Range<I> {
 
 impl<I: TS> TS for RangeInclusive<I> {
     fn name() -> String {
-        panic!("called RangeInclusive::name - Did you use a type alias?")
-    }
-
-    fn name_with_type_args(args: Vec<String>) -> String {
-        assert_eq!(
-            args.len(),
-            1,
-            "called RangeInclusive::name_with_type_args with {} args",
-            args.len()
-        );
-        format!("{{ start: {}, end: {}, }}", &args[0], &args[0])
+        format!("{{ start: {}, end: {}, }}", I::name(), I::name())
     }
 
     fn dependency_types() -> impl TypeList
@@ -763,3 +727,20 @@ impl_primitives! {
 }
 #[rustfmt::skip]
 pub(crate) use impl_primitives;
+
+
+#[doc(hidden)]
+#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
+pub struct Dummy;
+
+impl std::fmt::Display for Dummy {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{:?}", self)
+    }
+}
+
+impl TS for Dummy {
+    fn name() -> String { "Dummy".to_owned() }
+    fn transparent() -> bool { false }
+}
+


Test patch:
diff --git a/e2e/dependencies/consumer/src/main.rs b/e2e/dependencies/consumer/src/main.rs
index 5a79fba..01fadc8 100644
--- a/e2e/dependencies/consumer/src/main.rs
+++ b/e2e/dependencies/consumer/src/main.rs
@@ -1,10 +1,14 @@
 use ts_rs::TS;
-use dependency1::LibraryType;
+use dependency1::*;
 
 #[derive(TS)]
 #[ts(export)]
 struct ConsumerType {
-    pub ty: LibraryType
+    pub ty1: LibraryType1,
+    pub ty2_1: LibraryType2<i32>,
+    pub ty2_2: LibraryType2<&'static Self>,
+    pub ty2_3: LibraryType2<LibraryType2<Box<ConsumerType>>>,
+    pub ty2_4: LibraryType2<LibraryType2<LibraryType1>>,
 }
 
 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 aade3eb..5a635f1 100644
--- a/e2e/dependencies/dependency1/src/lib.rs
+++ b/e2e/dependencies/dependency1/src/lib.rs
@@ -1,6 +1,11 @@
 use ts_rs::TS;
 
 #[derive(TS)]
-pub struct LibraryType {
+pub struct LibraryType1 {
     pub a: i32
-}
\ No newline at end of file
+}
+
+#[derive(TS)]
+pub struct LibraryType2<T> {
+    pub t: T
+}
diff --git a/ts-rs/tests/chrono.rs b/ts-rs/tests/chrono.rs
index d9bc9ee..f4091be 100644
--- a/ts-rs/tests/chrono.rs
+++ b/ts-rs/tests/chrono.rs
@@ -27,6 +27,6 @@ fn chrono() {
 
     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, }"
+        "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/docs.rs b/ts-rs/tests/docs.rs
index 41e5e66..d007ec4 100644
--- a/ts-rs/tests/docs.rs
+++ b/ts-rs/tests/docs.rs
@@ -132,7 +132,7 @@ fn export_a() {
             " *\n",
             " * Testing\n",
             " */\n",
-            "name: string, }"
+            "name: string, };"
         )
     };
 
@@ -178,7 +178,7 @@ fn export_b() {
             " *\n",
             " * Testing\n",
             " */\n",
-            "name: string, }",
+            "name: string, };",
         )
     };
 
@@ -372,11 +372,8 @@ fn export_g() {
             " *\n",
             " * Testing\n",
             " */\n",
-            "variant_field: number, } })",
+            "variant_field: number, } });",
         )
-
-
-
     };
 
     let actual_content = fs::read_to_string("tests-out/docs/G.ts").unwrap();
diff --git a/ts-rs/tests/export_manually.rs b/ts-rs/tests/export_manually.rs
index 33a7af1..1ca37a7 100644
--- a/ts-rs/tests/export_manually.rs
+++ b/ts-rs/tests/export_manually.rs
@@ -32,7 +32,7 @@ fn export_manually() {
     } else {
         concat!(
             "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n",
-            "\nexport type User = { name: string, age: number, active: boolean, }"
+            "\nexport type User = { name: string, age: number, active: boolean, };"
         )
     };
 
@@ -53,7 +53,7 @@ fn export_manually_dir() {
     } else {
         concat!(
             "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n",
-            "\nexport type UserDir = { name: string, age: number, active: boolean, }"
+            "\nexport type UserDir = { name: string, age: number, active: boolean, };"
         )
     };
 
diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs
index 048f847..de4e5d6 100644
--- a/ts-rs/tests/generics.rs
+++ b/ts-rs/tests/generics.rs
@@ -10,6 +10,7 @@ use std::{
 use ts_rs::TS;
 
 #[derive(TS)]
+#[ts(export)]
 struct Generic<T>
 where
     T: TS,
@@ -60,27 +61,27 @@ declare! {
 fn test() {
     assert_eq!(
         TypeGroup::decl(),
-        "type TypeGroup = { foo: Array<Container>, }",
+        "type TypeGroup = { foo: Array<Container>, };",
     );
 
     assert_eq!(
         Generic::<()>::decl(),
-        "type Generic<T> = { value: T, values: Array<T>, }"
+        "type Generic<T> = { value: T, values: Array<T>, };"
     );
 
     assert_eq!(
         GenericAutoBound::<()>::decl(),
-        "type GenericAutoBound<T> = { value: T, values: Array<T>, }"
+        "type GenericAutoBound<T> = { value: T, values: Array<T>, };"
     );
 
     assert_eq!(
         GenericAutoBound2::<()>::decl(),
-        "type GenericAutoBound2<T> = { value: T, values: Array<T>, }"
+        "type GenericAutoBound2<T> = { value: T, values: Array<T>, };"
     );
 
     assert_eq!(
         Container::decl(),
-        "type Container = { foo: Generic<number>, bar: Array<Generic<number>>, baz: Record<string, Generic<string>>, }"
+        "type Container = { foo: Generic<number>, bar: Array<Generic<number>>, baz: Record<string, Generic<string>>, };"
     );
 }
 
@@ -142,7 +143,7 @@ fn generic_struct() {
 
     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]]>, }"
+        "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]]>, };"
     )
 }
 
@@ -162,19 +163,15 @@ fn inline() {
         t: Generic<Vec<String>>,
     }
 
-    assert_eq!(Generic::<()>::decl(), "type Generic<T> = { t: T, }");
+    assert_eq!(Generic::<()>::decl(), "type Generic<T> = { t: T, };");
     assert_eq!(
         Container::decl(),
-        "type Container = { g: Generic<string>, gi: { t: string, }, t: Array<string>, }"
+        "type Container = { g: Generic<string>, gi: { t: string, }, t: Array<string>, };"
     );
 }
 
 #[test]
-#[ignore = "We haven't figured out how to inline generics with bounds yet"]
-#[allow(unreachable_code)]
 fn inline_with_bounds() {
-    todo!("FIX ME: https://github.com/Aleph-Alpha/ts-rs/issues/214");
-
     #[derive(TS)]
     struct Generic<T: ToString> {
         t: T,
@@ -193,12 +190,11 @@ fn inline_with_bounds() {
 
     assert_eq!(
         Generic::<&'static str>::decl(),
-        "type Generic<T> = { t: T, }"
+        "type Generic<T> = { t: T, };"
     );
-    //                   ^^^^^^^^^^^^ Replace with something else
     assert_eq!(
         Container::decl(),
-        "type Container = { g: Generic<string>, gi: { t: string, }, t: number, }" // Actual output: { g: Generic<string>, gi: { t: T, }, t: T, }
+        "type Container = { g: Generic<string>, gi: { t: string, }, t: number, };"
     );
 }
 
@@ -222,11 +218,11 @@ fn inline_with_default() {
 
     assert_eq!(
         Generic::<()>::decl(),
-        "type Generic<T = string> = { t: T, }"
+        "type Generic<T = string> = { t: T, };"
     );
     assert_eq!(
         Container::decl(),
-        "type Container = { g: Generic<string>, gi: { t: string, }, t: number, }"
+        "type Container = { g: Generic<string>, gi: { t: string, }, t: number, };"
     );
 }
 
@@ -236,27 +232,21 @@ fn default() {
     struct A<T = String> {
         t: T,
     }
-    assert_eq!(A::<()>::decl(), "type A<T = string> = { t: T, }");
+    assert_eq!(A::<()>::decl(), "type A<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_eq!(B::<()>::decl(), "type B<U = A<number> | null> = { u: U, };");
     assert!(B::<()>::dependencies().iter().any(|dep| dep.ts_name == "A"));
 
     #[derive(TS)]
     struct Y {
         a1: A,
         a2: A<i32>,
-        // https://github.com/Aleph-Alpha/ts-rs/issues/56
-        // TODO: fixme
-        // #[ts(inline)]
-        // xi: X,
-        // #[ts(inline)]
-        // xi2: X<i32>
     }
-    assert_eq!(Y::decl(), "type Y = { a1: A, a2: A<number>, }")
+    assert_eq!(Y::decl(), "type Y = { a1: A<string>, a2: A<number>, };")
 }
 
 #[test]
@@ -265,7 +255,7 @@ fn trait_bounds() {
     struct A<T: ToString = i32> {
         t: T,
     }
-    assert_eq!(A::<i32>::decl(), "type A<T = number> = { t: T, }");
+    assert_eq!(A::<i32>::decl(), "type A<T = number> = { t: T, };");
 
     #[derive(TS)]
     struct B<T: ToString + Debug + Clone + 'static>(T);
@@ -289,7 +279,7 @@ fn trait_bounds() {
     }
 
     let ty = format!(
-        "type D<T> = {{ t: [{}], }}",
+        "type D<T> = {{ t: [{}], }};",
         "T, ".repeat(41).trim_end_matches(", ")
     );
     assert_eq!(D::<&str, 41>::decl(), ty)
@@ -346,6 +336,36 @@ fn deeply_nested() {
             a_null: T1<T0<null>>, \
             b_null: T1<P1<T0<P0<null>>>>, \
             c_null: T1<P1<null>>, \
-         }"
+         };"
+    );
+}
+
+#[test]
+fn inline_generic_enum() {
+    #[derive(TS)]
+    struct SomeType(String);
+
+    #[derive(TS)]
+    enum MyEnum<A, B> {
+        VariantA(A),
+        VariantB(B)
+    }
+
+    #[derive(TS)]
+    struct Parent {
+        e: MyEnum<i32, i32>,
+        #[ts(inline)]
+        e1: MyEnum<i32, SomeType>
+    }
+
+    // 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 = { \
+            e: MyEnum<number, number>, \
+            e1: { \"VariantA\": number } | { \"VariantB\": SomeType }, \
+        };"
     );
 }
diff --git a/ts-rs/tests/hashmap.rs b/ts-rs/tests/hashmap.rs
index 1fc5354..8f20512 100644
--- a/ts-rs/tests/hashmap.rs
+++ b/ts-rs/tests/hashmap.rs
@@ -13,7 +13,7 @@ fn hashmap() {
 
     assert_eq!(
         Hashes::decl(),
-        "type Hashes = { map: Record<string, string>, set: Array<string>, }"
+        "type Hashes = { map: Record<string, string>, set: Array<string>, };"
     )
 }
 
@@ -33,6 +33,6 @@ fn hashmap_with_custom_hasher() {
 
     assert_eq!(
         Hashes::decl(),
-        "type Hashes = { map: Record<string, string>, set: Array<string>, }"
+        "type Hashes = { map: Record<string, string>, set: Array<string>, };"
     )
 }
diff --git a/ts-rs/tests/indexmap.rs b/ts-rs/tests/indexmap.rs
index 7c8eb20..66319b8 100644
--- a/ts-rs/tests/indexmap.rs
+++ b/ts-rs/tests/indexmap.rs
@@ -14,6 +14,6 @@ fn indexmap() {
 
     assert_eq!(
         Indexes::decl(),
-        "type Indexes = { map: Record<string, string>, set: Array<string>, }"
+        "type Indexes = { map: Record<string, string>, set: Array<string>, };"
     )
 }
diff --git a/ts-rs/tests/issue-70.rs b/ts-rs/tests/issue-70.rs
new file mode 100644
index 0000000..a90bd4d
--- /dev/null
+++ b/ts-rs/tests/issue-70.rs
@@ -0,0 +1,57 @@
+#![allow(unused)]
+
+use std::collections::HashMap;
+use ts_rs::TS;
+
+#[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>, };");
+}
+
+#[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]>>, \
+        };"
+    );
+
+    #[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]>>, \
+        };"
+    );
+}
\ No newline at end of file
diff --git a/ts-rs/tests/lifetimes.rs b/ts-rs/tests/lifetimes.rs
index 10abf37..d524cea 100644
--- a/ts-rs/tests/lifetimes.rs
+++ b/ts-rs/tests/lifetimes.rs
@@ -8,7 +8,7 @@ fn contains_borrow() {
         s: &'a str,
     }
 
-    assert_eq!(S::decl(), "type S = { s: string, }")
+    assert_eq!(S::decl(), "type S = { s: string, };")
 }
 
 #[test]
@@ -29,6 +29,6 @@ fn contains_borrow_type_args() {
 
     assert_eq!(
         A::decl(),
-        "type A = { a: Array<number>, b: Array<B<number>>, c: Record<string, boolean>, }"
+        "type A = { a: Array<number>, b: Array<B<number>>, c: Record<string, boolean>, };"
     );
 }
diff --git a/ts-rs/tests/list.rs b/ts-rs/tests/list.rs
index 92f6f2e..f8c131e 100644
--- a/ts-rs/tests/list.rs
+++ b/ts-rs/tests/list.rs
@@ -8,5 +8,5 @@ fn list() {
         data: Option<Vec<u32>>,
     }
 
-    assert_eq!(List::decl(), "type List = { data: Array<number> | null, }");
+    assert_eq!(List::decl(), "type List = { data: Array<number> | null, };");
 }
diff --git a/ts-rs/tests/ranges.rs b/ts-rs/tests/ranges.rs
index 47c4bd9..f9d50a6 100644
--- a/ts-rs/tests/ranges.rs
+++ b/ts-rs/tests/ranges.rs
@@ -22,7 +22,16 @@ struct RangeTest {
 fn range() {
     assert_eq!(
         RangeTest::decl(),
-        "type RangeTest = { a: { start: number, end: number, }, b: { start: string, end: string, }, c: { start: { start: number, end: number, }, end: { start: number, end: number, }, }, d: { start: number, end: number, }, e: { start: Inner, end: Inner, }, }"
+        "type RangeTest = { \
+            a: { start: number, end: number, }, \
+            b: { start: string, end: string, }, \
+            c: { \
+                start: { start: number, end: number, }, \
+                end: { start: number, end: number, }, \
+            }, \
+            d: { start: number, end: number, }, \
+            e: { start: Inner, end: Inner, }, \
+        };"
     );
     assert_eq!(
         RangeTest::dependencies()
diff --git a/ts-rs/tests/raw_idents.rs b/ts-rs/tests/raw_idents.rs
index 9fae879..dc87d95 100644
--- a/ts-rs/tests/raw_idents.rs
+++ b/ts-rs/tests/raw_idents.rs
@@ -15,6 +15,6 @@ fn raw_idents() {
     let out = <r#enum as TS>::decl();
     assert_eq!(
         out,
-        "type enum = { type: number, use: number, struct: number, let: number, enum: number, }"
+        "type enum = { type: number, use: number, struct: number, let: number, enum: number, };"
     );
 }
diff --git a/ts-rs/tests/self_referential.rs b/ts-rs/tests/self_referential.rs
index c7fbad4..0357a5b 100644
--- a/ts-rs/tests/self_referential.rs
+++ b/ts-rs/tests/self_referential.rs
@@ -36,7 +36,7 @@ fn named() {
             t_arc: T, \
             self_arc: T, \
             has_t: { t: T, }, \
-         }"
+         };"
     );
 }
 
diff --git a/ts-rs/tests/semver.rs b/ts-rs/tests/semver.rs
index 81d307c..61800ed 100644
--- a/ts-rs/tests/semver.rs
+++ b/ts-rs/tests/semver.rs
@@ -11,5 +11,5 @@ fn semver() {
         version: Version,
     }
 
-    assert_eq!(Semver::decl(), "type Semver = { version: string, }")
+    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 bcb523e..d6a5be9 100644
--- a/ts-rs/tests/serde-skip-with-default.rs
+++ b/ts-rs/tests/serde-skip-with-default.rs
@@ -20,5 +20,5 @@ pub struct Foobar {
 
 #[test]
 fn serde_skip_with_default() {
-    assert_eq!(Foobar::decl(), "type Foobar = { something_else: number, }");
+    assert_eq!(Foobar::decl(), "type Foobar = { something_else: number, };");
 }
diff --git a/ts-rs/tests/union_with_data.rs b/ts-rs/tests/union_with_data.rs
index b35f368..43ff1a9 100644
--- a/ts-rs/tests/union_with_data.rs
+++ b/ts-rs/tests/union_with_data.rs
@@ -24,10 +24,10 @@ enum SimpleEnum {
 
 #[test]
 fn test_stateful_enum() {
-    assert_eq!(Bar::decl(), r#"type Bar = { field: number, }"#);
+    assert_eq!(Bar::decl(), r#"type Bar = { field: number, };"#);
     assert_eq!(Bar::dependencies(), vec![]);
 
-    assert_eq!(Foo::decl(), r#"type Foo = { bar: Bar, }"#);
+    assert_eq!(Foo::decl(), r#"type Foo = { bar: Bar, };"#);
     assert_eq!(
         Foo::dependencies(),
         vec![Dependency::from_ty::<Bar>().unwrap()]
diff --git a/ts-rs/tests/unsized.rs b/ts-rs/tests/unsized.rs
index bfddf75..9077665 100644
--- a/ts-rs/tests/unsized.rs
+++ b/ts-rs/tests/unsized.rs
@@ -15,6 +15,6 @@ fn contains_str() {
 
     assert_eq!(
         S::decl(),
-        "type S = { b: string, c: string, r: string, a: string, }"
+        "type S = { b: string, c: string, r: string, a: string, };"
     )
 }