Воспроизводимый пример здесь

Мне нужно: параметр contentType должен принимать любой объект класса, расширенный из Content (PublicContent, AdminContent, PrivateContent и т. Д.), И я хочу вызвать статический метод из этого типа параметра внутри метода execute.

У меня есть метод со следующей подписью:

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

И иерархия классов следующим образом:

// 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
}

Проблема в том, что когда я вызываю метод execute, передавая PublicContent в качестве параметра contentType, я получаю сообщение об ошибке

Тип 'typeof PublicContent' не может быть назначен типу 'typeof Content'

Вызов метода:

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

У меня вопрос: почему я получаю эту ошибку, поскольку PublicContent расширяет Content?

РЕДАКТИРОВАТЬ: по запросу @Chase, полные типы для Entity и 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;
  }
}
1
Bruno Peres 11 Фев 2021 в 20:41

2 ответа

Лучший ответ

Проблема, с которой вы столкнулись, заключается в том, что конструкторы суперкласса / подкласса не всегда образуют иерархию типов, даже если их экземпляры формируют. Давайте посмотрим на пример:

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

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

Здесь class Bar extends Foo означает, что если у вас есть значение типа Bar, вы можете присвоить его переменной типа Foo:

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

Но если вы попытаетесь назначить Bar конструктор (типа typeof Bar) значению того же типа, что и конструктор Foo (типа {{X3 }}), это не удается:

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

Это потому, что конструктору Bar требуется параметр типа number, когда вы вызываете его подпись конструкции (т. Е. new Bar(2)), а конструктор Foo вообще не принимает параметров (т. Е. , new Foo()). Если вы попытаетесь использовать Bar, как если бы это был конструктор Foo, и вызвать его без параметров, вы получите ошибку времени выполнения:

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

По той же причине ваш конструктор PublicContent не может быть назначен typeof Content. Первый требует параметра сигнатуры конструкции типа PublicContentProps, тогда как последний принимает любой параметр типа, расширяющего ContentProps. Если вы попытаетесь использовать PublicContent, как если бы это был конструктор Content, вы могли бы передать ему параметр какого-либо типа, кроме PublicContentProps, и это могло бы привести к ошибкам.


Так что вернемся назад. На самом деле вас не волнует, может ли объект, который вы передаете как contentType, назначить типу конструктора Content, потому что вы не собираетесь вызывать его подпись конструкции с произвольным ContentProps. Вам действительно важен только тип его статического метода create(). Я был бы склонен написать getContent() как общую функцию, подобную этой:

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

Это должно работать аналогично вашей существующей версии внутри функции, и теперь вы можете вызывать ее без ошибок, потому что PublicContent соответствует методу create для Content, а также является конструктор типа (new (entityProps: PublicContentProps) => PublicContent):

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

Детская площадка ссылка на код

2
jcalz 12 Фев 2021 в 02:32

Попробуйте сделать так, чтобы Content реализовал простой интерфейс, например IContent, и используйте этот интерфейс, чтобы получить параметр typeof on execute

1
André Smaniotto 11 Фев 2021 в 18:09
66159907