最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Getting error: Type 'typeof B' is not assignable to type 'typeof A' for class B tha

programmeradmin5浏览0评论

Reproducible example here

My need is: the contentType parameter should accept any class object extended from Content (PublicContent, AdminContent, PrivateContent, etc) and I want to call a static method from this parameter type inside the execute method.

I have a method with the following signature:

async execute<U extends ContentProps>(input: {
    contentType: typeof Content;
    contentPropsType: typeof ContentProps; 
}): Promise<Result<U, Failure>>;

and a class hierarchy as follows:

// content.entity.ts

export class ContentProps extends EntityProps {}

export class Content<T extends ContentProps> extends Entity<T> {
  public constructor(props: T) {
    super(props);
  }
}

// public-content.entity.ts
export class PublicContentProps extends ContentProps {
  readonly title: string;
  readonly text: string;
}

export class PublicContent extends Content<PublicContentProps> {
  constructor(props: PublicContentProps) {
    super(props);
  }
  // ommited
}

The issue is that when I call the execute method passing PublicContent as the contentType parameter I'm getting an error saying

Type 'typeof PublicContent' is not assignable to type 'typeof Content'

The method call is:

const result = await this.getContent.execute({
  contentType: PublicContent,
  contentPropsType: PublicContentProps,
});

My question is: Why I'm getting this error since PublicContent is extending Content?

EDIT: as requested by @Chase, the full types for Entity and EntityProps:

// entity.ts
export abstract class EntityProps extends BaseEntityProps {
  id?: string;
  createdAt?: Date;
  updatedAt?: Date;
}

export abstract class Entity<T extends EntityProps> extends BaseEntity<T> {
  get id(): string {
    return this.props.id;
  }

  get createdAt(): Date {
    return this.props.createdAt;
  }

  get updatedAt(): Date {
    return this.props.updatedAt;
  }

  protected constructor(entityProps: T) {
    super(entityProps);
  }
}


// base.entity.ts
export abstract class BaseEntityProps {}

export abstract class BaseEntity<T extends BaseEntityProps> extends Equatable {
  protected readonly props: T;

  protected constructor(baseEntityProps: T) {
    super();
    this.props = baseEntityProps;
  }

  static create<T = BaseEntity<BaseEntityProps>, U = BaseEntityProps>(
    this: {
      new (entityProps: U): T;
    },
    propsType: { new (): U },
    props: U,
  ): Result<T, ValidationFailure> {
    const violations = validateSchemaSync(propsType, props);

    return violations?.length
      ? Result.fail(new ValidationFailure(violations))
      : Result.ok(new this({ ...props }));
  }

  toJSON(): T {
    return this.props;
  }
}

Reproducible example here

My need is: the contentType parameter should accept any class object extended from Content (PublicContent, AdminContent, PrivateContent, etc) and I want to call a static method from this parameter type inside the execute method.

I have a method with the following signature:

async execute<U extends ContentProps>(input: {
    contentType: typeof Content;
    contentPropsType: typeof ContentProps; 
}): Promise<Result<U, Failure>>;

and a class hierarchy as follows:

// content.entity.ts

export class ContentProps extends EntityProps {}

export class Content<T extends ContentProps> extends Entity<T> {
  public constructor(props: T) {
    super(props);
  }
}

// public-content.entity.ts
export class PublicContentProps extends ContentProps {
  readonly title: string;
  readonly text: string;
}

export class PublicContent extends Content<PublicContentProps> {
  constructor(props: PublicContentProps) {
    super(props);
  }
  // ommited
}

The issue is that when I call the execute method passing PublicContent as the contentType parameter I'm getting an error saying

Type 'typeof PublicContent' is not assignable to type 'typeof Content'

The method call is:

const result = await this.getContent.execute({
  contentType: PublicContent,
  contentPropsType: PublicContentProps,
});

My question is: Why I'm getting this error since PublicContent is extending Content?

EDIT: as requested by @Chase, the full types for Entity and EntityProps:

// entity.ts
export abstract class EntityProps extends BaseEntityProps {
  id?: string;
  createdAt?: Date;
  updatedAt?: Date;
}

export abstract class Entity<T extends EntityProps> extends BaseEntity<T> {
  get id(): string {
    return this.props.id;
  }

  get createdAt(): Date {
    return this.props.createdAt;
  }

  get updatedAt(): Date {
    return this.props.updatedAt;
  }

  protected constructor(entityProps: T) {
    super(entityProps);
  }
}


// base.entity.ts
export abstract class BaseEntityProps {}

export abstract class BaseEntity<T extends BaseEntityProps> extends Equatable {
  protected readonly props: T;

  protected constructor(baseEntityProps: T) {
    super();
    this.props = baseEntityProps;
  }

  static create<T = BaseEntity<BaseEntityProps>, U = BaseEntityProps>(
    this: {
      new (entityProps: U): T;
    },
    propsType: { new (): U },
    props: U,
  ): Result<T, ValidationFailure> {
    const violations = validateSchemaSync(propsType, props);

    return violations?.length
      ? Result.fail(new ValidationFailure(violations))
      : Result.ok(new this({ ...props }));
  }

  toJSON(): T {
    return this.props;
  }
}
Share Improve this question edited Feb 11, 2021 at 22:20 Bruno Peres asked Feb 11, 2021 at 17:41 Bruno PeresBruno Peres 16.4k6 gold badges56 silver badges92 bronze badges 6
  • 1 Why the typeof? typeof Content gives you the type of class object (static side), wheras Content is the instance side type. So it shouldn't be patible. If you want to refer to the instance side (an object instantiated from the class) - remove the typeofs – Chase Commented Feb 11, 2021 at 17:58
  • @Chase this is because I'm using a static method from *Content inside the execute method. Basically, my need is: the contentType parameter should accept any class object extended from Content (PublicContent, AdminContent, PrivateContent, etc) – Bruno Peres Commented Feb 11, 2021 at 18:01
  • 1 Ahh, I see. Can you post the full types for EntityProps and Entity? as well as the full error - usually the error will plain about a specific inpatibility - in this case, it should tell you something along the lines of "constructor signature inpatible" – Chase Commented Feb 11, 2021 at 18:04
  • 1 Please consider modifying your example code so as to constitute a minimal reproducible example suitable for dropping into a standalone IDE like The TypeScript Playground which demonstrates the issue you're facing and only the issue you're facing. Ideally that would mean that you'd remove anything that doesn't relate to the problem. This will allow people who want to help you to get to work on the solution to the problem without first having to put effort into producing the problem in the first place... and it increases the chance that answers will be applicable. – jcalz Commented Feb 11, 2021 at 20:08
  • 1 Hi @jcalz thanks for your ment. Here a reproducible example. Thanks for your help! – Bruno Peres Commented Feb 11, 2021 at 22:10
 |  Show 1 more ment

2 Answers 2

Reset to default 8

The problem you're running into is that superclass/subclass constructors do not always form a type hierarchy even if their instances do. Let's look at an example:

class Foo {
  x = 1;
  constructor() { }
  static z = 3;
}

class Bar extends Foo {
  y: string;
  constructor(y: number) {
    super()
    this.y = y.toFixed(1);
  }
}

Here, class Bar extends Foo means if you have a value of type Bar, you can assign it to a variable of type Foo:

const bar: Bar = new Bar(2);
const foo: Foo = bar; // okay

But if you try to assign the Bar constructor (of type typeof Bar) to a value of the same type as the Foo constructor (of type typeof Foo), it fails:

const fooCtor: typeof Foo = Bar; // error!
// Type 'new (y: number) => Bar' is not assignable to type 'new () => Foo'

That's because the Bar constructor requires a parameter of type number when you call its construct signature (i.e., new Bar(2)), while the Foo constructor takes no parameters at all (i.e., new Foo()). If you try to use Bar as if it were the Foo constructor, and call it with no parameters, you'll get a runtime error:

const oopsAtRuntime = new fooCtor(); // TypeError: y is undefined

For the same reason, your PublicContent constructor is not assignable to typeof Content. The former requires a construct signature parameter of type PublicContentProps, while the latter will accept any parameter of type that extends ContentProps. If you try to use PublicContent as if it were the Content constructor, you might pass it a parameter of some type other than PublicContentProps, and that could lead to errors.


So let's step back. In fact you don't care if the object you pass as contentType is assignable to the Content constructor type because you're not going to call its construct signature with an arbitrary ContentProps. You really only care about the type of its static create() method. I'd be inclined to write getContent() as a generic function like this:

const getContent = <U extends ContentProps, T extends BaseEntity<U>>(input: {
  contentType: Pick<typeof Content, "create"> & (new (entityProps: U) => T);
  contentPropsType: new () => U;
}): U => { /* impl */ };

That should work similarly to your existing version on the inside of the function, and now you can call it without error, because PublicContent matches the create method of Content, as well as being a constructor of type (new (entityProps: PublicContentProps) => PublicContent):

const entity = getContent({
  contentType: PublicContent,
  contentPropsType: PublicContentProps,
}); // okay

Playground link to code

Try to make the Content implements a simple interface, like IContent, and use this interface to get the typeof on execute parameter

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论