Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Fixes
- Do not emit warning for `#[serde(crate = "..")]` ([#447](https://github.com/Aleph-Alpha/ts-rs/pull/447))
- Fix trait bound generation when using `#[ts(optional)]` on an `Option<Generic>` ([#454](https://github.com/Aleph-Alpha/ts-rs/pull/454))

# 11.1.0
### Features
Expand Down
8 changes: 7 additions & 1 deletion macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use syn::{
parse_quote, spanned::Spanned, ConstParam, Expr, GenericParam, Generics, Item, LifetimeParam,
Path, Result, Type, TypeArray, TypeParam, TypeParen, TypePath, TypeReference, TypeSlice,
Path, QSelf, Result, Type, TypeArray, TypeParam, TypeParen, TypePath, TypeReference, TypeSlice,
TypeTuple, WhereClause, WherePredicate,
};

Expand Down Expand Up @@ -486,6 +486,12 @@ fn used_type_params<'ty, 'out>(
}
}
}
Type::Path(TypePath {
qself: Some(QSelf { ty, .. }),
..
}) => {
used_type_params(out, ty, is_type_param);
}
_ => (),
}
}
Expand Down
14 changes: 6 additions & 8 deletions macros/src/optional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,15 @@ pub fn apply(
// explicit `#[ts(optional)]` on field.
// It takes precedence over the struct attribute, and is enforced **AT COMPILE TIME**
(_, Optional::Optional { nullable }) => (
// expression that evaluates to the string "?", but fails to compile if `ty` is not an `Option`.
parse_quote_spanned! { span => {
fn check_that_field_is_option<T: #crate_rename::IsOption>(_: std::marker::PhantomData<T>) {}
let x: std::marker::PhantomData<#field_ty> = std::marker::PhantomData;
check_that_field_is_option(x);
true
}},
parse_quote!(true),
if nullable {
field_ty.clone()
} else {
unwrap_option(crate_rename, field_ty)
// expression that evaluates to the the Option's inner type,
// but fails to compile if `field_ty` is not an `Option`.
parse_quote_spanned! {
span => <#field_ty as #crate_rename::IsOption>::Inner
}
},
),
// Inherited `#[ts(optional)]` from the struct.
Expand Down
11 changes: 8 additions & 3 deletions macros/src/types/named.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,16 @@ fn format_field(
);
let optional_annotation = quote!(if #is_optional { "?" } else { "" });

if field_attr.type_override.is_none() {
if field_attr.inline || field_attr.flatten {
dependencies.append_from(&ty);
} else {
dependencies.push(&ty);
}
}

if field_attr.flatten {
flattened_fields.push(quote!(<#ty as #crate_rename::TS>::inline_flattened()));
dependencies.append_from(&ty);
return Ok(());
}

Expand All @@ -125,10 +132,8 @@ 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())
} else {
dependencies.push(&ty);
quote!(<#ty as #crate_rename::TS>::name())
}
});
Expand Down
10 changes: 8 additions & 2 deletions macros/src/types/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,21 @@ fn format_field(
field.span(),
);

if field_attr.type_override.is_none() {
if field_attr.inline {
dependencies.append_from(&ty);
} else {
dependencies.push(&ty);
}
}

let formatted_ty = field_attr
.type_override
.map(|t| quote!(#t.to_owned()))
.unwrap_or_else(|| {
if field_attr.inline {
dependencies.append_from(&ty);
quote!(<#ty as #crate_rename::TS>::inline())
} else {
dependencies.push(&ty);
quote!(<#ty as #crate_rename::TS>::name())
}
});
Expand Down
8 changes: 6 additions & 2 deletions ts-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -666,9 +666,13 @@ impl Dependency {
note = "`#[ts(optional)]` was used on a field of type {Self}, which is not permitted",
label = "`#[ts(optional)]` is not allowed on field of type {Self}"
)]
pub trait IsOption {}
pub trait IsOption {
type Inner;
}

impl<T> IsOption for Option<T> {}
impl<T> IsOption for Option<T> {
type Inner = T;
}

// generate impls for primitive types
macro_rules! impl_primitives {
Expand Down
15 changes: 15 additions & 0 deletions ts-rs/tests/integration/optional_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ fn in_struct() {
assert_eq!(OptionalInStruct::inline(), format!("{{ {a}, {b}, {c}, }}"));
}

#[derive(Serialize, TS)]
#[ts(export, export_to = "optional_field/")]
struct GenericOptionalStruct<T> {
#[ts(optional)]
a: Option<T>,
}

#[test]
fn in_generic_struct() {
assert_eq!(
GenericOptionalStruct::<()>::decl(),
"type GenericOptionalStruct<T> = { a?: T, };"
)
}
Copy link

@sinder38 sinder38 Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The integration test will work with or without the changes.
Please use my example from the issue:

#[derive(Serialize, TS)]
#[ts(export, export_to = "optional_field/")]
struct GenericOptionalStruct<T1, T2>
{
	#[ts(optional)] // <- error is happening here
	a: Option<T2>,
	#[ts(optional = nullable)]
	b: Option<T1>,
	c: Option<T1>,
}

Copy link
Collaborator Author

@gustavo-shigueo gustavo-shigueo Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you're right, I tried adding more fields after I finished the fix to be thorough but I forgot that the test only fails if the ONLY field that mentions T is the one with #[ts(optional)], i.e., lines 31 - 34 ruin this test. I'll remove the extra fields

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep the current type though because I don't want the test to imply that the issue comes from having 2 or more generics


#[derive(Serialize, TS)]
#[ts(export, export_to = "optional_field/")]
enum OptionalInEnum {
Expand Down
Loading