diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs index 3c1fa3d0..e6267144 100644 --- a/macros/src/types/named.rs +++ b/macros/src/types/named.rs @@ -1,8 +1,6 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::{ - spanned::Spanned, Field, FieldsNamed, GenericArgument, Path, PathArguments, Result, Type, -}; +use syn::{Field, FieldsNamed, Path, Result}; use crate::{ attr::{Attr, ContainerAttr, FieldAttr, Inflection, Optional, StructAttr}, @@ -104,53 +102,47 @@ fn format_field( return Ok(()); } - let parsed_ty = field_attr.type_as(&field.ty); + let ty = field_attr.type_as(&field.ty); - let (ty, optional_annotation) = match (struct_optional, field_attr.optional) { + let opt = match (struct_optional, field_attr.optional) { ( - Optional { - optional: true, - nullable, - }, + opt @ Optional { optional: true, .. }, Optional { optional: false, .. }, - ) => match extract_option_argument(&parsed_ty) { - Ok(inner_type) => { - if nullable { - (&parsed_ty, "?") - } else { - (inner_type, "?") - } - } - Err(_) => (&parsed_ty, ""), - }, + ) => opt, + (_, opt @ Optional { optional: true, .. }) => opt, ( - _, - 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 { + opt @ Optional { optional: false, .. }, Optional { optional: false, .. }, - ) => (&parsed_ty, ""), + ) => opt, + }; + + let optional_annotation = if opt.optional { + quote! { if <#ty as #crate_rename::TS>::IS_OPTION { "?" } else { "" } } + } else { + quote! { "" } + }; + + let optional_annotation = if field_attr.optional.optional { + quote! { + if <#ty as #crate_rename::TS>::IS_OPTION { + #optional_annotation + } else { + panic!("`#[ts(optional)]` can only be used with the Option type") + } + } + } else { + optional_annotation }; if field_attr.flatten { flattened_fields.push(quote!(<#ty as #crate_rename::TS>::inline_flattened())); - dependencies.append_from(ty); + dependencies.append_from(&ty); return Ok(()); } @@ -159,11 +151,32 @@ fn format_field( .map(|t| quote!(#t)) .unwrap_or_else(|| { if field_attr.inline { - dependencies.append_from(ty); - quote!(<#ty as #crate_rename::TS>::inline()) + dependencies.append_from(&ty); + + if opt.optional && !opt.nullable { + quote! { + if <#ty as #crate_rename::TS>::IS_OPTION { + <#ty as #crate_rename::TS>::option_inner_inline().unwrap() + } else { + <#ty as #crate_rename::TS>::inline() + } + } + } else { + quote!(<#ty as #crate_rename::TS>::inline()) + } } else { - dependencies.push(ty); - quote!(<#ty as #crate_rename::TS>::name()) + dependencies.push(&ty); + if opt.optional && !opt.nullable { + quote! { + if <#ty as #crate_rename::TS>::IS_OPTION { + <#ty as #crate_rename::TS>::option_inner_name().unwrap() + } else { + <#ty as #crate_rename::TS>::name() + } + } + } else { + quote!(<#ty as #crate_rename::TS>::name()) + } } }); @@ -187,28 +200,3 @@ fn format_field( Ok(()) } - -fn extract_option_argument(ty: &Type) -> Result<&Type> { - match ty { - Type::Path(type_path) - if type_path.qself.is_none() - && type_path.path.leading_colon.is_none() - && type_path.path.segments.len() == 1 - && type_path.path.segments[0].ident == "Option" => - { - let segment = &type_path.path.segments[0]; - match &segment.arguments { - PathArguments::AngleBracketed(args) if args.args.len() == 1 => { - match &args.args[0] { - GenericArgument::Type(inner_ty) => Ok(inner_ty), - other => syn_err!(other.span(); "`Option` argument must be a type"), - } - } - other => { - syn_err!(other.span(); "`Option` type must have a single generic argument") - } - } - } - other => syn_err!(other.span(); "`optional` can only be used on an Option type"), - } -} diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 3d0ad6cd..09534781 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -394,6 +394,19 @@ pub trait TS { /// automatically read from your doc comments or `#[doc = ".."]` attributes const DOCS: Option<&'static str> = None; + #[doc(hidden)] + const IS_OPTION: bool = false; + + #[doc(hidden)] + fn option_inner_name() -> Option { + None + } + + #[doc(hidden)] + fn option_inner_inline() -> Option { + None + } + /// Identifier of this type, excluding generic parameters. fn ident() -> String { // by default, fall back to `TS::name()`. @@ -722,6 +735,7 @@ macro_rules! impl_shadow { impl TS for Option { type WithoutGenerics = Self; + const IS_OPTION: bool = true; fn name() -> String { format!("{} | null", T::name()) @@ -731,6 +745,14 @@ impl TS for Option { format!("{} | null", T::inline()) } + fn option_inner_name() -> Option { + Some(T::name()) + } + + fn option_inner_inline() -> Option { + Some(T::inline()) + } + fn visit_dependencies(v: &mut impl TypeVisitor) where Self: 'static, diff --git a/ts-rs/tests/integration/optional_field.rs b/ts-rs/tests/integration/optional_field.rs index f43d3445..00aaa307 100644 --- a/ts-rs/tests/integration/optional_field.rs +++ b/ts-rs/tests/integration/optional_field.rs @@ -88,6 +88,9 @@ fn inline() { assert_eq!(Inline::inline(), format!("{{ x: {{ {a}, {b}, {c}, }}, }}")); } +type Foo = Option; +type Bar = Option; + #[derive(TS)] #[ts(export, export_to = "optional_field/", optional)] struct OptionalStruct { @@ -97,13 +100,19 @@ struct OptionalStruct { #[ts(optional = nullable)] c: Option, + #[ts(optional = nullable)] d: i32, + + e: Foo, + f: Bar, } #[test] fn struct_optional() { assert_eq!( OptionalStruct::inline(), - format!("{{ a?: number, b?: number, c?: number | null, d: number, }}") + format!( + "{{ a?: number, b?: number, c?: number | null, d: number, e?: number, f?: number, }}" + ) ) }