Repository: Aleph-Alpha/ts-rs
Pull request #264 (2024-03-11 15:08:31)

`#[ts(concrete)]`: Allow generic parameter to be "disabled"
This PR aims to allow users to "disable" a generic parameter:

```rs
#[ts(concrete(Y = i32)]
struct X<Y> {
  y: Y
}
```
should behave as if the struct was declared without the generic parameter `Y`:
```rs
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:

```rs
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".
```rs
#[ts(concrete(Y = MyType))]
struct X<Y: MyTrait> {
  y: Y::AssociatedType
}
```

## TODOs
- [x] Cleanup
- [x] Figure out how exactly `TS::decl` and `TS::decl_concrete` should behave
  - `decl` is clear, and `decl_concrete` still uses the actual type parameters, even if `#[ts(concrete)]` is present.
- [x] Write more tests
- [x] How does this intersect with dependency tracking?
  - not at all. We end up with less types in `TS::generics` though.

escritorio-gustavo: Hey @NyxCode! I was quite confused when I saw #262 and had no idea what `#[ts(concrete)]` was lol

I'm still kinda confused on some things:

- Is the PR title a typo or are you planning a `#[ts(discrete)]` attribute as well?

- > This PR aims to allow users to "disable" a generic parameter:
  > 
  > ```rust
  > #[ts(concrete(Y = i32)]
  > struct X<Y> {
  >   y: Y
  > }
  > ```
  > should behave as if the struct was declared without the generic parameter `Y`:
  > 
  > ```rust
  > struct X<Y> {
  >   y: i32
  > }
  > ```

In this second definition, should `Y` still be present? The description sounds like it's not.

- > 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:
  > 
  > ```rust
  > 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".
  > ```rs
  > #[ts(concrete(Y = MyType))]
  > struct X<Y: MyTrait> {
  >   y: Y::AssociatedType
  > }
  > ```

I think I get it, but shouldn't `Y` be constrained to `Y: MyTrait<AssociatedType = MyType>` to avoid users doing weird stuff with their types like:

```rs
struct MyType;

impl MyTrait for MyType {
    type AssociatedType = i32;
}

struct OtherType;

impl OtherTrait for MyType {
    type AssociatedType = String;
}

#[ts(concrete(Y = MyType))]
struct X<Y: MyTrait> {
  y: Y::AssociatedType
}
```

This would generate valid TS and Rust would be totally cool with it, but the user can shoot themselves in the foot by serializing `X<OtherType>`, causing their TS type to be wrong (`y` will be typed as `number` but contain a `string`)

escritorio-gustavo: We also don't really support user defined traits as generic constraints for our derive macro anyway, so wouldn't this be a very limited usecase? Or does the fact that this removes the generic altogether eliminate that problem?
escritorio-gustavo: Last thing, does this add the `concrete` type to the dependencies?
escritorio-gustavo: > But what do we do for the concrete/disabled generic parameters? Should be keep them as they are as well, or should we replace them with the user-provided one as well?

I _think_ we should use the type passed into `#[ts(concrete)]`, since the idea is to pretend there is no generic at all
NyxCode: > Is the PR title a typo or are you planning a #[ts(discrete)] attribute as well?

😆 A typo, was studying maths before, I think that's where that came from ^^

> In this second definition, should Y still be present? The description sounds like it's not.

Correct, sorry about that. I think the 2nd snippet wouldn't even compile as-is. Fixed!

> I think I get it, but shouldn't Y be constrained to Y: MyTrait<AssociatedType = MyType> to avoid users doing weird stuff with their types

I think allowing that is intended, see #261 for @WilsonGramer's usecase.  
Besides, that's just the effect "disabling" a generic parameter has - you're only getting bindings for one specific type, and you have to make sure not to mix them.
It's the same case for the `struct X<Y>` in the description of this PR - There, you're also only getting bindings for one specific type, `X<i32>`

> We also don't really support user defined traits as generic constraints for our derive macro anyway, so wouldn't this be a very limited usecase? Or does the fact that this removes the generic altogether eliminate that problem?

Good question! So this prototype here kind of works, but I'll have to go back and see why exactly 😆 

But in general: We couldn't deal with trait bounds before because we're generating dummy types and plugging them into the generic parameters. If there's a trait bound we don't know about, we can't do that.
But with `#[ts(concrete)]`, the user is giving us the type to use, so we don't use a dummy type for that parameter.

escritorio-gustavo: > 😆 A typo, was studying maths before, I think that's where that came from ^^

I see! You must be neck deep in set theory 😆 

> I think allowing that is intended, see #261 for @WilsonGramer's usecase.
> Besides, that's just the effect "disabling" a generic parameter has - you're only getting bindings for one specific type, and you have to make sure not to mix them.
> It's the same case for the `struct X<Y>` in the description of this PR - There, you're also only getting bindings for one specific type, `X<i32>`

I hadn't even seen #261 lol, I think I just didn't see a linked issue, so I didn't look for one, my bad!

> But with `#[ts(concrete)]`, the user is giving us the type to use, so we don't use a dummy type for that parameter.

That makes sense, we use `MyType` as a replacement for the dummies, so if `MyType: MyTrait` it just works

> So this prototype here kind of works, but I'll have to go back and see why exactly 😆

Always fun when our code works and we don't know why 🤣 
NyxCode: I'm definetely still open to alternative solutions to the "associated types" problem!  
The thought process with this here was

"There are associated types in the struct" 
=> "We can't make that a generic TS def, since TS doesn't have that feature" 
=> "The generated TS type can't be generic"
=> "To make it that, we disable the generic parameter and specifying a concrete type instead"
escritorio-gustavo: > does this add the concrete type to the dependencies?

So, I experimented with this
```rs
for ty in attr.concrete.values() {
    dependencies.push(ty);
}
```

And this doesn't quite work, because in the `concrete_generic` test for example, this would add `TsDriver` to `dependencies`, when we want to add `TsDriver::Info`
NyxCode: > Edit, I just realized that this works with dependencies out of the box with the associated types, I can push a test for that

Was just about to write that I *think* it just works.

We're doing a `SomeType<A, B, C>` -> `SomeType<DummyA, DummyB, Concrete>` conversion in `decl`, and from then on, dependencies are resolved as they normally are, so everything *should* be fine.

There are a few interesting edge-cases though, so tests would be great! 
escritorio-gustavo: The case that won't work is this:
```rs
#[derive(TS)]
#[ts(export, export_to = "concrete_generic/", concrete(T = OtherDriver))]
struct OtherStruct<T: Driver> {
    u: T::Info,
    x: T,
}
```
escritorio-gustavo: As far as the "cleanup" todo goes, we could remove some clutter from `macros/src/attr/mod.rs` by breaking some of those functions/structs into their own modules
NyxCode: Right!
So the case which would be nice if it worked is:

```rs
#[derive(TS)]
struct OtherDriver;
impl Driver for OtherDriver {
    type Info = Foo;
}

#[derive(TS)]
#[ts(concrete(T = OtherDriver))]
struct OtherStruct<T: Driver> {
    u: T::Info,
    x: T,
}
```

But:

The `impl TS` for `OtherStruct` is still generic, and must work for every `T` we pass in.  
It's only at the step that we call `decl()` that we go from `OtherStruct<Something>` to `OtherStruct<OtherDriver>`.

Maybe we could generate an `impl TS for OtherStruct<OtherDriver>` instead? That'd be pretty cool!
escritorio-gustavo: > Maybe we could generate an `impl TS for OtherStruct<OtherDriver>` instead (it's currently a `impl<D: Driver> TS for OtherStruct<D>`)? That'd be pretty cool!

I've started experimenting with this and managed to convert the impl header, I'm clocking out for today, but tomorrow I'll work on `generate_decl_fn`
escritorio-gustavo: This is what the macro is expanding to:
```rs
impl ts_rs::TS for MyStruct<TsDriver> {
    type WithoutGenerics = MyStruct<TsDriver>;
    fn ident() -> String {
        "MyStruct".to_owned()
    }
    fn name() -> String {
        "MyStruct".to_owned()
    }
    fn decl_concrete() -> String {
        format!("type {} = {};", "MyStruct", Self::inline())
    }
    fn decl() -> String {
        let inline = <MyStruct<TsDriver> as ts_rs::TS>::inline();
        let generics = "";
        format!("type {}{generics} = {inline};", "MyStruct")
    }
    fn inline() -> String {
        format!(
            "{{ {} }}",
            <[String]>::join(
                &[format!(
                    "{}{}{}: {},",
                    "",
                    "u",
                    "",
                    <TsDriver::Info as ts_rs::TS>::name() // Ambiguous associated type
                )],
                " "
            )
        )
        .replace(" } & { ", " ")
    }
    fn inline_flattened() -> String {
        format!(
            "{{ {} }}",
            <[String]>::join(
                &[format!(
                    "{}{}{}: {},",
                    "",
                    "u",
                    "",
                    <TsDriver::Info as ts_rs::TS>::name() // Ambiguous associated type
                )],
                " "
            )
        )
    }
    #[allow(clippy::unused_unit)]
    fn generics() -> impl ts_rs::typelist::TypeList
    where
        Self: 'static,
    {
        use ts_rs::typelist::TypeList;
        ()
    }
    fn output_path() -> Option<std::path::PathBuf> {
        let path = std::env::var("TS_RS_EXPORT_DIR");
        let path = path.as_deref().unwrap_or("./bindings");
        Some(std::path::Path::new(path).join("concrete_generic/MyStruct.ts"))
    }
    #[allow(clippy::unused_unit)]
    fn dependency_types() -> impl ts_rs::typelist::TypeList
    where
        Self: 'static,
    {
        {
            use ts_rs::typelist::TypeList;
            ().push::<TsDriver::Info>() // Ambiguous associated type
                .extend(<TsDriver::Info as ts_rs::TS>::generics()) // Ambiguous associated type
        }
    }
}
#[cfg(test)]
#[test]
fn export_bindings_mystruct() {
    <MyStruct<TsDriver> as ts_rs::TS>::export().expect("could not export type");
}
```
escritorio-gustavo: I think we could store the bounds in `DerivedTS::concrete` and use them to allow this syntax
NyxCode: Ah, that's very annoying!  
Rust is refusing `TsDriver::Info`, since `Info` is not on `TsDriver` itself, but on the `Driver` trait.  
What *would* work is `<<DriverA as Driver>::Info as ts_rs::TS>`, or putting it inside a function to help the type resolution:
```rs
fn get_info_name<T: Driver>() -> String {
    <T::Info as TS>::name()
}
```

Seems like a tough problem. If you'd like, i'd be happy to take a look if you push it to a new branch. Getting the feeling that it might be difficult without doing too much special-casing though.
escritorio-gustavo: > Seems like a tough problem. If you'd like, i'd be happy to take a look if you push it to a new branch. Getting the feeling that it might be difficult without doing too much special-casing though.

Agreed. What could make this even more complicated is the fact that we'd have to handle both
```rs
struct MyStruct<T: Driver>
```
and
```rs
struct MyStruct<T> where T: Driver
```
NyxCode: It might even be impossible - what if there are two trait bounds?
```rs
struct MyStruct<D: Driver + Context> {
    info: D::Info,
    ctx: D::Ctx,
}
```
(`Info` is from `Driver`, `Ctx` is from `Context`)

Then, we'd have to figure out where the associated type came from to use that syntax, which we cant.
NyxCode: There, only the 2nd approach using a helper function seems to work: 
```rs
fn helper<T: Driver + Context>() {
    let x = <T::Ctx as TS>::name(); // works
    let y = <T::Info as TS>::name(); // works
}
```
escritorio-gustavo: Yeah, I think we should just keep the `<T: TS>` requirement, it's not that big a deal
NyxCode: After playing with this for a bit, i'm afraid I agree with you.  

Getting it to generate `<<TsDriver as Driver>::Info as TS>` was pretty hacky ^[1], but it immediately breaks if there are multiple trait bounds. There, we *could* just take the first one and hope for the best, but that's not great.

The alternative - using this "helper" function - would be super intrusive and complicated. We'd have to do that everywhere throughout the codebase, and even then, it's complicated.  
Whenever we want to call `<T as TS>::name()`, we'd have to first generate a helper function with the right trait bounds, e.g `fn helper<T: Driver>`. And even that doesn't buy us much, since to be actually usefull, it'd need to be `fn helper<T: Driver> where T::Info: TS`. So yeah, this get's out of hand really fast.
NyxCode: > I think we should use the type passed into #[ts(concrete)], since the idea is to pretend there is no generic at all

I'm still a bit unsure about this, though I find your argument convincing.  
However, here's the other argument i'm struggeling with:
- `decl_concrete` is supposed to give you the declaration of the actual type you have. If you've got `SomeType<i32, String>`, it'll give you `{ first: number, second: string }`.
- I don't expect anyone to call `decl_concrete` manually, it's more of an implementation detail. 
  Right now, `decl_concrete` only calls `Self::inline`, nothing more. Then, in `decl`, we're swapping out generic parameters, and *then* call `decl_concrete` on that new type. That's a pretty clean separation, and swapping some parameters in `decl_concrete` (the "concrete" parameters) and others (the non-"concrete" parameters) in `decl` feels more complicated than it needs to be,
NyxCode: Next problem:
Omitting concrete generics from the trait bounds *did* "solve" the scenario with associated types. However, it was just a workaround. For example, this here now fails to compile:
```rs
#[derive(TS)]
#[ts(concrete(A = i32))]
struct Generic<A> {
    a: A
}
```
That's because in `inline`, we somewhere call `<A as TS>::name()`, but our `impl` no longer contains the `A: TS` bound.  

Maybe we could track this information throughout the proc-macro, like we do with dependencies. Whenever we encounter a type on which we call `<T as TS>::name()`, we keep in mind that we have to require that we need a `T: TS` bound.

For associated types, we'd at some point call `<T::Type as TS>::name()`, storing `T::Type` somewhere, and finally generating a `where T::Type: TS` bound. That'd be great, since it'd not only fix the issue above, but also not require this anymore:
```rs
trait Driver {
    type Info: TS; // <-- this ": TS" bound
}
```

In some sense, `impl<D: Driver> TS for SomeType<D> where D::Info: TS` is the next-best thing we can do other than `impl TS for SomeType<TsDriver>`

NyxCode: Update: That seems to work, but results "overflow evaluating the requirement `T<'_>: TS`" for everything in `self_referential.rs`.  
Nothing is ever easy, is it? ._.
NyxCode: For 
```rs
#[derive(TS)]
struct Simple {
    t: &'static Simple
}
```
this naive approach generates
```
impl TS for Simple where &'static Simple: TS { ... }
```
which sends the trait solver into an infinite loop.
escritorio-gustavo: Maybe there is no need to make every type `: TS`. Everything works already except when using the generic with concrete type.
Maybe just check if any field uses the type `T` and if so add `T: TS`
escritorio-gustavo: I'll see if I can come up with something for that
escritorio-gustavo: I got it to work, but the code is kinda disgusting, even getting a clippy warning of shame 😅 
![Uploading image.png…]()

escritorio-gustavo: Idk why I said enum in the commit message. I meant struct but this should work with enums too
escritorio-gustavo: > I got it to work, but the code is kinda disgusting, even getting a clippy warning of shame 😅
> 
> ![image](https://private-user-images.githubusercontent.com/131818645/312184287-6da60e57-e720-420a-8876-14dfbbbbd9d6.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTAyNjU5MzUsIm5iZiI6MTcxMDI2NTYzNSwicGF0aCI6Ii8xMzE4MTg2NDUvMzEyMTg0Mjg3LTZkYTYwZTU3LWU3MjAtNDIwYS04ODc2LTE0ZGZiYmJiZDlkNi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwMzEyJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDMxMlQxNzQ3MTVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0xODE4NTg0NmRkZjc5YjExNTVkZjdjODExMzMzZGM5NWI1MDAwYzFjNzJlMGUxNDc2NWI3Mjc0OTY5NzEzOTU3JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.EyniuRfHJEWTVwICCfeWczcgaDKUAsus3vF427LxjjI)

Looking at this... Why do we need `_generics`? It's not used within the function right?
NyxCode: I think we don't need it since removing the special generics handeling and replacing it with our dummy types.
NyxCode: ```rs
#[derive(TS)]
#[ts(export, export_to = "concrete_generic/simple")]
#[ts(concrete(T = i32))]
struct Simple<T> {
    t: T,
}
```
works, but
```rs
#[derive(TS)]
#[ts(export, export_to = "concrete_generic/simple")]
#[ts(concrete(T = i32))]
struct WithOption<T> {
    opt: Option<T>,
}
```
doesn't, as soon as the `#[ts(concrete)]` is added. Not sure yet why, though.
NyxCode: Yeah - That's a case where we'd need a `Option<T>: TS` or `T: TS` bound
NyxCode: And for
```rs
#[derive(TS)]
#[ts(export, export_to = "concrete_generic/issue_261", concrete(T = TsDriver))]
struct Consumer1<T: Driver> {
    info: T::Info,
}
```
we don't have any bounds, so that still requires that `: TS` bound in the definition of `Driver`:
```rs
trait Driver {
    type Info: TS;
}
```

escritorio-gustavo: > we don't have any bounds (we'd need a `where T::Info: TS` bound), so that still requires that `: TS` bound in the definition of `Driver`:
> 
> ```rust
> trait Driver {
>     type Info: TS;
> }
> ```

Hm It thought that having `Info: TS` in `Driver` was the desired behavior
NyxCode: Maybe we could give the "For every field, add its type to the `where` clause" approach an other try.  
Maybe it works as a starting point, and we're only left trying to **not** add bounds for self-referential fields, e.g `where Option<Self>: TS`.
escritorio-gustavo: The `export_to` on the new tests was missing the trailing `/`
escritorio-gustavo: > Maybe we could give the "For every field, add its type to the `where` clause" approach an other try. Maybe it works as a starting point, and we're only left trying to **not** add bounds for self-referential fields, e.g `where Option<Self>: TS`.

I have reverted the commit where I did the `extra_ts_bounds` stuff to (hopefully) make things easier
escritorio-gustavo: > For
> 
> ```rust
> #[derive(TS)]
> struct Simple {
>     t: &'static Simple
> }
> ```
> 
> this naive approach generates
> 
> ```rust
> impl TS for Simple where &'static Simple: TS { ... }
> ```
> 
> which sends the trait solver into an infinite loop.

I think the problem here is that the bounds should only be generated for generic types and their associated types, not the type of every field
escritorio-gustavo: Maybe the where clause needs to be generated inside `type_def` for this to be possible
NyxCode: I think that's true! Lemme quickly generate the bounds like I did originally, and then we can see which bounds we can omit to prevent that.
escritorio-gustavo: > I think that's true! Lemme quickly generate the bounds like I did originally, and then we can see which bounds we can omit to prevent that.

Maybe filter on types whose path first segment is one of the generic identifiers (e.g.: `T::AssocType`, `T`) or types that contain those (e.g.: `Option<T::AssocType>`)
NyxCode: Alright, now everything works but `self_referential.rs`.  
I add bounds for 
- Every type of every field and
- for normal non-"discrete" type parameters

> Maybe filter on types whose path first segment is one of the generic identifiers (e.g.: T::AssocType, T) or types that contain those (e.g.: Option<T::AssocType>)

That might work! You wanna give that a try, or should I?
escritorio-gustavo: > Maybe filter on types whose path first segment is one of the generic identifiers (e.g.: `T::AssocType`, `T`) or types that contain those (e.g.: `Option<T::AssocType>`)

I've been tinkering with a possible filter function and have come up with this: 
```rs
fn filter_ty(ty: &Type, generic_idents: &[Ident]) -> bool {
    use syn::{PathArguments as P, GenericArgument as G};
    match ty {
        Type::Array(TypeArray { elem, .. }) |
        Type::Paren(TypeParen { elem, .. }) |
        Type::Reference(TypeReference { elem, .. }) |
        Type::Slice(TypeSlice { elem, .. }) => filter_ty(elem, generic_idents),
        Type::Tuple(TypeTuple { elems, .. }) => elems.iter().any(|x| filter_ty(x, generic_idents)),
        Type::Path(TypePath { qself: None, path }) => {
            let first_segment = path
                .segments
                .first()
                .expect("All paths have at least one segment");

            if generic_idents.contains(&first_segment.ident) {
                return true;
            }

            let last_segment = path
                .segments
                .last()
                .expect("All paths have at least one segment");

            return match last_segment.arguments {
                P::None => false,
                P::AngleBracketed(AngleBracketedGenericArguments {
                    ref args, ..
                }) => args
                        .iter()
                        .filter_map(|x| match x {
                            G::Type(ty) => Some(filter_ty(ty, generic_idents)),
                            _ => None,
                        })
                        .any(std::convert::identity),
                P::Parenthesized(_) => todo!(),
            }
        },
        _ => false,
    }
}
```

NyxCode: Amazing, that works! I'll write a few more tests to see if there are edge-cases where I think they are.

Very nice, thanks!
escritorio-gustavo: > Amazing, that works! I'll write a few more tests to see if there are edge-cases where I think they are.
> 
> Very nice, thanks!

There are a few I'm working on!

`Option<T>` will generate `where Option<T>: TS, T: TS`
`(T, U)` will generate `(T, U): TS`

I am fixing these, but I'm still getting some duplicates
![image](https://github.com/Aleph-Alpha/ts-rs/assets/131818645/d7c3ebe7-efe3-4d6e-84d4-22efe0bfcca5)


escritorio-gustavo: Turns out the duplication is from having both our versions still there xD
NyxCode: 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:
- We add a bound for every associated type
- We add a bound for every type parameter that (also) occurs **outside** of associated types

So for 
```rs
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: > I'm still struggeling a bit to understand _why_ this works though.

Yeah, I even failed to failed to realize I had 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:
- If the type is an array, reference, slice or a type in parenthesis `(T)` (not to be confused with a tuple with one element `(T,)` - notice the comma), all of these types are just some wrapper around some type `T`, so we call `fillter_ty` on that inner type
- If the type is a tuple `(T, U, ...)` we must run `filter_ty` on each type of the tuple (this is why I changed `filter_ty` to return `Option<Vec<Type>>`)
- If the type is anything else, other than a type path, we just return `None`
- Now, if the type is a path, this is where we **actually** filter:
  - Check the first segment of the path. If it corresponds to one of the generic identifiers it is guaranteed to either be the generic itself or one of its associated types, so we return the entire type, wrapped in `Some(vec![])` to respect the function signature
  - If the previous check failed, check the last segment of the path to see if it has generics (`PathArguments::AngleBracketed`)
    - If this segment doesn't contain generics, then none of our struct's generics were used in this path, so we return `None`
    - Otherwise we performe `filter_ty` on every generic of the type (like we did in the tuple case)
escritorio-gustavo: > 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

Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..82632b9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,30 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: 'bug: '
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Create the following type '....'
+3. Derive/implement `TS`
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Version**
+What version of the library are you using?
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..cd72dd0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: 'Feature request: '
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..f5eb619
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,14 @@
+## Goal
+
+What is this PR attempting to achieve? Is it a bug fix? Is it related to an issue?
+Closes #
+
+## Changes
+
+How did you go about solving the problem?
+
+## Checklist
+
+- [ ] I have followed the steps listed in the [Contributing guide](https://github.com/Aleph-Alpha/ts-rs/blob/main/CHANGELOG.md).
+- [ ] If necessary, I have added documentation related to the changes made.
+- [ ] I have added or updated the tests related to the changes made.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 636b3b3..091cd7b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,7 @@
 - Export Rust doc comments/attributes on structs/enums as TSDoc strings ([#187](https://github.com/Aleph-Alpha/ts-rs/pull/187))
 - `Result`, `Option`, `HashMap` and `Vec` had their implementations of `TS` changed ([#241](https://github.com/Aleph-Alpha/ts-rs/pull/241))
 - Implement `#[ts(...)]` equivalent for `#[serde(tag = "...")]` being used on a struct with named fields ([#244](https://github.com/Aleph-Alpha/ts-rs/pull/244))
+- Implement `#[ts(concrete(..))]` to specify a concrete type for a generic parameter ([#264](https://github.com/Aleph-Alpha/ts-rs/pull/264))
 
 ### Fixes
 
@@ -39,4 +40,4 @@
 - Improve support for references ([#199](https://github.com/Aleph-Alpha/ts-rs/pull/199))
 - Generic type aliases generate correctly ([#233](https://github.com/Aleph-Alpha/ts-rs/pull/233))
 - Improve compiler errors ([#257](https://github.com/Aleph-Alpha/ts-rs/pull/257))
-- Update dependencies ([#255](https://github.com/Aleph-Alpha/ts-rs/pull/255))
\ No newline at end of file
+- Update dependencies ([#255](https://github.com/Aleph-Alpha/ts-rs/pull/255))
diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs
index dad4fdf..9db5895 100644
--- a/macros/src/attr/enum.rs
+++ b/macros/src/attr/enum.rs
@@ -1,7 +1,9 @@
-use syn::{Attribute, Ident, Result};
+use std::collections::HashMap;
+
+use syn::{Attribute, Ident, Result, Type};
 
 use crate::{
-    attr::{parse_assign_inflection, parse_assign_str, Inflection},
+    attr::{parse_assign_inflection, parse_assign_str, parse_concrete, Inflection},
     utils::{parse_attrs, parse_docs},
 };
 
@@ -13,6 +15,7 @@ pub struct EnumAttr {
     pub export_to: Option<String>,
     pub export: bool,
     pub docs: String,
+    pub concrete: HashMap<Ident, Type>,
     tag: Option<String>,
     untagged: bool,
     content: Option<String>,
@@ -67,6 +70,7 @@ impl EnumAttr {
             export_to,
             export,
             docs,
+            concrete,
         }: EnumAttr,
     ) {
         self.rename = self.rename.take().or(rename);
@@ -78,6 +82,7 @@ impl EnumAttr {
         self.export = self.export || export;
         self.export_to = self.export_to.take().or(export_to);
         self.docs = docs;
+        self.concrete.extend(concrete);
     }
 }
 
@@ -90,7 +95,8 @@ impl_parse! {
         "export" => out.export = true,
         "tag" => out.tag = Some(parse_assign_str(input)?),
         "content" => out.content = Some(parse_assign_str(input)?),
-        "untagged" => out.untagged = true
+        "untagged" => out.untagged = true,
+        "concrete" => out.concrete = parse_concrete(input)?,
     }
 }
 
diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs
index 6f93cd9..b4440a4 100644
--- a/macros/src/attr/field.rs
+++ b/macros/src/attr/field.rs
@@ -1,6 +1,6 @@
-use syn::{Attribute, Ident, Result, Path};
+use syn::{Attribute, Ident, Path, Result};
 
-use super::{parse_assign_str, parse_assign_from_str};
+use super::{parse_assign_from_str, parse_assign_str};
 use crate::utils::{parse_attrs, parse_docs};
 
 #[derive(Default)]
diff --git a/macros/src/attr/mod.rs b/macros/src/attr/mod.rs
index 72196dc..e5b4c49 100644
--- a/macros/src/attr/mod.rs
+++ b/macros/src/attr/mod.rs
@@ -1,4 +1,4 @@
-use std::convert::TryFrom;
+use std::{collections::HashMap, convert::TryFrom};
 
 pub use field::*;
 pub use r#enum::*;
@@ -86,11 +86,42 @@ fn parse_assign_str(input: ParseStream) -> Result<String> {
     }
 }
 
+fn parse_concrete(input: ParseStream) -> Result<HashMap<syn::Ident, syn::Type>> {
+    struct Concrete {
+        ident: syn::Ident,
+        _equal_token: Token![=],
+        ty: syn::Type,
+    }
+
+    impl Parse for Concrete {
+        fn parse(input: ParseStream) -> Result<Self> {
+            Ok(Self {
+                ident: input.parse()?,
+                _equal_token: input.parse()?,
+                ty: input.parse()?,
+            })
+        }
+    }
+
+    let content;
+    syn::parenthesized!(content in input);
+
+    Ok(
+        syn::punctuated::Punctuated::<Concrete, Token![,]>::parse_terminated(&content)?
+            .into_iter()
+            .map(|concrete| (concrete.ident, concrete.ty))
+            .collect(),
+    )
+}
+
 fn parse_assign_inflection(input: ParseStream) -> Result<Inflection> {
     parse_assign_str(input).and_then(Inflection::try_from)
 }
 
-fn parse_assign_from_str<T>(input: ParseStream) -> Result<T> where T: Parse {
+fn parse_assign_from_str<T>(input: ParseStream) -> Result<T>
+where
+    T: Parse,
+{
     input.parse::<Token![=]>()?;
     match Lit::parse(input)? {
         Lit::Str(string) => string.parse(),
diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs
index 0e37e1f..49cc4b2 100644
--- a/macros/src/attr/struct.rs
+++ b/macros/src/attr/struct.rs
@@ -1,9 +1,9 @@
-use std::convert::TryFrom;
+use std::{collections::HashMap, convert::TryFrom};
 
-use syn::{Attribute, Ident, Result};
+use syn::{Attribute, Ident, Result, Type};
 
 use crate::{
-    attr::{parse_assign_str, Inflection, VariantAttr},
+    attr::{parse_assign_str, parse_concrete, Inflection, VariantAttr},
     utils::{parse_attrs, parse_docs},
 };
 
@@ -15,6 +15,7 @@ pub struct StructAttr {
     pub export: bool,
     pub tag: Option<String>,
     pub docs: String,
+    pub concrete: HashMap<Ident, Type>,
 }
 
 #[cfg(feature = "serde-compat")]
@@ -43,6 +44,7 @@ impl StructAttr {
             export_to,
             tag,
             docs,
+            concrete,
         }: StructAttr,
     ) {
         self.rename = self.rename.take().or(rename);
@@ -51,6 +53,7 @@ impl StructAttr {
         self.export = self.export || export;
         self.tag = self.tag.take().or(tag);
         self.docs = docs;
+        self.concrete.extend(concrete);
     }
 }
 
@@ -75,7 +78,8 @@ impl_parse! {
         "rename_all" => out.rename_all = Some(parse_assign_str(input).and_then(Inflection::try_from)?),
         "tag" => out.tag = Some(parse_assign_str(input)?),
         "export" => out.export = true,
-        "export_to" => out.export_to = Some(parse_assign_str(input)?)
+        "export_to" => out.export_to = Some(parse_assign_str(input)?),
+        "concrete" => out.concrete = parse_concrete(input)?,
     }
 }
 
diff --git a/macros/src/deps.rs b/macros/src/deps.rs
index da02a80..1fd2563 100644
--- a/macros/src/deps.rs
+++ b/macros/src/deps.rs
@@ -3,31 +3,37 @@ use quote::{quote, ToTokens};
 use syn::Type;
 
 #[derive(Default)]
-pub struct Dependencies(Vec<TokenStream>);
+pub struct Dependencies {
+    dependencies: Vec<TokenStream>,
+    pub types: Vec<Type>,
+}
 
 impl Dependencies {
     /// Adds all dependencies from the given type
     pub fn append_from(&mut self, ty: &Type) {
-        self.0
+        self.dependencies
             .push(quote![.extend(<#ty as ts_rs::TS>::dependency_types())]);
+        self.types.push(ty.clone());
     }
 
     /// Adds the given type.
     pub fn push(&mut self, ty: &Type) {
-        self.0.push(quote![.push::<#ty>()]);
-        self.0.push(quote![
+        self.dependencies.push(quote![.push::<#ty>()]);
+        self.dependencies.push(quote![
             .extend(<#ty as ts_rs::TS>::generics())
         ]);
+        self.types.push(ty.clone());
     }
 
-    pub fn append(&mut self, other: Dependencies) {
-        self.0.push(quote![.extend(#other)]);
+    pub fn append(&mut self, mut other: Dependencies) {
+        self.dependencies.push(quote![.extend(#other)]);
+        self.types.append(&mut other.types);
     }
 }
 
 impl ToTokens for Dependencies {
     fn to_tokens(&self, tokens: &mut TokenStream) {
-        let lines = &self.0;
+        let lines = &self.dependencies;
         tokens.extend(quote![{
             use ts_rs::typelist::TypeList;
             ()#(#lines)*
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 558d7dc..08376e5 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -1,11 +1,14 @@
 #![macro_use]
 #![deny(unused)]
 
+use std::collections::{HashMap, HashSet};
+
 use proc_macro2::{Ident, TokenStream};
 use quote::{format_ident, quote};
 use syn::{
     parse_quote, spanned::Spanned, ConstParam, GenericParam, Generics, Item, LifetimeParam, Result,
-    TypeParam, WhereClause,
+    Type, TypeArray, TypeParam, TypeParen, TypePath, TypeReference, TypeSlice, TypeTuple,
+    WhereClause,
 };
 
 use crate::{deps::Dependencies, utils::format_generics};
@@ -17,12 +20,12 @@ mod deps;
 mod types;
 
 struct DerivedTS {
-    generics: Generics,
     ts_name: String,
     docs: String,
     inline: TokenStream,
     inline_flattened: Option<TokenStream>,
     dependencies: Dependencies,
+    concrete: HashMap<Ident, Type>,
 
     export: bool,
     export_to: Option<String>,
@@ -56,13 +59,13 @@ impl DerivedTS {
         };
 
         let ident = self.ts_name.clone();
-        let impl_start = generate_impl_block_header(&rust_ty, &generics);
-        let assoc_type = generate_assoc_type(&rust_ty, &generics);
-        let name = self.generate_name_fn();
+        let impl_start = generate_impl_block_header(&rust_ty, &generics, &self.dependencies);
+        let assoc_type = generate_assoc_type(&rust_ty, &generics, &self.concrete);
+        let name = self.generate_name_fn(&generics);
         let inline = self.generate_inline_fn();
-        let decl = self.generate_decl_fn(&rust_ty);
+        let decl = self.generate_decl_fn(&rust_ty, &generics);
         let dependencies = &self.dependencies;
-        let generics_fn = self.generate_generics_fn();
+        let generics_fn = self.generate_generics_fn(&generics);
 
         quote! {
             #impl_start {
@@ -94,11 +97,11 @@ impl DerivedTS {
 
     /// Returns an expression which evaluates to the TypeScript name of the type, including generic
     /// parameters.
-    fn name_with_generics(&self) -> TokenStream {
+    fn name_with_generics(&self, generics: &Generics) -> TokenStream {
         let name = &self.ts_name;
-        let mut generics_ts_names = self
-            .generics
+        let mut generics_ts_names = generics
             .type_params()
+            .filter(|ty| !self.concrete.contains_key(&ty.ident))
             .map(|ty| &ty.ident)
             .map(|generic| quote!(<#generic as ts_rs::TS>::name()))
             .peekable();
@@ -125,8 +128,11 @@ impl DerivedTS {
     /// struct B;
     /// impl ts_rs::TS for B { /* .. */ }
     /// ```
-    fn generate_generic_types(&self) -> TokenStream {
-        let generics = self.generics.type_params().map(|ty| ty.ident.clone());
+    fn generate_generic_types(&self, generics: &Generics) -> TokenStream {
+        let generics = generics
+            .type_params()
+            .filter(|ty| !self.concrete.contains_key(&ty.ident))
+            .map(|ty| ty.ident.clone());
 
         quote! {
             #(
@@ -154,7 +160,12 @@ impl DerivedTS {
             "export_bindings_{}",
             rust_ty.to_string().to_lowercase().replace("r#", "")
         );
-        let generic_params = generics.type_params().map(|_| quote! { ts_rs::Dummy });
+        let generic_params = generics
+            .type_params()
+            .map(|ty| match self.concrete.get(&ty.ident) {
+                None => quote! { ts_rs::Dummy },
+                Some(ty) => quote! { #ty },
+            });
         let ty = quote!(<#rust_ty<#(#generic_params),*> as ts_rs::TS>);
 
         quote! {
@@ -166,10 +177,10 @@ impl DerivedTS {
         }
     }
 
-    fn generate_generics_fn(&self) -> TokenStream {
-        let generics = self
-            .generics
+    fn generate_generics_fn(&self, generics: &Generics) -> TokenStream {
+        let generics = generics
             .type_params()
+            .filter(|ty| !self.concrete.contains_key(&ty.ident))
             .map(|TypeParam { ident, .. }| quote![.push::<#ident>().extend(<#ident as ts_rs::TS>::generics())]);
         quote! {
             #[allow(clippy::unused_unit)]
@@ -183,8 +194,8 @@ impl DerivedTS {
         }
     }
 
-    fn generate_name_fn(&self) -> TokenStream {
-        let name = self.name_with_generics();
+    fn generate_name_fn(&self, generics: &Generics) -> TokenStream {
+        let name = self.name_with_generics(generics);
         quote! {
             fn name() -> String {
                 #name
@@ -226,23 +237,26 @@ impl DerivedTS {
     /// `decl_concrete()` is simple, and simply defers to `inline()`.
     /// For `decl()`, however, we need to change out the generic parameters of the type, replacing
     /// them with the dummy types generated by `generate_generic_types()`.
-    fn generate_decl_fn(&mut self, rust_ty: &Ident) -> TokenStream {
+    fn generate_decl_fn(&mut self, rust_ty: &Ident, generics: &Generics) -> TokenStream {
         let name = &self.ts_name;
-        let generic_types = self.generate_generic_types();
-        let ts_generics = format_generics(&mut self.dependencies, &self.generics);
+        let generic_types = self.generate_generic_types(generics);
+        let ts_generics = format_generics(&mut self.dependencies, generics, &self.concrete);
 
         use GenericParam as G;
         // These are the generic parameters we'll be using.
-        let generic_idents = self.generics.params.iter().filter_map(|p| match p {
+        let generic_idents = generics.params.iter().filter_map(|p| match p {
             G::Lifetime(_) => None,
-            // Since we named our dummy types the same as the generic parameters, we can just keep
-            // the identifier of the generic parameter - its name is shadowed by the dummy struct.
-            //
+            G::Type(TypeParam { ident, .. }) => match self.concrete.get(ident) {
+                // Since we named our dummy types the same as the generic parameters, we can just keep
+                // the identifier of the generic parameter - its name is shadowed by the dummy struct.
+                None => Some(quote!(#ident)),
+                // If the type parameter is concrete, we use the type the user provided using
+                // `#[ts(concrete)]`
+                Some(concrete) => Some(quote!(#concrete)),
+            },
             // We keep const parameters as they are, since there's no sensible default value we can
             // use instead. This might be something to change in the future.
-            G::Type(TypeParam { ident, .. }) | G::Const(ConstParam { ident, .. }) => {
-                Some(quote!(#ident))
-            }
+            G::Const(ConstParam { ident, .. }) => Some(quote!(#ident)),
         });
         quote! {
             fn decl_concrete() -> String {
@@ -258,28 +272,31 @@ impl DerivedTS {
     }
 }
 
-fn generate_assoc_type(rust_ty: &Ident, generics: &Generics) -> TokenStream {
+fn generate_assoc_type(
+    rust_ty: &Ident,
+    generics: &Generics,
+    concrete: &HashMap<Ident, Type>,
+) -> TokenStream {
     use GenericParam as G;
 
-    let generics_params = generics
-        .params
-        .iter()
-        .map(|x| match x {
-            G::Type(_) => quote! { ts_rs::Dummy },
-            G::Const(ConstParam { ident, .. }) => quote! { #ident },
-            G::Lifetime(LifetimeParam { lifetime, .. }) => quote! { #lifetime },
-        })
-        .collect::<Vec<_>>();
-
-    if generics_params.is_empty() {
-        quote! { type WithoutGenerics = #rust_ty; }
-    } else {
-        quote! { type WithoutGenerics = #rust_ty<#(#generics_params),*>; }
-    }
+    let generics_params = generics.params.iter().map(|x| match x {
+        G::Type(ty) => match concrete.get(&ty.ident) {
+            None => quote! { ts_rs::Dummy },
+            Some(ty) => quote! { #ty },
+        },
+        G::Const(ConstParam { ident, .. }) => quote! { #ident },
+        G::Lifetime(LifetimeParam { lifetime, .. }) => quote! { #lifetime },
+    });
+
+    quote! { type WithoutGenerics = #rust_ty<#(#generics_params),*>; }
 }
 
 // generate start of the `impl TS for #ty` block, up to (excluding) the open brace
-fn generate_impl_block_header(ty: &Ident, generics: &Generics) -> TokenStream {
+fn generate_impl_block_header(
+    ty: &Ident,
+    generics: &Generics,
+    dependencies: &Dependencies,
+) -> TokenStream {
     use GenericParam as G;
 
     let bounds = generics.params.iter().map(|param| match param {
@@ -308,24 +325,66 @@ fn generate_impl_block_header(ty: &Ident, generics: &Generics) -> TokenStream {
         G::Lifetime(LifetimeParam { lifetime, .. }) => quote!(#lifetime),
     });
 
-    let where_bound = add_ts_to_where_clause(generics);
+    let where_bound = generate_where_clause(generics, dependencies);
     quote!(impl <#(#bounds),*> ts_rs::TS for #ty <#(#type_args),*> #where_bound)
 }
 
-fn add_ts_to_where_clause(generics: &Generics) -> Option<WhereClause> {
-    let generic_types = generics
-        .type_params()
-        .map(|ty| ty.ident.clone())
-        .collect::<Vec<_>>();
-    if generic_types.is_empty() {
-        return generics.where_clause.clone();
+fn generate_where_clause(generics: &Generics, dependencies: &Dependencies) -> WhereClause {
+    let used_types = {
+        let is_type_param = |id: &Ident| generics.type_params().any(|p| &p.ident == id);
+
+        let mut used_types = HashSet::new();
+        for ty in &dependencies.types {
+            used_type_params(&mut used_types, ty, is_type_param);
+        }
+        used_types.into_iter()
+    };
+
+    let existing = generics.where_clause.iter().flat_map(|w| &w.predicates);
+    parse_quote! {
+        where #(#existing,)* #(#used_types: ts_rs::TS),*
     }
-    match generics.where_clause {
-        None => Some(parse_quote! { where #( #generic_types : ts_rs::TS ),* }),
-        Some(ref w) => {
-            let bounds = w.predicates.iter();
-            Some(parse_quote! { where #(#bounds,)* #( #generic_types : ts_rs::TS ),* })
+}
+
+// Extracts all type parameters which are used within the given type.
+// Associated types of a type parameter are extracted as well.
+// Note: This will not extract `I` from `I::Item`, but just `I::Item`!
+fn used_type_params<'ty, 'out>(
+    out: &'out mut HashSet<&'ty Type>,
+    ty: &'ty Type,
+    is_type_param: impl Fn(&'ty Ident) -> bool + Copy + 'out,
+) {
+    use syn::{
+        AngleBracketedGenericArguments as GenericArgs, GenericArgument as G, PathArguments as P,
+    };
+
+    match ty {
+        Type::Array(TypeArray { elem, .. })
+        | Type::Paren(TypeParen { elem, .. })
+        | Type::Reference(TypeReference { elem, .. })
+        | Type::Slice(TypeSlice { elem, .. }) => used_type_params(out, elem, is_type_param),
+        Type::Tuple(TypeTuple { elems, .. }) => elems
+            .iter()
+            .for_each(|elem| used_type_params(out, elem, is_type_param)),
+        Type::Path(TypePath { qself: None, path }) => {
+            let first = path.segments.first().unwrap();
+            if is_type_param(&first.ident) {
+                // The type is either a generic parameter (e.g `T`), or an associated type of that
+                // generic parameter (e.g `I::Item`). Either way, we return it.
+                out.insert(ty);
+                return;
+            }
+
+            let last = path.segments.last().unwrap();
+            if let P::AngleBracketed(GenericArgs { ref args, .. }) = last.arguments {
+                for generic in args {
+                    if let G::Type(ty) = generic {
+                        used_type_params(out, ty, is_type_param);
+                    }
+                }
+            }
         }
+        _ => (),
     }
 }
 
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index 41556b4..4d87b64 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -1,6 +1,6 @@
 use proc_macro2::TokenStream;
 use quote::{format_ident, quote};
-use syn::{Fields, Generics, ItemEnum, Variant};
+use syn::{Fields, ItemEnum, Variant};
 
 use crate::{
     attr::{EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr},
@@ -17,12 +17,11 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
     };
 
     if s.variants.is_empty() {
-        return Ok(empty_enum(name, enum_attr, s.generics.clone()));
+        return Ok(empty_enum(name, enum_attr));
     }
 
     if s.variants.is_empty() {
         return Ok(DerivedTS {
-            generics: s.generics.clone(),
             ts_name: name,
             docs: enum_attr.docs,
             inline: quote!("never".to_owned()),
@@ -30,6 +29,7 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
             dependencies: Dependencies::default(),
             export: enum_attr.export,
             export_to: enum_attr.export_to,
+            concrete: enum_attr.concrete,
         });
     }
 
@@ -41,12 +41,10 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
             &mut dependencies,
             &enum_attr,
             variant,
-            &s.generics,
         )?;
     }
 
     Ok(DerivedTS {
-        generics: s.generics.clone(),
         inline: quote!([#(#formatted_variants),*].join(" | ")),
         inline_flattened: Some(quote!(
             format!("({})", [#(#formatted_variants),*].join(" | "))
@@ -56,6 +54,7 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
         export: enum_attr.export,
         export_to: enum_attr.export_to,
         ts_name: name,
+        concrete: enum_attr.concrete,
     })
 }
 
@@ -64,7 +63,6 @@ fn format_variant(
     dependencies: &mut Dependencies,
     enum_attr: &EnumAttr,
     variant: &Variant,
-    generics: &Generics,
 ) -> syn::Result<()> {
     let variant_attr = VariantAttr::new(&variant.attrs, enum_attr)?;
 
@@ -84,7 +82,6 @@ fn format_variant(
         // since we are generating the variant as a struct, it doesn't have a name
         &format_ident!("_"),
         &variant.fields,
-        generics,
     )?;
     let variant_dependencies = variant_type.dependencies;
     let inline_type = variant_type.inline;
@@ -116,7 +113,9 @@ fn format_variant(
                     quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
                 } else {
                     let ty = match (type_override, type_as) {
-                        (Some(_), Some(_)) => syn_err_spanned!(variant; "`type` is not compatible with `as`"),
+                        (Some(_), Some(_)) => {
+                            syn_err_spanned!(variant; "`type` is not compatible with `as`")
+                        }
                         (Some(type_override), None) => quote! { #type_override },
                         (None, Some(type_as)) => {
                             quote!(<#type_as as ts_rs::TS>::name())
@@ -165,7 +164,9 @@ fn format_variant(
                         quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
                     } else {
                         let ty = match (type_override, type_as) {
-                            (Some(_), Some(_)) => syn_err_spanned!(variant; "`type` is not compatible with `as`"),
+                            (Some(_), Some(_)) => {
+                                syn_err_spanned!(variant; "`type` is not compatible with `as`")
+                            }
                             (Some(type_override), None) => quote! { #type_override },
                             (None, Some(type_as)) => {
                                 quote!(<#type_as as ts_rs::TS>::name())
@@ -193,10 +194,9 @@ fn format_variant(
 }
 
 // bindings for an empty enum (`never` in TS)
-fn empty_enum(name: impl Into<String>, enum_attr: EnumAttr, generics: Generics) -> DerivedTS {
+fn empty_enum(name: impl Into<String>, enum_attr: EnumAttr) -> DerivedTS {
     let name = name.into();
     DerivedTS {
-        generics,
         inline: quote!("never".to_owned()),
         docs: enum_attr.docs,
         inline_flattened: None,
@@ -204,5 +204,6 @@ fn empty_enum(name: impl Into<String>, enum_attr: EnumAttr, generics: Generics)
         export: enum_attr.export,
         export_to: enum_attr.export_to,
         ts_name: name,
+        concrete: enum_attr.concrete,
     }
 }
diff --git a/macros/src/types/mod.rs b/macros/src/types/mod.rs
index 1bdd276..d135a77 100644
--- a/macros/src/types/mod.rs
+++ b/macros/src/types/mod.rs
@@ -1,4 +1,4 @@
-use syn::{Fields, Generics, Ident, ItemStruct, Result};
+use syn::{Fields, Ident, ItemStruct, Result};
 
 use crate::{attr::StructAttr, utils::to_ts_ident, DerivedTS};
 
@@ -13,26 +13,21 @@ pub(crate) use r#enum::r#enum_def;
 pub(crate) fn struct_def(s: &ItemStruct) -> Result<DerivedTS> {
     let attr = StructAttr::from_attrs(&s.attrs)?;
 
-    type_def(&attr, &s.ident, &s.fields, &s.generics)
+    type_def(&attr, &s.ident, &s.fields)
 }
 
-fn type_def(
-    attr: &StructAttr,
-    ident: &Ident,
-    fields: &Fields,
-    generics: &Generics,
-) -> Result<DerivedTS> {
+fn type_def(attr: &StructAttr, ident: &Ident, fields: &Fields) -> Result<DerivedTS> {
     let name = attr.rename.clone().unwrap_or_else(|| to_ts_ident(ident));
     match fields {
         Fields::Named(named) => match named.named.len() {
-            0 => unit::empty_object(attr, &name, generics.clone()),
-            _ => named::named(attr, &name, named, generics),
+            0 => unit::empty_object(attr, &name),
+            _ => named::named(attr, &name, named),
         },
         Fields::Unnamed(unnamed) => match unnamed.unnamed.len() {
-            0 => unit::empty_array(attr, &name, generics.clone()),
-            1 => newtype::newtype(attr, &name, unnamed, generics),
-            _ => tuple::tuple(attr, &name, unnamed, generics),
+            0 => unit::empty_array(attr, &name),
+            1 => newtype::newtype(attr, &name, unnamed),
+            _ => tuple::tuple(attr, &name, unnamed),
         },
-        Fields::Unit => unit::null(attr, &name, generics.clone()),
+        Fields::Unit => unit::null(attr, &name),
     }
 }
diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs
index 8ba236b..8cba5bb 100644
--- a/macros/src/types/named.rs
+++ b/macros/src/types/named.rs
@@ -1,6 +1,6 @@
 use proc_macro2::TokenStream;
 use quote::{quote, ToTokens};
-use syn::{spanned::Spanned, Field, FieldsNamed, GenericArgument, Generics, PathArguments, Result, Type};
+use syn::{spanned::Spanned, Field, FieldsNamed, GenericArgument, PathArguments, Result, Type};
 
 use crate::{
     attr::{FieldAttr, Inflection, Optional, StructAttr},
@@ -9,15 +9,11 @@ use crate::{
     DerivedTS,
 };
 
-pub(crate) fn named(
-    attr: &StructAttr,
-    name: &str,
-    fields: &FieldsNamed,
-    generics: &Generics,
-) -> Result<DerivedTS> {
+pub(crate) fn named(attr: &StructAttr, name: &str, fields: &FieldsNamed) -> Result<DerivedTS> {
     let mut formatted_fields = Vec::new();
     let mut flattened_fields = Vec::new();
     let mut dependencies = Dependencies::default();
+
     if let Some(tag) = &attr.tag {
         let formatted = format!("{}: \"{}\",", tag, name);
         formatted_fields.push(quote! {
@@ -32,7 +28,6 @@ pub(crate) fn named(
             &mut dependencies,
             field,
             &attr.rename_all,
-            generics,
         )?;
     }
 
@@ -48,7 +43,6 @@ pub(crate) fn named(
     };
 
     Ok(DerivedTS {
-        generics: generics.clone(),
         inline: quote!(#inline.replace(" } & { ", " ")),
         inline_flattened: Some(quote!(format!("{{ {} }}", #fields))),
         docs: attr.docs.clone(),
@@ -56,6 +50,7 @@ pub(crate) fn named(
         export: attr.export,
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
+        concrete: attr.concrete.clone(),
     })
 }
 
@@ -75,7 +70,6 @@ fn format_field(
     dependencies: &mut Dependencies,
     field: &Field,
     rename_all: &Option<Inflection>,
-    _generics: &Generics,
 ) -> Result<()> {
     let FieldAttr {
         type_as,
@@ -121,8 +115,12 @@ fn format_field(
     if flatten {
         match (&type_as, &type_override, &rename, inline) {
             (Some(_), _, _, _) => syn_err_spanned!(field; "`as` is not compatible with `flatten`"),
-            (_, Some(_), _, _) => syn_err_spanned!(field; "`type` is not compatible with `flatten`"),
-            (_, _, Some(_), _) => syn_err_spanned!(field; "`rename` is not compatible with `flatten`"),
+            (_, Some(_), _, _) => {
+                syn_err_spanned!(field; "`type` is not compatible with `flatten`")
+            }
+            (_, _, Some(_), _) => {
+                syn_err_spanned!(field; "`rename` is not compatible with `flatten`")
+            }
             (_, _, _, true) => syn_err_spanned!(field; "`inline` is not compatible with `flatten`"),
             _ => {}
         }
@@ -178,7 +176,9 @@ fn extract_option_argument(ty: &Type) -> Result<&Type> {
                         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(); "`Option` type must have a single generic argument")
+                }
             }
         }
         other => syn_err!(other.span(); "`optional` can only be used on an Option<T> type"),
diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs
index 24ce4d2..df56a20 100644
--- a/macros/src/types/newtype.rs
+++ b/macros/src/types/newtype.rs
@@ -1,5 +1,5 @@
 use quote::{quote, ToTokens};
-use syn::{FieldsUnnamed, Generics, Result, Type};
+use syn::{FieldsUnnamed, Result, Type};
 
 use crate::{
     attr::{FieldAttr, StructAttr},
@@ -7,12 +7,7 @@ use crate::{
     DerivedTS,
 };
 
-pub(crate) fn newtype(
-    attr: &StructAttr,
-    name: &str,
-    fields: &FieldsUnnamed,
-    generics: &Generics,
-) -> Result<DerivedTS> {
+pub(crate) fn newtype(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) -> Result<DerivedTS> {
     if attr.rename_all.is_some() {
         syn_err!("`rename_all` is not applicable to newtype structs");
     }
@@ -33,9 +28,13 @@ pub(crate) fn newtype(
 
     match (&rename_inner, skip, optional.optional, flatten) {
         (Some(_), ..) => syn_err_spanned!(fields; "`rename` is not applicable to newtype fields"),
-        (_, true, ..) => return super::unit::null(attr, name, generics.clone()),
-        (_, _, true, ..) => syn_err_spanned!(fields; "`optional` is not applicable to newtype fields"),
-        (_, _, _, true) => syn_err_spanned!(fields; "`flatten` is not applicable to newtype fields"),
+        (_, true, ..) => return super::unit::null(attr, name),
+        (_, _, true, ..) => {
+            syn_err_spanned!(fields; "`optional` is not applicable to newtype fields")
+        }
+        (_, _, _, true) => {
+            syn_err_spanned!(fields; "`flatten` is not applicable to newtype fields")
+        }
         _ => {}
     };
 
@@ -64,7 +63,6 @@ pub(crate) fn newtype(
     };
 
     Ok(DerivedTS {
-        generics: generics.clone(),
         inline: inline_def,
         inline_flattened: None,
         docs: attr.docs.clone(),
@@ -72,5 +70,6 @@ pub(crate) fn newtype(
         export: attr.export,
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
+        concrete: attr.concrete.clone(),
     })
 }
diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs
index cfe536d..d223279 100644
--- a/macros/src/types/tuple.rs
+++ b/macros/src/types/tuple.rs
@@ -1,6 +1,6 @@
 use proc_macro2::TokenStream;
 use quote::{quote, ToTokens};
-use syn::{Field, FieldsUnnamed, Generics, Result, Type};
+use syn::{Field, FieldsUnnamed, Result, Type};
 
 use crate::{
     attr::{FieldAttr, StructAttr},
@@ -8,12 +8,7 @@ use crate::{
     DerivedTS,
 };
 
-pub(crate) fn tuple(
-    attr: &StructAttr,
-    name: &str,
-    fields: &FieldsUnnamed,
-    generics: &Generics,
-) -> Result<DerivedTS> {
+pub(crate) fn tuple(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) -> Result<DerivedTS> {
     if attr.rename_all.is_some() {
         syn_err!("`rename_all` is not applicable to tuple structs");
     }
@@ -24,11 +19,10 @@ pub(crate) fn tuple(
     let mut formatted_fields = Vec::new();
     let mut dependencies = Dependencies::default();
     for field in &fields.unnamed {
-        format_field(&mut formatted_fields, &mut dependencies, field, generics)?;
+        format_field(&mut formatted_fields, &mut dependencies, field)?;
     }
 
     Ok(DerivedTS {
-        generics: generics.clone(),
         inline: quote! {
             format!(
                 "[{}]",
@@ -41,6 +35,7 @@ pub(crate) fn tuple(
         export: attr.export,
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
+        concrete: attr.concrete.clone(),
     })
 }
 
@@ -48,7 +43,6 @@ fn format_field(
     formatted_fields: &mut Vec<TokenStream>,
     dependencies: &mut Dependencies,
     field: &Field,
-    _generics: &Generics,
 ) -> Result<()> {
     let FieldAttr {
         type_as,
diff --git a/macros/src/types/unit.rs b/macros/src/types/unit.rs
index 5d50a5c..40bacb7 100644
--- a/macros/src/types/unit.rs
+++ b/macros/src/types/unit.rs
@@ -1,13 +1,12 @@
 use quote::quote;
-use syn::{Generics, Result};
+use syn::Result;
 
 use crate::{attr::StructAttr, deps::Dependencies, DerivedTS};
 
-pub(crate) fn empty_object(attr: &StructAttr, name: &str, generics: Generics) -> Result<DerivedTS> {
+pub(crate) fn empty_object(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
     check_attributes(attr)?;
 
     Ok(DerivedTS {
-        generics: generics.clone(),
         inline: quote!("Record<string, never>".to_owned()),
         inline_flattened: None,
         docs: attr.docs.clone(),
@@ -15,14 +14,14 @@ pub(crate) fn empty_object(attr: &StructAttr, name: &str, generics: Generics) ->
         export: attr.export,
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
+        concrete: attr.concrete.clone(),
     })
 }
 
-pub(crate) fn empty_array(attr: &StructAttr, name: &str, generics: Generics) -> Result<DerivedTS> {
+pub(crate) fn empty_array(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
     check_attributes(attr)?;
 
     Ok(DerivedTS {
-        generics: generics.clone(),
         inline: quote!("never[]".to_owned()),
         inline_flattened: None,
         docs: attr.docs.clone(),
@@ -30,14 +29,14 @@ pub(crate) fn empty_array(attr: &StructAttr, name: &str, generics: Generics) ->
         export: attr.export,
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
+        concrete: attr.concrete.clone(),
     })
 }
 
-pub(crate) fn null(attr: &StructAttr, name: &str, generics: Generics) -> Result<DerivedTS> {
+pub(crate) fn null(attr: &StructAttr, name: &str) -> Result<DerivedTS> {
     check_attributes(attr)?;
 
     Ok(DerivedTS {
-        generics,
         inline: quote!("null".to_owned()),
         inline_flattened: None,
         docs: attr.docs.clone(),
@@ -45,6 +44,7 @@ pub(crate) fn null(attr: &StructAttr, name: &str, generics: Generics) -> Result<
         export: attr.export,
         export_to: attr.export_to.clone(),
         ts_name: name.to_owned(),
+        concrete: attr.concrete.clone(),
     })
 }
 
diff --git a/macros/src/utils.rs b/macros/src/utils.rs
index fb60061..19325ce 100644
--- a/macros/src/utils.rs
+++ b/macros/src/utils.rs
@@ -1,9 +1,10 @@
-use std::convert::TryFrom;
+use std::{collections::HashMap, convert::TryFrom};
 
 use proc_macro2::{Ident, TokenStream};
 use quote::quote;
 use syn::{
     spanned::Spanned, Attribute, Error, Expr, ExprLit, GenericParam, Generics, Lit, Meta, Result,
+    Type,
 };
 
 use crate::deps::Dependencies;
@@ -223,20 +224,29 @@ mod warning {
 /// this expands to an expression which evaluates to a `String`.
 ///
 /// If a default type arg is encountered, it will be added to the dependencies.
-pub fn format_generics(deps: &mut Dependencies, generics: &Generics) -> TokenStream {
+pub fn format_generics(
+    deps: &mut Dependencies,
+    generics: &Generics,
+    concrete: &HashMap<Ident, Type>,
+) -> TokenStream {
     let mut expanded_params = generics
         .params
         .iter()
         .filter_map(|param| match param {
-            GenericParam::Type(type_param) => Some({
+            GenericParam::Type(type_param) => {
+                if concrete.contains_key(&type_param.ident) {
+                    return None;
+                }
                 let ty = type_param.ident.to_string();
                 if let Some(default) = &type_param.default {
                     deps.push(default);
-                    quote!(format!("{} = {}", #ty, <#default as ts_rs::TS>::name()))
+                    Some(quote!(
+                        format!("{} = {}", #ty, <#default as ts_rs::TS>::name())
+                    ))
                 } else {
-                    quote!(#ty.to_owned())
+                    Some(quote!(#ty.to_owned()))
                 }
-            }),
+            }
             _ => None,
         })
         .peekable();
diff --git a/ts-rs/src/chrono.rs b/ts-rs/src/chrono.rs
index 2584091..281ec43 100644
--- a/ts-rs/src/chrono.rs
+++ b/ts-rs/src/chrono.rs
@@ -35,9 +35,15 @@ impl<T: TimeZone + 'static> TS for DateTime<T> {
     fn inline() -> String {
         "string".to_owned()
     }
-    fn inline_flattened() -> String { panic!("{} cannot be flattened", Self::name()) }
-    fn decl() -> String { panic!("{} cannot be declared", Self::name()) }
-    fn decl_concrete() -> String { panic!("{} cannot be declared", Self::name()) }
+    fn inline_flattened() -> String {
+        panic!("{} cannot be flattened", Self::name())
+    }
+    fn decl() -> String {
+        panic!("{} cannot be declared", Self::name())
+    }
+    fn decl_concrete() -> String {
+        panic!("{} cannot be declared", Self::name())
+    }
 }
 
 impl<T: TimeZone + 'static> TS for Date<T> {
@@ -51,7 +57,13 @@ impl<T: TimeZone + 'static> TS for Date<T> {
     fn inline() -> String {
         "string".to_owned()
     }
-    fn inline_flattened() -> String { panic!("{} cannot be flattened", Self::name()) }
-    fn decl() -> String { panic!("{} cannot be declared", Self::name()) }
-    fn decl_concrete() -> String { panic!("{} cannot be declared", Self::name()) }
+    fn inline_flattened() -> String {
+        panic!("{} cannot be flattened", Self::name())
+    }
+    fn decl() -> String {
+        panic!("{} cannot be declared", Self::name())
+    }
+    fn decl_concrete() -> String {
+        panic!("{} cannot be declared", Self::name())
+    }
 }
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index 512e078..d21e251 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -150,18 +150,18 @@ pub mod typelist;
 /// ### exporting
 /// Because Rusts procedural macros are evaluated before other compilation steps, TypeScript
 /// bindings __cannot__ be exported during compile time.
-/// 
+///
 /// Bindings can be exported within a test, which ts-rs generates for you by adding `#[ts(export)]`
 /// to a type you wish to export to a file.  
-/// When `cargo test` is run, all types annotated with `#[ts(export)]` and all of their 
+/// When `cargo test` is run, all types annotated with `#[ts(export)]` and all of their
 /// dependencies will be written to `TS_RS_EXPORT_DIR`, or `./bindings` by default.
-/// 
-/// For each individual type, path and filename within the output directory can be changed using 
+///
+/// For each individual type, path and filename within the output directory can be changed using
 /// `#[ts(export_to = "...")]`. By default, the filename will be derived from the name of the type.
-/// 
+///
 /// If, for some reason, you need to do this during runtime or cannot use `#[ts(export)]`, bindings
 /// can be exported manually:
-/// 
+///
 /// | Function              | Includes Dependencies | To                 |
 /// |-----------------------|-----------------------|--------------------|
 /// | [`TS::export`]        | ❌                    | `TS_RS_EXPORT_DIR` |
@@ -202,10 +202,27 @@ pub mod typelist;
 ///   <br/><br/>
 ///
 /// - **`#[ts(rename_all = "..")]`**  
-///   Rename all fields/variants of the type.
+///   Rename all fields/variants of the type.  
 ///   Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
 ///   <br/><br/>
 ///
+/// - **`#[ts(concrete(..)]`**  
+///   Disables one ore more generic type parameters by specifying a concrete type for them.  
+///   The resulting TypeScript definition will not be generic over these parameters and will use the
+///   provided type instead.  
+///   This is especially useful for generic types containing associated types. Since TypeScript does
+///   not have an equivalent construct to associated types, we cannot generate a generic definition
+///   for them. Using `#[ts(concrete(..)]`, we can however generate a non-generic definition.
+///   Example:
+///   ```
+///   # use ts_rs::TS;
+///   ##[derive(TS)]
+///   ##[ts(concrete(I = std::vec::IntoIter<String>))]
+///   struct SearchResult<I: Iterator>(Vec<I::Item>);
+///   // will always generate `type SearchResult = Array<String>`.
+///   ```
+///   <br/><br/>
+///
 /// ### struct attributes
 /// - **`#[ts(tag = "..")]`**  
 ///   Include the structs name (or value of `#[ts(rename = "..")]`) as a field with the given key.
@@ -390,22 +407,22 @@ pub trait TS {
         deps
     }
 
-    /// Manually export this type to the filesystem. 
+    /// Manually export this type to the filesystem.
     /// To export this type together with all of its dependencies, use [`TS::export_all`].
     ///
     /// # Automatic Exporting
-    /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be 
+    /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be
     /// exported automatically whenever `cargo test` is run.  
     /// In that case, there is no need to manually call this function.
     ///
     /// # Target Directory
     /// The target directory to which the type will be exported may be changed by setting the
     /// `TS_RS_EXPORT_DIR` environment variable. By default, `./bindings` will be used.
-    /// 
+    ///
     /// To specify a target directory manually, use [`TS::export_all_to`], which also exports all
     /// dependencies.
     ///
-    /// To alter the filename or path of the type within the target directory, 
+    /// To alter the filename or path of the type within the target directory,
     /// use `#[ts(export_to = "...")]`.
     fn export() -> Result<(), ExportError>
     where
@@ -414,49 +431,49 @@ pub trait TS {
         let path = Self::default_output_path()
             .ok_or_else(std::any::type_name::<Self>)
             .map_err(ExportError::CannotBeExported)?;
-        
+
         export::export_to::<Self, _>(path)
     }
-    
+
     /// Manually export this type to the filesystem, together with all of its dependencies.  
     /// To export only this type, without its dependencies, use [`TS::export`].
     ///
     /// # Automatic Exporting
-    /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be 
+    /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be
     /// exported automatically whenever `cargo test` is run.  
     /// In that case, there is no need to manually call this function.
     ///
     /// # Target Directory
     /// The target directory to which the types will be exported may be changed by setting the
     /// `TS_RS_EXPORT_DIR` environment variable. By default, `./bindings` will be used.
-    /// 
+    ///
     /// To specify a target directory manually, use [`TS::export_all_to`].
-    /// 
-    /// To alter the filenames or paths of the types within the target directory, 
+    ///
+    /// To alter the filenames or paths of the types within the target directory,
     /// use `#[ts(export_to = "...")]`.
     fn export_all() -> Result<(), ExportError>
-        where
-            Self: 'static,
+    where
+        Self: 'static,
     {
         export::export_all_into::<Self>(&*export::default_out_dir())
     }
-    
+
     /// Manually export this type into the given directory, together with all of its dependencies.  
     /// To export only this type, without its dependencies, use [`TS::export`].
-    /// 
+    ///
     /// Unlike [`TS::export_all`], this function disregards `TS_RS_EXPORT_DIR`, using the provided
     /// directory instead.
-    /// 
-    /// To alter the filenames or paths of the types within the target directory, 
+    ///
+    /// To alter the filenames or paths of the types within the target directory,
     /// use `#[ts(export_to = "...")]`.
     ///
     /// # Automatic Exporting
-    /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be 
+    /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be
     /// exported automatically whenever `cargo test` is run.  
     /// In that case, there is no need to manually call this function.
     fn export_all_to(out_dir: impl AsRef<Path>) -> Result<(), ExportError>
-        where
-            Self: 'static,
+    where
+        Self: 'static,
     {
         export::export_all_into::<Self>(out_dir)
     }
@@ -465,7 +482,7 @@ pub trait TS {
     /// This function does not format the output, even if the `format` feature is enabled. TODO
     ///
     /// # Automatic Exporting
-    /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be 
+    /// Types annotated with `#[ts(export)]`, together with all of their dependencies, will be
     /// exported automatically whenever `cargo test` is run.  
     /// In that case, there is no need to manually call this function.
     fn export_to_string() -> Result<String, ExportError>
@@ -477,12 +494,12 @@ pub trait TS {
 
     /// Returns the output path to where `T` should be exported.  
     /// The returned path does _not_ include the base directory from `TS_RS_EXPORT_DIR`.  
-    /// 
+    ///
     /// To get the output path containing `TS_RS_EXPORT_DIR`, use [`TS::default_output_path`].
     ///
     /// When deriving `TS`, the output path can be altered using `#[ts(export_to = "...")]`.  
     /// See the documentation of [`TS`] for more details.
-    /// 
+    ///
     /// The output of this function depends on the environment variable `TS_RS_EXPORT_DIR`, which is
     /// used as base directory. If it is not set, `./bindings` is used as default directory.
     ///
@@ -493,7 +510,7 @@ pub trait TS {
     }
 
     /// Returns the output path to where `T` should be exported.  
-    /// 
+    ///
     /// The output of this function depends on the environment variable `TS_RS_EXPORT_DIR`, which is
     /// used as base directory. If it is not set, `./bindings` is used as default directory.
     ///
diff --git a/ts-rs/src/typelist.rs b/ts-rs/src/typelist.rs
index c54fdf2..123bf52 100644
--- a/ts-rs/src/typelist.rs
+++ b/ts-rs/src/typelist.rs
@@ -1,11 +1,11 @@
-//! A simple zero-sized collection of types. 
+//! A simple zero-sized collection of types.
 
 use std::{any::TypeId, marker::PhantomData};
 
 use crate::TS;
 
 /// A visitor used to iterate over a [`TypeList`].  
-/// 
+///
 /// Example:
 /// ```
 /// # use ts_rs::TS;
@@ -33,10 +33,10 @@ pub trait TypeVisitor: Sized {
 }
 
 /// A list containing types implementing `TS + 'static + ?Sized`.  
-/// 
-/// To construct a [`TypeList`], start with the empty list, which is the unit type `()`, and 
+///
+/// To construct a [`TypeList`], start with the empty list, which is the unit type `()`, and
 /// repeatedly call [`TypeList::push`] or [`TypeList::extend`] on it.  
-/// 
+///
 /// Example:
 /// ```
 /// # use ts_rs::typelist::TypeList;
@@ -49,10 +49,10 @@ pub trait TypeVisitor: Sized {
 ///       .extend(unsigned)
 /// }
 /// ```
-/// 
-/// The only way to get access to the types contained in a [`TypeList`] is to iterate over it by 
+///
+/// The only way to get access to the types contained in a [`TypeList`] is to iterate over it by
 /// creating a visitor implementing [`TypeVisitor`] and calling [`TypeList::for_each`].
-/// 
+///
 /// Under the hood, [`TypeList`] is recursively defined as follows:
 /// - The unit type `()` is the empty [`TypeList`]
 /// - For every `T: TS`, `(PhantomData<T>,)` is a [`TypeList`]


Test patch:
diff --git a/ts-rs/tests/concrete_generic.rs b/ts-rs/tests/concrete_generic.rs
new file mode 100644
index 0000000..5d45877
--- /dev/null
+++ b/ts-rs/tests/concrete_generic.rs
@@ -0,0 +1,91 @@
+#![allow(unused)]
+
+mod issue_261 {
+    use ts_rs::TS;
+
+    trait Driver {
+        type Info;
+    }
+
+    struct TsDriver;
+    impl Driver for TsDriver {
+        type Info = String;
+    }
+
+    #[derive(TS)]
+    #[ts(export, export_to = "concrete_generic/issue_261/")]
+    struct OtherInfo {
+        x: i32,
+    }
+
+    #[derive(TS)]
+    #[ts(export, export_to = "concrete_generic/issue_261/")]
+    struct OtherDriver;
+    impl Driver for OtherDriver {
+        type Info = OtherInfo;
+    }
+
+    #[derive(TS)]
+    #[ts(export, export_to = "concrete_generic/issue_261/", concrete(T = TsDriver))]
+    struct Consumer1<T: Driver> {
+        info: T::Info,
+    }
+
+    #[derive(TS)]
+    #[ts(export, export_to = "concrete_generic/issue_261/", concrete(T = OtherDriver))]
+    struct Consumer2<T: Driver> {
+        info: T::Info,
+        driver: T,
+    }
+
+    #[test]
+    fn concrete_generic_param() {
+        assert_eq!(
+            Consumer1::<TsDriver>::decl(),
+            "type Consumer1 = { info: string, };"
+        );
+        // `decl` must use the concrete generic, no matter what we pass in
+        assert_eq!(
+            Consumer1::<TsDriver>::decl(),
+            Consumer1::<OtherDriver>::decl()
+        );
+
+        assert_eq!(
+            Consumer2::<OtherDriver>::decl_concrete(),
+            "type Consumer2 = { info: OtherInfo, driver: OtherDriver, };"
+        );
+    }
+}
+
+mod simple {
+    use ts_rs::TS;
+
+    #[derive(TS)]
+    #[ts(export, export_to = "concrete_generic/simple/")]
+    #[ts(concrete(T = i32))]
+    struct Simple<T> {
+        t: T,
+    }
+
+    #[derive(TS)]
+    #[ts(export, export_to = "concrete_generic/simple/")]
+    struct Tuple<T> {
+        f: Option<T>,
+    }
+
+    #[derive(TS)]
+    #[ts(export, export_to = "concrete_generic/simple/")]
+    #[ts(concrete(T = i32))]
+    struct WithOption<T> {
+        opt: Option<T>,
+    }
+
+    #[test]
+    fn simple() {
+        assert_eq!(Simple::<String>::decl(), "type Simple = { t: number, };");
+        assert_eq!(
+            WithOption::<String>::decl(),
+            "type WithOption = { opt: number | null, };"
+        );
+    }
+}
diff --git a/ts-rs/tests/self_referential.rs b/ts-rs/tests/self_referential.rs
index c2a0e8a..f800f7d 100644
--- a/ts-rs/tests/self_referential.rs
+++ b/ts-rs/tests/self_referential.rs
@@ -1,5 +1,4 @@
 #![allow(dead_code)]
-
 use std::{collections::HashMap, sync::Arc};
 
 #[cfg(feature = "serde-compat")]
diff --git a/ts-rs/tests/struct_rename.rs b/ts-rs/tests/struct_rename.rs
index af8a93f..ef4ed77 100644
--- a/ts-rs/tests/struct_rename.rs
+++ b/ts-rs/tests/struct_rename.rs
@@ -4,11 +4,7 @@
 use ts_rs::TS;
 
 #[derive(TS)]
-#[ts(
-    export,
-    export_to = "struct_rename/",
-    rename_all = "UPPERCASE"
-)]
+#[ts(export, export_to = "struct_rename/", rename_all = "UPPERCASE")]
 struct RenameAllUpper {
     a: i32,
     b: i32,
@@ -20,11 +16,7 @@ fn rename_all() {
 }
 
 #[derive(TS)]
-#[ts(
-    export,
-    export_to = "struct_rename/",
-    rename_all = "camelCase"
-)]
+#[ts(export, export_to = "struct_rename/", rename_all = "camelCase")]
 struct RenameAllCamel {
     crc32c_hash: i32,
     b: i32,
@@ -40,11 +32,7 @@ fn rename_all_camel_case() {
 }
 
 #[derive(TS)]
-#[ts(
-    export,
-    export_to = "struct_rename/",
-    rename_all = "PascalCase"
-)]
+#[ts(export, export_to = "struct_rename/", rename_all = "PascalCase")]
 struct RenameAllPascal {
     crc32c_hash: i32,
     b: i32,
@@ -59,11 +47,7 @@ fn rename_all_pascal_case() {
 }
 
 #[derive(serde::Serialize, TS)]
-#[ts(
-    export,
-    export_to = "struct_rename/",
-    rename_all = "camelCase"
-)]
+#[ts(export, export_to = "struct_rename/", rename_all = "camelCase")]
 struct RenameSerdeSpecialChar {
     #[serde(rename = "a/b")]
     b: i32,