简体   繁体   中英

Using typescript generics properly

I have an interface of the following format which describes database methods like so:

export default interface IRepository {
  createAndSave(data: ICreateUserDTO): Promise<User>
  findById<T>({ id }: { id: number }): Promise<T | null> // right here
  ...
}

As you can see from the snippet above, findById method is meant to take in a type and return a resolved promise of type T or a null value. I go-ahead to implement this in a class like so.

class DatabaseOps {
private ormManager: Repository<User>
...
async findById<User>({ id }: { id: number }): Promise<User | null> {
    const t = await this.ormManager.findOne({
      where: { id },
    }) 

    return t
  }
...
}

When I try to create the findById method like that, typescript gives this error of this format

Type 'import("../src/user/infra/typeorm/entities/User").default' is not assignable to type 'User'.
  'User' could be instantiated with an arbitrary type which could be unrelated to 'import("../src/audience/infra/typeorm/entities/User").default'

I tried to use typescript assertion to override this error like so

class DatabaseOps {
private ormManager: Repository<User>
...
async findById<User>({ id }: { id: number }): Promise<User | null> {
    const t = await this.ormManager.findOne({
      where: { id },
    }) 

    return t as Promise<User> // here
  }
...
}

but I still get the error, I am not really sure what to do from this point onward.

Here is what the User model definition looks like, I am making use of TypeORM

export default class User {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column({
    type: 'json',
    nullable: true,
  })
  data: object

  @Column({ type: 'tinyint', default: 1 })
  status: number
  ...
}

What could be the cause of this and how do I rectify it? Any help will be appreciated. Thank you very much!

The IRepository.findById method's type signature doesn't mean what you think it means.

When you write findById<T> , it means that the method promises to work with any type T . Whoever calls the method chooses which type it is. Kind of like this:

const r : IRepository = ...
const x = r.findById<User>( ... )
const y = r.findById<number>( ... )
consy z = r.findById<string>( ... )
... and so on

And since the caller of the method can choose any type, it means that the implementer of the method must implement it such that it can work with any type. So it can't be just User . It has to be any type, whatever the caller happens to choose.


Now, what you probably meant to do was to create not just a repository, but a repository of a certain thing . To do this, the generic parameter should be on the interface, not on the method:

export default interface IRepository<T, DTO> {
  createAndSave(data: DTO): Promise<T>
  findById({ id }: { id: number }): Promise<T | null>
  ...
}

Then you can implement IRepository<User, ICreateUserDTO> in your class:

class UserRepository {
    ...

    async createAndSave(data: ICreateUserDTO): Promise<User> {
        ...
    }

    async findById({ id }: { id: number }): Promise<User | null> {
        const t = await this.ormManager.findOne({
          where: { id },
        }) 

        return t as Promise<User> // here
      }

    ...
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM