Aleph-Alpha / ts-rs

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

`#[ts(concrete)]`: Allow generic parameter to be "disabled" #264

Closed NyxCode closed 6 months ago

NyxCode commented 6 months ago

This PR aims to allow users to "disable" a generic parameter:

#[ts(concrete(Y = i32)]
struct X<Y> {
  y: Y
}

should behave as if the struct was declared without the generic parameter Y:

struct X {
  y: i32
}

Motivation

The main motivation behind this are associated types, which have no equivalent in TypeScript. Therefore, given a type like this, there's no generic TypeScript type we could generate:

trait MyTrait {
  type AssociatedType;
}

struct X<Y: MyTrait> {
  y: Y::AssociatedType
}

The solution here is to "disable" the generic parameter "Y" by making it "concrete".

#[ts(concrete(Y = MyType))]
struct X<Y: MyTrait> {
  y: Y::AssociatedType
}

TODOs

NyxCode commented 6 months ago

Awesome stuff! I'm still struggeling a bit to understand why this works though.

I read the code a few times, and my understanding of it is:

So for

struct Struct<A, I: Iterator> {
    a: Vec<A>,
    i: I::Item,
}

the associated types are [I::Item], and the type parameters used outside of associated types are [A].

So in the end, we've got impl<I, A> where I: Iterator, I::Item: TS, A: TS.
That seems perfect, great!

escritorio-gustavo commented 6 months ago

I'm still struggeling a bit to understand why this works though.

Yeah, I even failed to failed to realize I had to filter_map rather than just filter on the first commit that added filter_ty. Here's a rough overview of how this works:

  1. Start with dependencies.types
  2. Filter map on filter_ty, which will result in impl Iterator<Item = Vec<Type>>
  3. Flatten into impl Iterator<Item = Type>
  4. Collect into a HashSet to remove duplicates
  5. Recollect into a Vec because HashSet can't be used in quote!

Now here's what filter_ty does:

NyxCode commented 6 months ago

Is there anything you think still needs to be done here? I'm pretty happy with where this ended up, especially with the new trait bounds 😊

escritorio-gustavo commented 6 months ago

Is there anything you think still needs to be done here?

There is nothing I can think of, from my side this is ready for merge. @WilsonGramer in its current state does this PR fully resolve your issue?

I'm pretty happy with where this ended up, especially with the new trait bounds 😊

Me too! And creating that filter function turned out to be a lot of fun :D

escritorio-gustavo commented 6 months ago

@WilsonGramer in its current state does this PR fully resolve your issue?

I think it may be a while until he responds, maybe we should merge this and if needed @WilsonGramer can open a new issue. This PR is already getting way too long anyway

WilsonGramer commented 6 months ago

@escritorio-gustavo @NyxCode Sorry for the delay, I will test it now!

WilsonGramer commented 6 months ago

@escritorio-gustavo It looks like this example with nested structs fails to compile:

use ts_rs::TS;

trait Driver {
    type Info;
}

struct TsDriver;

#[derive(TS)]
struct TsInfo;

impl Driver for TsDriver {
    type Info = TsInfo;
}

#[derive(TS)]
#[ts(export, concrete(D = TsDriver))]
struct Inner<D: Driver> {
    info: D::Info,
}

#[derive(TS)]
#[ts(export, concrete(D = TsDriver))]
struct Outer<D: Driver> {
    inner: Inner<D>,
}
error[E0277]: the trait bound `<D as Driver>::Info: ts_rs::TS` is not satisfied
  --> src/lib.rs:25:12
   |
25 |     inner: Inner<D>,
   |            ^^^^^^^^ the trait `ts_rs::TS` is not implemented for `<D as Driver>::Info`
   |
note: required for `Inner<D>` to implement `ts_rs::TS`
  --> src/lib.rs:16:10
   |
16 | #[derive(TS)]
   |          ^^ unsatisfied trait bound introduced in this `derive` macro
   = note: this error originates in the derive macro `TS` (in Nightly builds, run with -Z macro-backtrace for more info)

I think it's adding the bound to D because it thinks D is being used in Inner, when it's actually only the associated type that's being used. I ran into this issue in my compiler because I have, for example, an Expression<D> that contains a Type<D> — both types only use D::Info.

WilsonGramer commented 6 months ago

If I add a Info: TS bound in the trait, I get another error:

use ts_rs::TS;

trait Driver {
    type Info: TS;
}

struct TsDriver;

#[derive(TS)]
struct TsInfo;

impl Driver for TsDriver {
    type Info = TsInfo;
}

#[derive(TS)]
#[ts(export, concrete(D = TsDriver))]
struct Inner<D: Driver> {
    info: D::Info,
}

#[derive(TS)]
#[ts(export, concrete(D = TsDriver))]
struct Outer<D: Driver> {
    inner: Inner<D>,
}
error[E0277]: the trait bound `TsDriver: ts_rs::TS` is not satisfied
   --> src/lib.rs:22:10
    |
22  | #[derive(TS)]
    |          ^^ the trait `ts_rs::TS` is not implemented for `TsDriver`
    |
    = help: the following other types implement trait `ts_rs::TS`:
              bool
              char
              isize
              i8
              i16
              i32
              i64
              i128
            and 66 others
note: required for `Outer<TsDriver>` to implement `ts_rs::TS`
   --> src/lib.rs:22:10
    |
22  | #[derive(TS)]
    |          ^^ unsatisfied trait bound introduced in this `derive` macro
note: required by a bound in `ts_rs::TS::WithoutGenerics`
   --> /Users/wilson/.cargo/git/checkouts/ts-rs-092fe627e6f08b9c/95e6dac/ts-rs/src/lib.rs:332:27
    |
332 |     type WithoutGenerics: TS + ?Sized;
    |                           ^^ required by this bound in `TS::WithoutGenerics`
    = note: this error originates in the derive macro `TS` (in Nightly builds, run with -Z macro-backtrace for more info)

I believe this is the same issue — it thinks D is used directly because it's passed as a generic parameter to another type.

escritorio-gustavo commented 6 months ago

I am not sure there's anything we can do to automatically fix this, since that would require struct Outer<D: Driver> where D::Info: TS even though Outer doesn't directly contain D::Info, but at least the following code compiles:

use ts_rs::TS;

trait Driver {
    type Info;
}

#[derive(TS)]
struct TsDriver;

#[derive(TS)]
struct TsInfo;

impl Driver for TsDriver {
    type Info = TsInfo;
}

#[derive(TS)]
#[ts(export, concrete(D = TsDriver))]
struct Inner<D: Driver> {
    info: D::Info,
}

#[derive(TS)]
#[ts(export, concrete(D = TsDriver))]
struct Outer<D: Driver>
where
    D::Info: TS,
{
    inner: Inner<D>,
}

You'd need to derive TS for TsDriver and add the where clause I mentioned manually

escritorio-gustavo commented 6 months ago

Alternatively, this also compiles:

use ts_rs::TS;

trait Driver {
    type Info: TS;
}

#[derive(TS)]
struct TsDriver;

#[derive(TS)]
struct TsInfo;

impl Driver for TsDriver {
    type Info = TsInfo;
}

#[derive(TS)]
#[ts(export, concrete(D = TsDriver))]
struct Inner<D: Driver> {
    info: D::Info,
}

#[derive(TS)]
#[ts(export, concrete(D = TsDriver))]
struct Outer<D: Driver> {
    inner: Inner<D>,
}
escritorio-gustavo commented 6 months ago

Either way, you need TsDriver to implement TS because D is directly used in Outer

WilsonGramer commented 6 months ago

@escritorio-gustavo What do you think about adding a #[ts(bound)] attribute to suppress the bounds if needed?

#[derive(TS)]
#[ts(export, concrete(D = TsDriver), bound = "")]
struct Outer<D: Driver> {
    inner: Inner<D>,
}

That way the D::Info: TS bound is only added in the TS impl for Outer, and isn't required for all users of Outer.

NyxCode commented 6 months ago

Yeah, I don't think there's a better trait bound we can generate here.
You'd run into the same issue with serde (literally the exact same error if you replace TS with Serialize).
There, there's a #[serde(bound(..))] workaround to make the trait bounds more specific, though I have never used it.

escritorio-gustavo commented 6 months ago

There, there's a #[serde(bound(..))] workaround to make the trait bounds more specific, though I have never used it.

I've never used it either, I think this should be a brand new issue & PR showing the desired macro expansion, this PR is getting hard to follow

NyxCode commented 6 months ago

@WilsonGramer's snippet

#[derive(TS)]
#[ts(export, concrete(D = TsDriver), bound = "")]
struct Outer<D: Driver> {
inner: Inner<D>,
}

Here, the bound would still need to be D::Info: TS, because Inner is using impl<D: Driver> TS for Inner<D> where D::Inner: TS. Just leaving it blank would not compile.

My suggestion would be to - unless there's something missing in this PR - merge this first, and tackle a potential #[ts(bound)] separately.

The easiest approach there would be to have #[ts(bound(...)] completely replace our trait bounds. Serde does that more granually on a per-field basis (which I think we could also pretty easily do), but we'll have to see which one makes more sense here.

WilsonGramer commented 6 months ago

I think opening a new PR for the bound attribute makes sense! I can draft an issue later today.

NyxCode commented 6 months ago

Awesome, thanks!

Then we're all set & can merge this?