Вывод аргументов конструктора класса из экземпляра класса в TypeScript

#typescript

#typescript

Вопрос:

У меня есть одноэлементный класс, который является расширением массива 20×20. (Я выполняю поиск по слову.) Я обнаружил этот восхитительный синтаксис для экспорта экземпляра одноэлементного класса с сохранением как имени экземпляра, так и имени класса:

 const Grid = new class Grid extends Array<string[]> {
  constructor(private readonly size = 20) {
    super(size);
    // ...
  }

  // ...
};

export default Grid;
  

Ура, это отлично работает. Теперь для тестирования. Чтобы сделать вещи управляемыми, я хочу создать новую сетку с гораздо меньшим размером (возможно, 8×8). В JavaScript получить конструктор легко.

 import Grid from './grid';

const GridConstructor = Grid.constructor;

it('does a thing', () => {
  const testGrid = new GridConstructor(8);
});
  

Поскольку я знаю, что ожидает аргумент, да, я могу просто сделать это и объявить GridConstructor как

 const GridConstructor: new(size: number) => typeof Grid = Grid.constructor;
  

Это, конечно, разумное решение. Однако я не разумный человек и должен вводить ВСЕ, используя механизм набора текста TypeScript. (Хорошо, так что мне действительно просто нравится учиться. Кто знает, когда это может быть полезно?)
TypeScript считает, что Grid.constructor это тип Function . Насколько я могу судить, у него нет никакой информации о том, какова форма этой функции.

Можно ли вывести эту форму?

Лучшее, что я придумал, это:

 type Constructor<T extends { constructor: new(...args: any[]) => any; }> = 
  new(...args: ConstructorParameters<T['constructor']>) => InstanceType<T['constructor']>;

const GridConstructor: Constructor<typeof Grid> = Grid.constructor;
  

Это приводит к ошибке:

Type 'Grid' does not satisfy the constraint '{ constructor: new (...args: any[]) => any; }'.

Игровая площадка TypeScript

Ответ №1:

В настоящее время это ограничение в TypeScript. Экземпляры класса не имеют строго типизированных constructor свойств; как вы отметили, компилятор видит только его тип как Function . Существует довольно давняя открытая проблема, microsoft / TypeScript #3841, требующая изменения. Но «очевидное» исправление, когда компилятор создает экземпляры класса с именем Foo , имеющим constructor свойство type typeof Foo , приведет к сбою. Это объясняется в этом комментарии архитектором языка:

Общая проблема, с которой мы сталкиваемся здесь, заключается в том, что множество допустимых JavaScript-кодов имеют производные конструкторы, которые не являются надлежащими подтипами их базовых конструкторов. Другими словами, производные конструкторы «нарушают» принцип подстановки на статической стороне класса.

Например:

 class Foo {
  x: string;
  constructor(x: string) {
    this.x = x.toUpperCase();
  }
}
  

Давайте представим, что каждое значение foo where foo instanceof Foo имеет constructor свойство type typeof Foo . Тогда компилятор позволит вам написать следующую функцию без утверждения типа:

 function cloneFoo(foo: Foo): Foo {
  const ctor = foo.constructor as typeof Foo; // need assertion here in current TS
  return new ctor(foo.x); 
}
  

Тогда, пока вы просто имеете дело с Foo самим собой, проблем не будет:

 cloneFoo(new Foo("x")); // okay
  

Но в JavaScript есть class иерархии, в которых конструкторам подклассов может потребоваться другой набор параметров, чем для их базовых классов, и TypeScript также поддерживает это:

 class Bar extends Foo {
  y: string;
  constructor(x: string, y: string) {
    super(x);
    this.y = y.toUpperCase();
  }
}
  

Здесь Bar конструктору ‘s требуется второй параметр, а Foo ‘s — нет. Но каждый экземпляр Bar также является экземпляром Foo , в соответствии как с принципом субтитрируемости, так и с JS-средой выполнения (например, new Bar("x", "y") instanceof Foo вычисляется true как ).

Но это приводит непосредственно к проблеме. Если каждый Bar экземпляр также является Foo экземпляром, то Bar свойство каждого constructor экземпляра должно быть допустимым typeof Foo . Но это не так:

 cloneFoo(new Bar("x", "y")); // error at runtime! y is undefined
  

Мы получили строго типизированный конструктор, но получили ошибки во время выполнения в exchange. Чтобы предотвратить эти ошибки, вам нужно либо отказаться от возможности назначения экземпляров подкласса экземплярам суперкласса, что делает extends неправильное название… или вам пришлось бы ограничить конструкторы подклассов, чтобы вы могли требовать те же параметры, что и у суперкласса, что было бы «массовым критическим изменением», поскольку множество существующих class иерархий JavaScript нарушают это правило.

Таким образом, хотя текущая типизация constructor as just Function бесполезна, она, по крайней мере, не разрушает ни иерархии классов, ни систему типов, что приятно. Возможно, может быть что-то лучше, чем Function , но не все, что вы можете безопасно вызывать new .

Таким образом, проблема оставалась нетронутой в течение многих лет.


Обратите внимание, что в вашем случае подклассы, очевидно, невозможны, поскольку у вас есть private свойство, которое затрудняет расширение. Поэтому, возможно, можно было бы открыть новую проблему (или прокомментировать существующую) с просьбой constructor ввести строгую типизацию для любого класса со private свойством. В некотором смысле это будет создание истинных final классов в TypeScript… но это уже было предложено и отклонено в microsoft / TypeScript # 8306.

В любом случае, на данный момент так оно и есть.


Если вы знаете, что безопасно использовать строго типизированный constructor , то утверждение типа является разумным решением; но это равносильно тому, что вы уже делаете:

 const GridConstructor = Grid.constructor as new (size?: number) => typeof Grid;
  

Другим обходным путем здесь является ручное объявление строго типизированного constructor свойства:

 const Grid = new class _Grid extends Array<string[]> {
  ["constructor"]: typeof _Grid
  constructor(private readonly size = 20) {
    super(size);
  }
}
  

(Кавычки и скобки вокруг constructor необходимы, чтобы избежать ошибок… и переименование из Grid в _Grid не является необходимым, но помогает избежать путаницы в отношении того, о чем мы говорим).

Конечно, это вряд ли лучше, чем утверждение типа; вы заставляете себя самостоятельно выписывать тип конструктора.


Итак, я бы сказал, что ответ здесь просто отрицательный; компилятор намеренно не позволяет вам выводить тип параметров конструктора из типа экземпляра, и неясно, когда и изменится ли это когда-нибудь.

Игровая площадка ссылка на код

Ответ №2:

используйте этот способ

 class Foo {
  x: string;
  constructor(x: string) {
    this.x = x.toUpperCase();
  }
}

const foo = new Foo('hello');

const foo2 = new (foo.constructor as any)("world")