From 2baa543e32cfe4c46d68a0fbaa5e46dc956e5ad5 Mon Sep 17 00:00:00 2001 From: Jose Marin Date: Tue, 17 Oct 2023 15:13:49 -0400 Subject: [PATCH 01/10] add "as" derive helper attribute --- macros/src/attr/field.rs | 4 ++++ macros/src/types/named.rs | 28 ++++++++++++++++------ macros/src/types/newtype.rs | 20 ++++++++++++---- macros/src/types/tuple.rs | 19 +++++++++++---- ts-rs/tests/type_as.rs | 47 +++++++++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 ts-rs/tests/type_as.rs diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs index 13daef55d..4745e69d2 100644 --- a/macros/src/attr/field.rs +++ b/macros/src/attr/field.rs @@ -5,6 +5,7 @@ use crate::utils::parse_attrs; #[derive(Default)] pub struct FieldAttr { + pub type_as: Option, pub type_override: Option, pub rename: Option, pub inline: bool, @@ -29,6 +30,7 @@ impl FieldAttr { fn merge( &mut self, FieldAttr { + type_as, type_override, rename, inline, @@ -38,6 +40,7 @@ impl FieldAttr { }: FieldAttr, ) { self.rename = self.rename.take().or(rename); + self.type_as = self.type_as.take().or(type_as); self.type_override = self.type_override.take().or(type_override); self.inline = self.inline || inline; self.skip = self.skip || skip; @@ -48,6 +51,7 @@ impl FieldAttr { impl_parse! { FieldAttr(input, out) { + "as" => out.type_as = Some(parse_assign_str(input)?), "type" => out.type_override = Some(parse_assign_str(input)?), "rename" => out.rename = Some(parse_assign_str(input)?), "inline" => out.inline = true, diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs index 87861243a..262eb6b2b 100644 --- a/macros/src/types/named.rs +++ b/macros/src/types/named.rs @@ -63,6 +63,7 @@ fn format_field( generics: &Generics, ) -> Result<()> { let FieldAttr { + type_as, type_override, rename, inline, @@ -75,16 +76,29 @@ fn format_field( return Ok(()); } - let (ty, optional_annotation) = match optional { - true => (extract_option_argument(&field.ty)?, "?"), - false => (&field.ty, ""), + if let (Some(_type_as), Some(_type_override)) = (&type_as, &type_override) { + syn_err!("`type` is not compatible with `as`") + } + + let parsed_ty = if let Some(_type_as) = &type_as { + syn::parse_str::(_type_as)? + } else { + field.ty.clone() + }; + + let (ty, optional_annotation) = { + match optional { + true => (extract_option_argument(&parsed_ty)?, "?"), + false => (&parsed_ty, ""), + } }; if flatten { - match (&type_override, &rename, inline) { - (Some(_), _, _) => syn_err!("`type` is not compatible with `flatten`"), - (_, Some(_), _) => syn_err!("`rename` is not compatible with `flatten`"), - (_, _, true) => syn_err!("`inline` is not compatible with `flatten`"), + match (&type_as, &type_override, &rename, inline) { + (Some(_), _, _, _) => syn_err!("`as` is not compatible with `flatten`"), + (_, Some(_), _, _) => syn_err!("`type` is not compatible with `flatten`"), + (_, _, Some(_), _) => syn_err!("`rename` is not compatible with `flatten`"), + (_, _, _, true) => syn_err!("`inline` is not compatible with `flatten`"), _ => {} } diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs index b5070d75a..af9e75de1 100644 --- a/macros/src/types/newtype.rs +++ b/macros/src/types/newtype.rs @@ -1,5 +1,5 @@ use quote::quote; -use syn::{FieldsUnnamed, Generics, Result}; +use syn::{FieldsUnnamed, Generics, Result, Type}; use crate::{ attr::{FieldAttr, StructAttr}, @@ -22,6 +22,7 @@ pub(crate) fn newtype( } let inner = fields.unnamed.first().unwrap(); let FieldAttr { + type_as, type_override, rename: rename_inner, inline, @@ -38,17 +39,26 @@ pub(crate) fn newtype( _ => {} }; - let inner_ty = &inner.ty; + if let (Some(_type_as), Some(_type_override)) = (&type_as, &type_override) { + syn_err!("`type` is not compatible with `as`") + } + + let inner_ty = if let Some(_type_as) = &type_as { + syn::parse_str::(_type_as)? + } else { + inner.ty.clone() + }; + let mut dependencies = Dependencies::default(); match (inline, &type_override) { (_, Some(_)) => (), - (true, _) => dependencies.append_from(inner_ty), - (false, _) => dependencies.push_or_append_from(inner_ty), + (true, _) => dependencies.append_from(&inner_ty), + (false, _) => dependencies.push_or_append_from(&inner_ty), }; let inline_def = match &type_override { Some(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 => format_type(&inner_ty, &mut dependencies, generics), }; let generic_args = format_generics(&mut dependencies, generics); diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs index ce64c625d..eff6cc61a 100644 --- a/macros/src/types/tuple.rs +++ b/macros/src/types/tuple.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::{Field, FieldsUnnamed, Generics, Result}; +use syn::{Field, FieldsUnnamed, Generics, Result, Type}; use crate::{ attr::{FieldAttr, StructAttr}, @@ -58,8 +58,8 @@ fn format_field( field: &Field, generics: &Generics, ) -> Result<()> { - let ty = &field.ty; let FieldAttr { + type_as, type_override, rename, inline, @@ -71,6 +71,15 @@ fn format_field( if skip { return Ok(()); } + + let ty = if let Some(_type_as) = &type_as { + syn::parse_str::(_type_as)? + } else { + field.ty.clone() + }; + if let (Some(_type_as), Some(_type_override)) = (&type_as, &type_override) { + syn_err!("`type` is not compatible with `as`") + } if rename.is_some() { syn_err!("`rename` is not applicable to tuple structs") } @@ -84,16 +93,16 @@ fn format_field( formatted_fields.push(match &type_override { Some(o) => quote!(#o.to_owned()), None if inline => quote!(<#ty as ts_rs::TS>::inline()), - None => format_type(ty, dependencies, generics), + None => format_type(&ty, dependencies, generics), }); match (inline, &type_override) { (_, Some(_)) => (), (false, _) => { - dependencies.push_or_append_from(ty); + dependencies.push_or_append_from(&ty); } (true, _) => { - dependencies.append_from(ty); + dependencies.append_from(&ty); } }; diff --git a/ts-rs/tests/type_as.rs b/ts-rs/tests/type_as.rs new file mode 100644 index 000000000..5adf991ed --- /dev/null +++ b/ts-rs/tests/type_as.rs @@ -0,0 +1,47 @@ +#![allow(dead_code)] + +use std::time::Instant; + +use ts_rs::TS; + +#[derive(TS)] +struct ExternalTypeDef { + a: i32, + b: i32, + c: i32, +} + +#[test] +fn struct_properties() { + #[derive(TS)] + struct Override { + a: i32, + #[ts(as = "ExternalTypeDef")] + #[ts(inline)] + x: Instant, + } + + assert_eq!( + Override::inline(), + "{ a: number, x: { a: number, b: number, c: number, }, }" + ) +} + +#[test] +fn enum_variants() { + #[derive(TS)] + enum OverrideEnum { + A(#[ts(as = "ExternalTypeDef")] Instant), + B { + #[ts(as = "ExternalTypeDef")] + x: i32, + y: i32, + z: i32, + }, + } + + assert_eq!( + OverrideEnum::inline(), + r#"{ "A": ExternalTypeDef } | { "B": { x: ExternalTypeDef, y: number, z: number, } }"# + ) +} From 6b65f056af50fcc6f48ab2cee321b6c99362eda8 Mon Sep 17 00:00:00 2001 From: Jose Marin Date: Fri, 20 Oct 2023 15:54:02 -0400 Subject: [PATCH 02/10] fix: missing type_as implementations for enums --- macros/src/types/enum.rs | 43 +++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs index 043379a47..a233bb2c0 100644 --- a/macros/src/types/enum.rs +++ b/macros/src/types/enum.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Fields, Generics, ItemEnum, Variant}; +use syn::{Fields, Generics, ItemEnum, Type, Variant}; use crate::{ attr::{EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr}, @@ -95,12 +95,26 @@ fn format_variant( }, Tagged::Adjacently { tag, content } => match &variant.fields { Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => { - let FieldAttr { type_override, .. } = - FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?; + let FieldAttr { + type_as, + type_override, + .. + } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?; + + if let (Some(_type_as), Some(_type_override)) = (&type_as, &type_override) { + syn_err!("`type` is not compatible with `as`") + } + + let parsed_ty = if let Some(_type_as) = &type_as { + syn::parse_str::(_type_as)? + } else { + unnamed.unnamed[0].ty.clone() + }; + let ty = if let Some(type_override) = type_override { quote! { #type_override } } else { - format_type(&unnamed.unnamed[0].ty, dependencies, generics) + format_type(&parsed_ty, dependencies, generics) }; quote!(format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #ty)) } @@ -120,13 +134,28 @@ fn format_variant( }, None => match &variant.fields { Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => { - let FieldAttr { type_override, .. } = - FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?; + let FieldAttr { + type_as, + type_override, + .. + } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?; + + if let (Some(_type_as), Some(_type_override)) = (&type_as, &type_override) { + syn_err!("`type` is not compatible with `as`") + } + + let parsed_ty = if let Some(_type_as) = &type_as { + syn::parse_str::(_type_as)? + } else { + unnamed.unnamed[0].ty.clone() + }; + let ty = if let Some(type_override) = type_override { quote! { #type_override } } else { - format_type(&unnamed.unnamed[0].ty, dependencies, generics) + format_type(&parsed_ty, dependencies, generics) }; + quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #name, #ty)) } Fields::Unit => quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name)), From 979ea9b47577eb4817f2fe7587157badb0d04bb4 Mon Sep 17 00:00:00 2001 From: escritorio-gustavo <131818645+escritorio-gustavo@users.noreply.github.com> Date: Tue, 30 Jan 2024 10:23:03 -0300 Subject: [PATCH 03/10] Fix expected &str found String --- macros/src/types/enum.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs index bb4000b0b..9f52987eb 100644 --- a/macros/src/types/enum.rs +++ b/macros/src/types/enum.rs @@ -119,7 +119,7 @@ fn format_variant( let ty = match (type_override, type_as) { (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_as)?, dependencies, generics), + (None, Some(type_as)) => format_type(&syn::parse_str::(&type_as)?, dependencies, generics), (None, None) => format_type(unnamed.unnamed[0].ty, dependencies, generics), }; @@ -163,7 +163,7 @@ fn format_variant( let ty = match (type_override, type_as) { (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_as)?, dependencies, generics), + (None, Some(type_as)) => format_type(&syn::parse_str::(&type_as)?, dependencies, generics), (None, None) => format_type(unnamed.unnamed[0].ty, dependencies, generics), }; From 836e145b928030f1e2269d47868e1893953eb023 Mon Sep 17 00:00:00 2001 From: escritorio-gustavo <131818645+escritorio-gustavo@users.noreply.github.com> Date: Tue, 30 Jan 2024 10:24:58 -0300 Subject: [PATCH 04/10] Fix expected &Type found Type --- macros/src/types/enum.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs index 9f52987eb..4a1985fbf 100644 --- a/macros/src/types/enum.rs +++ b/macros/src/types/enum.rs @@ -120,7 +120,7 @@ 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_as)?, dependencies, generics), - (None, None) => format_type(unnamed.unnamed[0].ty, dependencies, generics), + (None, None) => format_type(&unnamed.unnamed[0].ty, dependencies, generics), }; if skip { @@ -164,7 +164,7 @@ 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_as)?, dependencies, generics), - (None, None) => format_type(unnamed.unnamed[0].ty, dependencies, generics), + (None, None) => format_type(&unnamed.unnamed[0].ty, dependencies, generics), }; if skip { From 96a664a160120ffc4d41d16b8157aa047c354d61 Mon Sep 17 00:00:00 2001 From: escritorio-gustavo <131818645+escritorio-gustavo@users.noreply.github.com> Date: Tue, 30 Jan 2024 10:28:30 -0300 Subject: [PATCH 05/10] Remove duplicated check --- macros/src/types/enum.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs index 4a1985fbf..8978fa52d 100644 --- a/macros/src/types/enum.rs +++ b/macros/src/types/enum.rs @@ -112,10 +112,6 @@ fn format_variant( .. } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?; - if type_as.is_some() && type_override.is_some() { - syn_err!("`type` is not compatible with `as`") - } - let ty = match (type_override, type_as) { (Some(_), Some(_)) => syn_err!("`type` is not compatible with `as`"), (Some(type_override), None) => quote! { #type_override }, From f9a7ee423a7442a71b8b574127c15e4232413a99 Mon Sep 17 00:00:00 2001 From: escritorio-gustavo <131818645+escritorio-gustavo@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:08:52 -0300 Subject: [PATCH 06/10] Apply requested changes to named.rs --- macros/src/types/named.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs index d6e698bed..8145f7578 100644 --- a/macros/src/types/named.rs +++ b/macros/src/types/named.rs @@ -92,21 +92,19 @@ fn format_field( return Ok(()); } - if let (Some(_type_as), Some(_type_override)) = (&type_as, &type_override) { + if type_as.is_some() && type_override.is_some() { syn_err!("`type` is not compatible with `as`") } - let parsed_ty = if let Some(_type_as) = &type_as { - syn::parse_str::(_type_as)? + let parsed_ty = if let Some(ref type_as) = type_as { + syn::parse_str::(type_as)? } else { field.ty.clone() }; - let (ty, optional_annotation) = { - match optional { - true => (extract_option_argument(&parsed_ty)?, "?"), - false => (&parsed_ty, ""), - } + let (ty, optional_annotation) = match optional { + true => (extract_option_argument(&parsed_ty)?, "?"), + false => (&parsed_ty, ""), }; if flatten { From 76ea907551cf29afae49f55147d39f252e6ad54c Mon Sep 17 00:00:00 2001 From: escritorio-gustavo <131818645+escritorio-gustavo@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:11:40 -0300 Subject: [PATCH 07/10] Apply requested changes to newtype.rs --- macros/src/types/newtype.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs index 3753cae30..a51ee45a1 100644 --- a/macros/src/types/newtype.rs +++ b/macros/src/types/newtype.rs @@ -39,24 +39,26 @@ pub(crate) fn newtype( _ => {} }; - if let (Some(_type_as), Some(_type_override)) = (&type_as, &type_override) { + if type_as.is_some() && type_override.is_some() { syn_err!("`type` is not compatible with `as`") } - let inner_ty = if let Some(_type_as) = &type_as { - syn::parse_str::(_type_as)? + let inner_ty = if let Some(ref type_as) = type_as { + syn::parse_str::(type_as)? } else { inner.ty.clone() }; let mut dependencies = Dependencies::default(); - match (inline, &type_override) { - (_, Some(_)) => (), - (true, _) => dependencies.append_from(&inner_ty), - (false, _) => dependencies.push_or_append_from(&inner_ty), + + match (type_override.is_none(), inline) { + (false, _) => (), + (true, true) => dependencies.append_from(&inner_ty), + (true, false) => dependencies.push_or_append_from(&inner_ty), }; - let inline_def = match &type_override { - Some(o) => quote!(#o.to_owned()), + + 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), }; From d5d202614f21d0cd0211b894a761af4dcedb327f Mon Sep 17 00:00:00 2001 From: escritorio-gustavo <131818645+escritorio-gustavo@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:12:24 -0300 Subject: [PATCH 08/10] Apply requested changes to tuple.rs --- macros/src/types/tuple.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs index eff6cc61a..2704f0a4e 100644 --- a/macros/src/types/tuple.rs +++ b/macros/src/types/tuple.rs @@ -71,32 +71,36 @@ fn format_field( if skip { return Ok(()); } - - let ty = if let Some(_type_as) = &type_as { - syn::parse_str::(_type_as)? + + let ty = if let Some(ref type_as) = type_as { + syn::parse_str::(type_as)? } else { field.ty.clone() }; - if let (Some(_type_as), Some(_type_override)) = (&type_as, &type_override) { + + if type_as.is_some() && type_override.is_some() { syn_err!("`type` is not compatible with `as`") } + if rename.is_some() { syn_err!("`rename` is not applicable to tuple structs") } + if optional { syn_err!("`optional` is not applicable to tuple fields") } + if flatten { syn_err!("`flatten` is not applicable to tuple fields") } - formatted_fields.push(match &type_override { - Some(o) => quote!(#o.to_owned()), + 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), }); - match (inline, &type_override) { + match (inline, type_override) { (_, Some(_)) => (), (false, _) => { dependencies.push_or_append_from(&ty); From c7bcb73dc4da72eaea90c61f26dad238c83df82a Mon Sep 17 00:00:00 2001 From: escritorio-gustavo <131818645+escritorio-gustavo@users.noreply.github.com> Date: Tue, 30 Jan 2024 16:19:05 -0300 Subject: [PATCH 09/10] Use parsed_ty --- macros/src/types/named.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs index 39924090f..d82bc5979 100644 --- a/macros/src/types/named.rs +++ b/macros/src/types/named.rs @@ -107,11 +107,11 @@ fn format_field( Optional { optional: true, nullable } => { let inner_type = extract_option_argument(&field.ty)?; // inner type of the optional match nullable { - true => (&field.ty, "?"), // if it's nullable, we keep the original type + true => (&parsed_ty, "?"), // if it's nullable, we keep the original type false => (inner_type, "?"), // if not, we use the Option's inner type } }, - Optional { optional: false, .. } => (&field.ty, "") + Optional { optional: false, .. } => (&parsed_ty, "") }; if flatten { From 5773e28cb67fda92c1614591ae41890b88c5e506 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Tue, 30 Jan 2024 21:08:42 +0100 Subject: [PATCH 10/10] fix interaction with #[ts(optional)], expand tests --- macros/src/types/named.rs | 15 ++++++++++----- ts-rs/tests/type_as.rs | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs index d82bc5979..d61267b89 100644 --- a/macros/src/types/named.rs +++ b/macros/src/types/named.rs @@ -2,6 +2,7 @@ use proc_macro2::TokenStream; use quote::quote; use syn::{Field, FieldsNamed, GenericArgument, Generics, PathArguments, Result, Type}; +use crate::attr::Optional; use crate::{ attr::{FieldAttr, Inflection, StructAttr}, deps::Dependencies, @@ -9,7 +10,6 @@ use crate::{ utils::{raw_name_to_ts_field, to_ts_ident}, DerivedTS, }; -use crate::attr::Optional; pub(crate) fn named( attr: &StructAttr, @@ -104,14 +104,19 @@ fn format_field( }; let (ty, optional_annotation) = match optional { - Optional { optional: true, nullable } => { - let inner_type = extract_option_argument(&field.ty)?; // inner type of the optional + Optional { + optional: true, + nullable, + } => { + let inner_type = extract_option_argument(&parsed_ty)?; // inner type of the optional match nullable { true => (&parsed_ty, "?"), // if it's nullable, we keep the original type false => (inner_type, "?"), // if not, we use the Option's inner type } - }, - Optional { optional: false, .. } => (&parsed_ty, "") + } + Optional { + optional: false, .. + } => (&parsed_ty, ""), }; if flatten { diff --git a/ts-rs/tests/type_as.rs b/ts-rs/tests/type_as.rs index 5adf991ed..f727bc75d 100644 --- a/ts-rs/tests/type_as.rs +++ b/ts-rs/tests/type_as.rs @@ -1,9 +1,15 @@ #![allow(dead_code)] +use std::cell::UnsafeCell; +use std::mem::MaybeUninit; +use std::ptr::NonNull; +use std::sync::atomic::AtomicPtr; use std::time::Instant; use ts_rs::TS; +type Unsupported = UnsafeCell>>>; + #[derive(TS)] struct ExternalTypeDef { a: i32, @@ -19,12 +25,18 @@ fn struct_properties() { #[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, }, }" - ) + "{ a: number, x: { a: number, b: number, c: number, }, y: ExternalTypeDef, }" + ); + assert!(Override::dependencies() + .iter() + .any(|d| d.ts_name == "ExternalTypeDef")); } #[test] @@ -34,7 +46,7 @@ fn enum_variants() { A(#[ts(as = "ExternalTypeDef")] Instant), B { #[ts(as = "ExternalTypeDef")] - x: i32, + x: Unsupported, y: i32, z: i32, }, @@ -45,3 +57,22 @@ fn enum_variants() { r#"{ "A": ExternalTypeDef } | { "B": { x: ExternalTypeDef, y: number, z: number, } }"# ) } + +#[test] +fn complex() { + #[derive(TS)] + struct Outer { + #[ts(as = "Option")] + #[ts(optional = nullable, inline)] + x: Unsupported, + #[ts(as = "Option")] + #[ts(optional = nullable)] + y: Unsupported, + } + + let external = ExternalTypeDef::inline(); + assert_eq!( + Outer::inline(), + format!(r#"{{ x?: {external} | null, y?: ExternalTypeDef | null, }}"#) + ) +}