#Generic type constraint not working on class type parameter

1 messages · Page 1 of 1 (latest)

wispy heart
#

I am setting up an abstraction for a database repository pattern. My ProductRepository class takes a single generic type parameter, which is constrained using the Product interface to ensure that the type contains an id: number field. I then define functions and arguments of my database accessor based on a generic type parameter passed into my DB type.

However, the constraint does not seem to be taken into account when I attempt to call the findFirst function of my DB type, and I get the error:

Type '{ id: number; }' is not assignable to type 'Partial<T>'

Despite the fact that my generic T should be constrained to the Product interface, which does contain an id, and so I would expect that the object { id } matches the type Partial<T extends Product>. Typescript gives me no further information about why the types do not match.

Interestingly, if I instead pass Product directly as the type parameter (instead of T), this error goes away (but I get other errors). For context, there is no Product table in my database, but this is instead an abstraction for functionality that many tables will share.

The minimal code block is in a comment following this one (as well as a link to a replication in typescript playground).

#
interface Product {
  id: number;
  title: string;
  productId: number;
}

interface FindArgs<T> {
  where: Partial<T>;
}

interface FindManyArgs<T> extends FindArgs<T> {
  orderBy: { id: string };
}

type DB<T> = {
  findFirst: (args: FindArgs<T>) => Promise<T | null>;
  findMany: (args: FindManyArgs<T>) => Promise<T[] | null>;
};

export class ProductRepository<T extends Product> {
  db: DB<T>;

  constructor(db: DB<T>) {
    this.db = db;
  }

  async getProduct(id: number) {
    const product = await this.db.findFirst({ where: { id } });

    if (!product) {
      return null;
    }

    return this.supplementProduct(product);
  }

  private supplementProduct(product:T ) {
    // { ... fetchQuery and shopify API logic }
    return product;
  }
}
#
modern shoreBOT
#

@wispy heart Here's a shortened URL of your playground link! You can remove the full link from your message.

eamc_#0

Preview:```ts
interface Product {
id: number
title: string
productId: number
}

interface FindArgs<T> {
where: Partial<T>
}

interface FindManyArgs<T> extends FindArgs<T> {
orderBy: {id: string}
}

type DB<T> = {
findFirst: (args: FindArgs<T>) => Promise<T | null>
fin
...```

visual magnet
#

fyi, use single backticks for inline code rather than codeblocks
it looks like this rather than this.

#

i think this is a case of the "{ id: number } is assignable to the constraint of T, but T could be instantiated with a different subtype", idk why it's not that message. maybe it's just through too many layers

#

anyways suppose you have some subtype of Product that has a specific shape of id, or maybe it's limited, just anything that would make id a subtype of number rather than any possible number

suppose a Product with id: 0 | 1 | 2 | 3
that would be a valid type for T, since it would be a subtype of Product
but then using where { id: number } wouldn't match what the id is, since only 4 ids actually exist

maybe a Product like that won't make sense in your system, but it's a possibility with how you've set it up, so ts is warning about that

#

that's what this error is (seemingly) about

wispy heart
#

Oh! Right on, thanks, that makes sense.

visual magnet
#

changing the id parameter to T['id'] doesn't remove the error, though. maybe that's not quite what the issue is

wispy heart
#

Yeah, I'm pretty sure I have used generics similarly to this before and hadn't run into this problem. I'm wondering if it might be getting confused because there are a few layers that the generic is passed

#

I also tried manually adding the Product Constraint to each generic and it still didn't seem to work

visual magnet
#

removing the layers and just putting the types in the class directly doesn't seem to change it

wispy heart
#

Yeah, but, if I pass Product directly to the type parameter of DB it does work

#

Which leads me to believe that somehow, in the context I am calling findFirst, it is not treating T as a Product

visual magnet
#

also suspected contravariance but that doesn't seem like it

wispy heart
#

I'm also open to more general refactors if there might be a better way to do this without solving this problem directly, but I'm using prisma and trying to avoid duplicating logic on each table and prisma does not seem to play nicely with generics and abstraction so I needed to set up my own types for the functions

modern shoreBOT
#
that_guy977#0

Preview:```ts
interface Product {
id: number
}

export class ProductRepository<T extends Product> {
findFirst(args: Partial<T>) {}

async getProduct(id: T["id"]) {
const prod: Product = {id: 0}
const t: T = prod // expected error
const pt: Partial<
...```

visual magnet
#

good luck if you try though