From e9789fadabd386c3be62b7db69601a148acb4c16 Mon Sep 17 00:00:00 2001 From: gustavo-shigueo <58121396+gustavo-shigueo@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:32:15 -0300 Subject: [PATCH 1/4] Add test case --- ts-rs/tests/integration/flatten.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ts-rs/tests/integration/flatten.rs b/ts-rs/tests/integration/flatten.rs index 8bb3a423..486f2471 100644 --- a/ts-rs/tests/integration/flatten.rs +++ b/ts-rs/tests/integration/flatten.rs @@ -29,6 +29,19 @@ struct C { d: i32, } +#[derive(TS)] +#[ts(export, export_to = "flatten/")] +pub struct Inner {} + +// Create a parent struct that flattens the zero-field struct: +#[derive(TS)] +#[ts(export, export_to = "flatten/")] +pub struct Outer { + #[ts(flatten)] + pub inner: Inner, + pub other_field: String, +} + #[test] fn test_def() { let cfg = Config::from_env(); @@ -36,4 +49,5 @@ fn test_def() { C::inline(&cfg), "{ b: { c: number, a: number, b: number, } & ({ [key in string]: number }), d: number, }" ); + assert_eq!(Outer::inline(&cfg), "{ other_field: string, }"); } From c5119ec3b0db35bc890ae24e866857cb15a0c5a1 Mon Sep 17 00:00:00 2001 From: gustavo-shigueo <58121396+gustavo-shigueo@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:32:40 -0300 Subject: [PATCH 2/4] Allow unit struct to be flattened as empty object --- macros/src/types/unit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macros/src/types/unit.rs b/macros/src/types/unit.rs index 2a95b502..27c2c5b3 100644 --- a/macros/src/types/unit.rs +++ b/macros/src/types/unit.rs @@ -13,7 +13,7 @@ pub(crate) fn empty_object(attr: &StructAttr, ts_name: Expr) -> DerivedTS { DerivedTS { crate_rename: crate_rename.clone(), inline: quote!("Record".to_owned()), - inline_flattened: None, + inline_flattened: Some(quote!("{ }".to_owned())), docs: attr.docs.clone(), dependencies: Dependencies::new(crate_rename), export: attr.export, From 0aef518d96f481837b543cffe4526d6ae69b794f Mon Sep 17 00:00:00 2001 From: gustavo-shigueo <58121396+gustavo-shigueo@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:01:40 -0300 Subject: [PATCH 3/4] Allow flattening unit struct --- macros/src/types/enum.rs | 53 +++++++++++++++++----------------------- macros/src/types/unit.rs | 2 +- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs index 2366e886..90ca954f 100644 --- a/macros/src/types/enum.rs +++ b/macros/src/types/enum.rs @@ -139,7 +139,7 @@ fn format_variant( }; let formatted = match (untagged_variant, enum_attr.tagged()?) { - (true, _) | (_, Tagged::Untagged) => quote!(#parsed_ty), + (true, _) | (_, Tagged::Untagged) => parsed_ty, (false, Tagged::Externally) => match &variant.fields { Fields::Unit => quote!(format!("\"{}\"", #ts_name)), Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => { @@ -183,36 +183,29 @@ fn format_variant( format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #ts_name, #content, #parsed_ty) ), }, - (false, Tagged::Internally { tag }) => match variant_type.inline_flattened { - Some(_) => { - quote! { #parsed_ty } - } - None => match &variant.fields { - Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => { - let field = &unnamed.unnamed[0]; - let field_attr = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?; - - field_attr.assert_validity(field)?; - - if field_attr.skip { - quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #ts_name)) - } else { - let ty = match field_attr.type_override { - Some(type_override) => quote! { #type_override }, - None => { - let ty = field_attr.type_as(&field.ty); - quote!(<#ty as #crate_rename::TS>::name(cfg)) - } - }; - - quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #ts_name, #ty)) - } - } - Fields::Unit => quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #ts_name)), - _ => { - quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #ts_name, #parsed_ty)) + (false, Tagged::Internally { tag }) => match &variant.fields { + Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => { + let field = &unnamed.unnamed[0]; + let field_attr = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?; + + field_attr.assert_validity(field)?; + + if field_attr.skip { + quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #ts_name)) + } else { + let ty = match field_attr.type_override { + Some(type_override) => quote! { #type_override }, + None => { + let ty = field_attr.type_as(&field.ty); + quote!(<#ty as #crate_rename::TS>::name(cfg)) + } + }; + + quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #ts_name, #ty)) } - }, + } + Fields::Unit => quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #ts_name)), + _ => parsed_ty, }, }; diff --git a/macros/src/types/unit.rs b/macros/src/types/unit.rs index 27c2c5b3..cf6f4b80 100644 --- a/macros/src/types/unit.rs +++ b/macros/src/types/unit.rs @@ -51,7 +51,7 @@ pub(crate) fn null(attr: &StructAttr, ts_name: Expr) -> DerivedTS { DerivedTS { crate_rename: crate_rename.clone(), inline: quote!("null".to_owned()), - inline_flattened: None, + inline_flattened: Some(quote!("{ }".to_owned())), docs: attr.docs.clone(), dependencies: Dependencies::new(crate_rename), export: attr.export, From 647e389709fb091ca2398abfdec0b27e2bf77086 Mon Sep 17 00:00:00 2001 From: gustavo-shigueo <58121396+gustavo-shigueo@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:33:16 -0300 Subject: [PATCH 4/4] Improve #[ts(tag)] validation --- macros/src/attr/enum.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs index e70ba4b5..0f6796df 100644 --- a/macros/src/attr/enum.rs +++ b/macros/src/attr/enum.rs @@ -245,6 +245,15 @@ impl Attr for EnumAttr { item; "content cannot be used without tag" ), + (false, Some(_), None) => { + for variant in item.variants.iter() { + if let Fields::Unnamed(ref unnamed) = variant.fields { + if unnamed.unnamed.len() > 1 { + syn_err_spanned!(variant; r#"`#[ts(tag = "...")]` cannot be used with tuple variants"#); + } + } + } + } _ => (), };