forJ
forJ

Reputation: 4617

Type XXX is not assignable to type 'T' when using generic in typescript

I would like to create a static function that can be referenced in the child classes. An example of BaseClass is as below

export abstract class BaseEntity {
    public static of<T>(params: Partial<T>, type: new () => T): T {
        const item = new type();

        Object.assign(type, params);

        return item;
    }
}

then I extended the BaseEntity with User class like below

export class User extends BaseEntity{
    public static of(params: Partial<User>): User {
        return super.of<User>(params, User);
    }
}

It seems like a legit code for me as I am providing User as the generic type and I have explicitly coded that the BaseEntity static function should return that of same type (which throws no error). However, writing this code produces below error

TS2417: Class static side 'typeof User' incorrectly extends base class static side 'typeof BaseEntity'.    The types returned by 'of(...)' are incompatible between these types.Type 'User' is not assignable to type 'T'. 'T' could be instantiated with an arbitrary type which could be unrelated to 'User'.

It seems to be saying that type User cannot be returned as the returned type T from parent class is not compatible with User type. It makes no sense to me whatsoever because I am explicitly providing to the generic type that T should be User. What am I doing wrong and how can I overcome this problem?

Upvotes: 0

Views: 674

Answers (1)

Bart Hofland
Bart Hofland

Reputation: 3905

Hmmm. Are you trying to override static methods here? That's not gonna work, I'm afraid. ;) Static methods are bound to a class' constructor instead of its prototype. You can only call them on the class itself, not on an instance of a class. So your BaseEntity.of() method and User.of() method are actually two completely unrelated functions.

Your BaseEntity.of() method seems to be a factory method for your entities. Perhaps you can consider to use the BaseEntity.of() method and drop the User.of() method. The sample code below only creates only the static of() method in the BaseEntity class and uses it for initializing two User variables. (For demonstration purposes, I also added a name property to the User class.)

Some additional remarks though:

  • It might be useful to swap the parameters of the of() function: first the type parameter (which should always be supplied), and second the params parameter (which could be optional in case you'd want to create a default instance).
  • I noticed that in your BaseEntity.of() method you assign the contents of the params object to type, but I assume you want to assign them to item instead.

BaseEntity.ts:

export abstract class BaseEntity {
  static of<T extends BaseEntity>(type: new () => T, params?: Partial<T>) {
    const item = new type();
    Object.assign(item, params);
    return item;
  }

  //...
}

User.ts:

import { BaseEntity } from 'BaseEntity';

export class User extends BaseEntity {
  name?: string;
}

main.ts:

import { BaseEntity } from 'BaseEntity';
import { User } from 'User';

const user1 = BaseEntity.of(User, { name: 'John Doe' });
const user2 = BaseEntity.of(User);

console.log(user1.name); // John Doe
console.log(user2.name); // undefined

I assume that your BaseEntity class contains additional (base entity related) logic. But if that's not the case, you might want to extract the entity creation logic from the BaseEntity class and put it in a "global" factory function instead. When doing so, you can drop the BaseEntity class completely. As a result, your concrete entity classes will have one less dependency.

createEntity.ts:

export function createEntity<T>(type: new() => T, params?: Partial<T>) {
  const item = new type();
  Object.assign(item, params);
  return item;
}

User.ts:

export class User {
  name?: string;
}

main.ts:

import { createEntity } from 'createEntity';
import { User } from 'User';

const user1 = createEntity(User, { name: 'John Doe' });
const user2 = createEntity(User);

console.log(user1.name); // John Doe
console.log(user2.name); // undefined

Upvotes: 1

Related Questions