У меня десятки форм с разными полями — флажками, загрузками, вводами.
Допустим, мне нужен класс для обработки формы — проблема в том, что одному классу нужен inputForm, uploadForm. Для другой формы требуется только checkboxForm. Как справиться с этим методом ООП? Я придумал параметр настройки, проблема в том, что автозаполнение работает, поэтому я могу получить доступ к неинициализированным полям. В идеале я не хочу, чтобы они были видны (при автозаполнении в качестве клиента класса). Я знаю шаблон декоратора, но, похоже, у меня получится беспорядочное решение с парой оберток.
class InputForm {
// Define InputForm class here
}
class UploadForm {
// Define UploadForm class here
}
class CheckboxForm {
// Define CheckboxForm class here
}
interface Config {
includeInputForm: boolean;
includeUploadForm: boolean;
includeCheckboxForm: boolean;
}
class MultiForm {
inputForm?: InputForm;
uploadForm?: UploadForm;
checkboxForm?: CheckboxForm;
constructor(private config: Config) {
if (this.config.includeInputForm) {
this.inputForm = new InputForm();
}
if (this.config.includeUploadForm) {
this.uploadForm = new UploadForm();
}
if (this.config.includeCheckboxForm) {
this.checkboxForm = new CheckboxForm();
}
}
}
const config: Config = {
includeInputForm: true,
includeUploadForm: false,
includeCheckboxForm: true
};
const multiForm = new MultiForm(config);
@jcalz Да, это прекрасно решает вопрос. Хотя выглядит очень сложно :)
Вы можете рассмотреть совершенно другой подход к этому; почему бы просто не передать интересующие вас свойства напрямую, например это? Я имею в виду, возможно, это не тот подход, который вам нужен, но, по крайней мере, он более компонуемый. Хотя я напишу ответ.






В TypeScript объявление класса должно иметь статически известные члены (экземпляры классов реализуются как интерфейсы, а интерфейсы должны иметь статически известные члены). Поэтому вы не можете иметь такой код, как
class MultiForm {
⋮
}
const mf1 = new MultiForm(config1);
const mf2 = new MultiForm(config2);
где mf1 и mf2 имеют разные известные ключи. Либо оба имеют свойство inputForm, либо ни один из них не имеет.
Итак, простой ответ на заданный вопрос: «Нет, вы не можете этого сделать».
Однако вы можете описать тип конструктора класса, который ведет себя таким образом, то есть вы можете написать обычное объявление класса, а затем утверждать, что переменная, содержащая ссылку на этот класс, имеет желаемый тип. Так это будет выглядеть
class _MultiForm {
⋮
}
type MultiForm<T extends Config> = ⋯;
const MultiForm = _MultiForm as
new <T extends Config>(config: T) => MultiForm<T>;
const mf1 = new MultiForm(config1);
const mf2 = new MultiForm(config2);
Здесь у нас есть общий псевдоним MultiForm<T>типа, который должен иметь или отсутствовать ключи в зависимости от конкретного подтипа Config, переданного в качестве аргумента типа T. И тогда mf1 и mf2 могут иметь разные ключи.
Для некоторых случаев этого может быть достаточно, хотя потребуется много испытаний; все, что требует, чтобы классы имели статически известные ключи, сломается, поэтому даже если вышеописанное сработает, следующее не будет:
class Oops<T extends Config> extends MultiForm<T> { } // error!
// --------------------------------> ~~~~~~~~~
// Base constructor return type 'MultiForm<T>' is not an
// object type or intersection of object types with
// statically known members.
Но давайте продолжим, предполагая, что все в порядке.
Я предполагаю, что мы оставим объявление вашего класса без изменений, за исключением его переименования:
class _MultiForm {
inputForm?: InputForm;
uploadForm?: UploadForm;
checkboxForm?: CheckboxForm;
constructor(private config: Config) {
if (this.config.includeInputForm) {
this.inputForm = new InputForm();
}
if (this.config.includeUploadForm) {
this.uploadForm = new UploadForm();
}
if (this.config.includeCheckboxForm) {
this.checkboxForm = new CheckboxForm();
}
}
}
Теперь нам нужно определить, как написать класс _MultiForm и псевдоним типа MultiForm<T>, и это, вероятно, будет несколько некрасиво, несмотря ни на что, поскольку нам нужно перевести что-то вроде {includeUploadForm: true, includeInputForm: false, includeCheckboxForm: true} так, чтобы оно было «точно таким же, как _MultiForm, за исключением того, что свойства uploadForm и checkboxForm обязательны, а свойство inputForm опущено. Мы можем использовать типы утилит Обязательно и Опустить, но это все равно будет некрасиво.
Сначала давайте определим объединение ключей MultiForm, которые мы хотим сохранить/опустить:
type FormKeys = "inputForm" | "uploadForm" | "checkboxForm";
И для каждого из них будет соответствующая includeXxx версия, которую мы можем использовать литералы шаблона для генерации:
type IncludeForm<K extends string> = `include${Capitalize<K>}`
Это позволяет нам переопределить Config в их терминах, хотя это и не обязательно:
type Config = { [P in IncludeForm<FormKeys>]: boolean }
Затем для данного подтипа T из Config давайте вычислим PresentProps<T>, подмножество FormKeys, которое мы хотим иметь и требуется:
type PresentProps<T extends Config> = {
[K in FormKeys]: T[IncludeForm<K>] extends true ? K : never
}[FormKeys]
Это распространяемый тип объекта (как он придуман в microsoft/TypeScript#47109 ), где мы используем сопоставленный тип и индексируем его, чтобы получить объединение его содержимого. Это означает, что мы получаем объединение T[IncludeForm<K>] extends true ? K : never для всех K в FormKeys. Это означает, что мы получаем все ключи формы K, где свойство T ключа IncludeForm<K> равно true.
Тогда MultiForm наконец:
type MultiForm<T extends Config> =
Omit<_MultiForm, FormKeys> &
Required<Pick<_MultiForm, PresentProps<T>>>
где мы сначала опускаем все ключи формы из _MultiForm, а затем добавляем обратно все те, которые присутствуют как true в T по мере необходимости.
Давайте проверим это:
const config = {
includeInputForm: true,
includeUploadForm: false,
includeCheckboxForm: true
} as const;
const multiForm = new MultiForm(config);
multiForm.checkboxForm
// ^? (property) checkboxForm: CheckboxForm
multiForm.inputForm
// ^? (property) inputForm: InputForm
multiForm.uploadForm; // error!
// -----> ~~~~~~~~~~
// Property 'uploadForm' does not exist on type
Теперь это работает так, как вы хотели. За исключением того, что я написал не так, как вы. Если вы аннотируете const config: Config = как тип config, вы выбрасываете любую более конкретную информацию, которую компилятор имеет о типе. Если вы хотите, чтобы компилятор знал, что Config имеет тип includeInputForm, а не true, вы не можете аннотировать. Кроме того, вам нужно что-то вроде константного утверждения, чтобы отслеживать литералы boolean и true, потому что они также имеют тенденцию расширяться до false.
Еще один тест:
const mf2 = new MultiForm({
includeUploadForm: true,
includeCheckboxForm: false,
includeInputForm: false
});
mf2.uploadForm // okay
mf2.inputForm // error
Также хорошо выглядит. Обратите внимание: объявив встроенную конфигурацию как литерал объекта , мы можем полностью избежать необходимости в boolean.
Итак, это работает. Стоит ли оно того? Я бы сказал, что, вероятно, нет. Он борется с системой типов TypeScript. Ему приходится обходить обычные объявления классов, а затем преодолевать препятствия типов. Вместо того, чтобы иметь as const, почему бы просто не инициализировать класс одним свойством includeXXX, которое является некоторым подтипом forms, и сделать класс универсальным? Да, вам нужно будет написать {inputForm?: InputForm, checkboxForm?: CheckboxForm, uploadForm?: UploadForm} вместо multiForm.forms.inputForm, но теперь multiForm.inputForm всегда будет иметь свойство MultiForm<T>, поэтому ключи forms известны. И вам не нужно прыгать через обручи, переводя MultiForm в другие вещи, вы просто инициализируете свойства самостоятельно. Или, может быть, это не сработает, и технически это выходит за рамки заданного вопроса. Но вам, вероятно, следует убедиться, что ваш вариант использования действительно требует такого сложного решения, прежде чем приступать к нему.
Объявления классов должны иметь статически известные ключи. Вы можете описать такой тип с помощью условно включенных ключей, но вам нужно утверждение типа, чтобы сказать, что
MultiFormявляется его конструктором. Это может выглядеть так, как показано по ссылке на игровую площадку. Это может быть «упс», но это противоречит тому, как TypeScript работает. Это полностью решает вопрос? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?