Aleph-Alpha / ts-rs

Generate TypeScript bindings from Rust types
MIT License
1.08k stars 107 forks source link

Infered types (`_`) in `#[ts(as = "...")]` #299

Closed escritorio-gustavo closed 5 months ago

escritorio-gustavo commented 5 months ago

Goal

This PR aims to allow the use of infered types (_) inside the #[ts(as = "...")] attribute. The _ type will be interpreted as the field's original type. E.g.:

#[derive(TS)]
struct Foo {
    #[ts(as = "Option<_>"]
    bar: i32,
}

is the same as:

#[derive(TS)]
struct Foo {
    #[ts(as = "Option<i32>"]
    bar: i32,
}

but without having to repeat the original type.

This allows us to somewhat support using #[ts(optional)] with any type, by using #[ts(as = "Option<_>", optional)] without requiring us to change the semantics of #[ts(optional)]

Changes

Added a recursive function to traverse the provided type and replace Type::Infer(_) with the field's original type

Checklist

NyxCode commented 5 months ago

Interesting! So _ refers to the original type, right?

NyxCode commented 5 months ago

I do think this is pretty neat, though I think it might be a bit unintuitive for anyone who doesn't know what _ does in that context.
Have you considered a different syntax, e.g #[ts(as = "Option<type>")]. I feel like we might be able to come up with something more intuitive, but I'm not sure if the extra effort/complexity involved in parsing this would be worth it.

If the syntax was #[ts(as = "Option<$type>")], we might be able to just string-replace $type with the actual type before parsing it into a syn::Type. Pretty hacky, though.

gustavo-shigueo commented 5 months ago

Interesting! So _ refers to the original type, right?

Yes, this is mostly a way to resolve the discussion in #175

gustavo-shigueo commented 5 months ago

I do think this is pretty neat, though I think it might be a bit unintuitive for anyone who doesn't know what _ does in that context. Have you considered a different syntax, e.g #[ts(as = "Option<type>")]. I feel like we might be able to come up with something more intuitive, but I'm not sure if the extra effort/complexity involved in parsing this would be worth it.

Both of these would require some heavy refactoring, because as expects valid syntax for a type. Option<type> isn't a valid type because type is a keyword and any syntax we come up with (like Option<$type>) is not gonna be valid either.

Type::Infer(_) is the only thing I could think of that both:

If the syntax was #[ts(as = "Option<$type>")], we might be able to just string-replace $type with the actual type before parsing it into a syn::Type. Pretty hacky, though.

We could make a parse_type function like:

fn parse_type(input: ParseStream) -> Result<Type> {
    let str = parse_assign_str(input)?;
    syn::parse_str(&str.replace("$type", "_"))
}

but since this function wouldn't have access to the original_type the parser implemented in this PR would have to stay, and this function's job would be to replace our custom syntax with _

abhay-agarwal commented 5 months ago

I think the understore is super intuitive, as it's already widely used throughout rust syntax. So the code is instantly readable. The other nice thing is that you could, for example, turn a type into a vector, such as as = Vec<_>, or as = Result<_,Something>, etc.

escritorio-gustavo commented 5 months ago

There is also precedent for this behavior in serde_with https://github.com/jonasbb/serde_with?tab=readme-ov-file#examples

Annotate your struct or enum to enable the custom de/serializer. The #[serde_as] attribute must be placed before the #[derive].

The as is analogous to the with attribute of serde. You mirror the type structure of the field you want to de/serialize. You can specify converters for the inner types of a field, e.g., Vec<DisplayFromStr>. The default de/serialization behavior can be restored by using _ as a placeholder, e.g., BTreeMap<_, DisplayFromStr>.

NyxCode commented 5 months ago

Oh, alright! Maybe it's just me then.
I feel like the semantics of _ are very different here, but I think you two convinced me!

NyxCode commented 5 months ago

Also, you were spot on with your take in #175. This solution keeps #[ts(optional)] as it was. Specifically, #[ts(optional)] still doesn't alter the type, just how it's represented. This seems super clean to me!

NyxCode commented 5 months ago

Interestingly, this kinda makes #[ts(optional = nullable)] obsolete. It'd be equivalent to

#[ts(as = "Option<_>", optional)]
field: Option<i32>,
escritorio-gustavo commented 5 months ago

Interestingly, this kinda makes #[ts(optional = nullable)] obsolete. It'd be equivalent to

#[ts(as = "Option<_>", optional)]
field: Option<i32>,

I just hope no one is crazy enough to do that 😆