Skip to content

Commit 858f413

Browse files
Fix trait bound generation when using #[ts(optional)] on an Option<Generic> (#454)
* Add test for generic optional * Fix dependecy tracking * Simplify compile-time enforcing of Option * Update CHANGELOG * Remove extra fields from test to make sure it actually tests the problem * Go back to applying optional before adding to dependencies * Handle qualified types
1 parent e2c7f7c commit 858f413

File tree

7 files changed

+51
-16
lines changed

7 files changed

+51
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

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

89
# 11.1.0
910
### Features

macros/src/lib.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use proc_macro2::{Ident, TokenStream};
77
use quote::{format_ident, quote};
88
use syn::{
99
parse_quote, spanned::Spanned, ConstParam, Expr, GenericParam, Generics, Item, LifetimeParam,
10-
Path, Result, Type, TypeArray, TypeParam, TypeParen, TypePath, TypeReference, TypeSlice,
10+
Path, QSelf, Result, Type, TypeArray, TypeParam, TypeParen, TypePath, TypeReference, TypeSlice,
1111
TypeTuple, WhereClause, WherePredicate,
1212
};
1313

@@ -486,6 +486,12 @@ fn used_type_params<'ty, 'out>(
486486
}
487487
}
488488
}
489+
Type::Path(TypePath {
490+
qself: Some(QSelf { ty, .. }),
491+
..
492+
}) => {
493+
used_type_params(out, ty, is_type_param);
494+
}
489495
_ => (),
490496
}
491497
}

macros/src/optional.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,15 @@ pub fn apply(
7777
// explicit `#[ts(optional)]` on field.
7878
// It takes precedence over the struct attribute, and is enforced **AT COMPILE TIME**
7979
(_, Optional::Optional { nullable }) => (
80-
// expression that evaluates to the string "?", but fails to compile if `ty` is not an `Option`.
81-
parse_quote_spanned! { span => {
82-
fn check_that_field_is_option<T: #crate_rename::IsOption>(_: std::marker::PhantomData<T>) {}
83-
let x: std::marker::PhantomData<#field_ty> = std::marker::PhantomData;
84-
check_that_field_is_option(x);
85-
true
86-
}},
80+
parse_quote!(true),
8781
if nullable {
8882
field_ty.clone()
8983
} else {
90-
unwrap_option(crate_rename, field_ty)
84+
// expression that evaluates to the the Option's inner type,
85+
// but fails to compile if `field_ty` is not an `Option`.
86+
parse_quote_spanned! {
87+
span => <#field_ty as #crate_rename::IsOption>::Inner
88+
}
9189
},
9290
),
9391
// Inherited `#[ts(optional)]` from the struct.

macros/src/types/named.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,16 @@ fn format_field(
114114
);
115115
let optional_annotation = quote!(if #is_optional { "?" } else { "" });
116116

117+
if field_attr.type_override.is_none() {
118+
if field_attr.inline || field_attr.flatten {
119+
dependencies.append_from(&ty);
120+
} else {
121+
dependencies.push(&ty);
122+
}
123+
}
124+
117125
if field_attr.flatten {
118126
flattened_fields.push(quote!(<#ty as #crate_rename::TS>::inline_flattened()));
119-
dependencies.append_from(&ty);
120127
return Ok(());
121128
}
122129

@@ -125,10 +132,8 @@ fn format_field(
125132
.map(|t| quote!(#t))
126133
.unwrap_or_else(|| {
127134
if field_attr.inline {
128-
dependencies.append_from(&ty);
129135
quote!(<#ty as #crate_rename::TS>::inline())
130136
} else {
131-
dependencies.push(&ty);
132137
quote!(<#ty as #crate_rename::TS>::name())
133138
}
134139
});

macros/src/types/tuple.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,21 @@ fn format_field(
6666
field.span(),
6767
);
6868

69+
if field_attr.type_override.is_none() {
70+
if field_attr.inline {
71+
dependencies.append_from(&ty);
72+
} else {
73+
dependencies.push(&ty);
74+
}
75+
}
76+
6977
let formatted_ty = field_attr
7078
.type_override
7179
.map(|t| quote!(#t.to_owned()))
7280
.unwrap_or_else(|| {
7381
if field_attr.inline {
74-
dependencies.append_from(&ty);
7582
quote!(<#ty as #crate_rename::TS>::inline())
7683
} else {
77-
dependencies.push(&ty);
7884
quote!(<#ty as #crate_rename::TS>::name())
7985
}
8086
});

ts-rs/src/lib.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -669,9 +669,13 @@ impl Dependency {
669669
note = "`#[ts(optional)]` was used on a field of type {Self}, which is not permitted",
670670
label = "`#[ts(optional)]` is not allowed on field of type {Self}"
671671
)]
672-
pub trait IsOption {}
672+
pub trait IsOption {
673+
type Inner;
674+
}
673675

674-
impl<T> IsOption for Option<T> {}
676+
impl<T> IsOption for Option<T> {
677+
type Inner = T;
678+
}
675679

676680
// generate impls for primitive types
677681
macro_rules! impl_primitives {

ts-rs/tests/integration/optional_field.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@ fn in_struct() {
2121
assert_eq!(OptionalInStruct::inline(), format!("{{ {a}, {b}, {c}, }}"));
2222
}
2323

24+
#[derive(Serialize, TS)]
25+
#[ts(export, export_to = "optional_field/")]
26+
struct GenericOptionalStruct<T> {
27+
#[ts(optional)]
28+
a: Option<T>,
29+
}
30+
31+
#[test]
32+
fn in_generic_struct() {
33+
assert_eq!(
34+
GenericOptionalStruct::<()>::decl(),
35+
"type GenericOptionalStruct<T> = { a?: T, };"
36+
)
37+
}
38+
2439
#[derive(Serialize, TS)]
2540
#[ts(export, export_to = "optional_field/")]
2641
enum OptionalInEnum {

0 commit comments

Comments
 (0)