Reputation: 16365
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;
}
}
Upvotes: 4
Views: 2685
Reputation: 328292
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
Upvotes: 8
Reputation: 53
Try to make the Content
implements a simple interface, like IContent
, and use this interface to get the typeof
on execute
parameter
Upvotes: 1