Учитывая интерфейс IThing
interface IThing {
name: string;
}
Есть ли способ гарантировать, что классы, реализующие IThing, используют уникальные значения для name?
т.е.
class ThingOne implements IThing {
name = "Name_One"
}
class ThingTwo implements IThing {
name = "Name_Two"
}
Должен быть разрешен, в то время как
class ThingOne implements IThing {
name = "Name_One"
}
class ThingTwo implements IThing {
name = "Name_One"
}
Должно привести к ошибке TypeScript.
Значения свойств предназначены для чтения и написания от руки, поскольку они описывают сам объект, поэтому их нельзя сгенерировать.
Подход объединения может быть осуществим, хотя проект, над которым я работаю, будет иметь> 100 классов, которые реализуют IThing, и (если это имеет какое-либо значение) будет несколько свойств, которые должны иметь ограничение уникальности.
Трудно написать что-то, что не было бы ни незаконным, ни избыточным, ни тем и другим одновременно. Что вы думаете о этом подходе, когда вы поддерживаете ThingRegistry перекрестные ссылки на имена и экземпляры классов? Если это кажется многообещающим, я мог бы написать ответ с объяснением; если нет, то что мне не хватает?
Прежде всего, спасибо за ваши усилия. ThingRegistry выглядит многообещающе, хотя я не смог бы структурно дифференцировать каждый класс, интерфейс, который я предоставил в вопросе, действительно намного больше с дополнительными свойствами, которые будут иметь то же ограничение, что и имя. Я начал рассматривать возможность написания скрипта проверки, который бы собирал и создавал экземпляры всех экземпляров классов, а затем сообщал о любых проблемах с определениями. Однако этот подход кажется шатким, поскольку каждое новое определение класса нужно будет добавлять в скрипт вручную.
Если вы не хотите действительно странного поведения, вам нужно структурно различать классы; по крайней мере, каждый из них должен иметь элемент protected или private, чтобы компилятор рассматривал их как отдельные. Единственное, что отличает классы друг от друга, — это значения этих свойств? Если да, то мне интересно, почему у вас нет одного класса? Существуют и другие возможные подходы к обеспечению уникальности, но если вам нужны отдельные объявления классов для каждого, то я думаю, что все, что вы делаете, будет неуклюжим.
Классы различаются двумя способами; через значения свойств и через функциональность функций-членов. Каждый класс служит определением статистики, определения содержат функцию generate, которая может скомпилировать конкретное определение в набор выражений SQL, которые затем можно использовать для создания SQL-скрипта, генерирующего отчет. Сложность данной статистики сильно различается, некоторые функции generate просто возвращают свойства данного определения в правильно отформатированной строке, другие выполняют больше работы как часть процесса.
Создание одного класса, который мог бы обрабатывать все возможные случаи, привело бы к одной чрезвычайно большой функции generate, которую я пытаюсь избежать, чтобы каждая статистика и ее «компиляция» в SQL могли быть как можно более автономными. Классовый подход помогает в достижении этой цели, а также гарантирует, что все статистические данные имеют одинаковый интерфейс, так что предыдущий уровень (во всем процессе создания отчетов) может обрабатывать любую комбинацию статистических данных.
Если вам случится создать два структурно идентичных класса (где все строковые литеральные типы свойств идентичны, маловероятно, если у вас есть несколько свойств, но в вашем примере это практически неизбежно), то компилятор не сможет их различить; поэтому, если вы хотите избежать этого, вам нужно добавить какое-то структурное отличие, даже если оно вам не нужно.
В любом случае, может быть, этот подход будет работать лучше для вас, когда вы поддерживаете реестр Thing классов, но компилятор использует фабричную функцию, чтобы помочь отловить ошибки. Хотели бы вы, чтобы это было записано как ответ?
Я немного изучил ваш пример, и он, безусловно, кажется работоспособным. Единственное, что меня действительно беспокоит, - это сложность решения, я бы с удовольствием принял рецензию в качестве решения вопроса, не могли бы вы указать мне на некоторые дополнительные ресурсы, чтобы я мог лучше понять, как это работает?



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Я вижу единственное решение с использованием Set для отслеживания уникальных имен
let usedNames = new Set<string>();
class ThingOne implements IThing {
name: string;
constructor(name: string) {
if (usedNames.has(name)) {
throw new Error(`Name "${name}" has already been used.`);
}
usedNames.add(name);
this.name = name;
}
}TypeScript на самом деле не имеет встроенного способа гарантировать уникальность свойств в разных классах и даже не отслеживает, какие классы расширяют/реализуют другие классы/интерфейсы таким образом, чтобы разработчик мог запросить (структурный тип система означает, что класс может реализовать интерфейс, даже если вы не объявляете его как таковой, и поэтому набор вещей, реализующих интерфейс, потенциально бесконечен, в отличие от систем номинальных типов).
Поэтому, если вам нужна такая гарантия, вам нужно будет значительно реорганизовать свой код, чтобы вместо этого использовать некоторые другие функции TypeScript в качестве обходного пути.
Один из возможных подходов состоит в том, чтобы хранить ваши классы в объекте реестра и добавлять каждый класс к объекту, вызывая функцию утверждения, которая проверяет новый класс на соответствие существующим на предмет правильности/уникальности, а также записывает эту информацию в тип объекта реестра через сужение. Может быть так:
interface IThing {
name: string;
prop: string;
generate(): string;
}
type ValidateUniqueThing<TS extends IThing, T extends IThing> = {
[K in "name" | "prop"]:
string extends T[K] ? [Error, "Make", K, "readonly PLEASE"] :
T[K] extends TS[K] ? [Error, "Conflicting ", K, "Property with class named ",
Extract<TS, { [P in K]: T[K] }>["name"]] :
T[K]
}
function registerThing<R extends Record<keyof R, IThing>, T extends IThing>(
registry: { [K in keyof R]: new () => R[K] },
name: T['name'],
newThing: new () => (
T extends ValidateUniqueThing<R[keyof R], T> ? T : ValidateUniqueThing<R[keyof R], T>
)
): asserts registry is Extract<
{ [K in keyof R | T['name']]: new () => K extends keyof R ? R[K] : T },
typeof registry
> {
Object.assign(registry, { [name]: newThing });
}
Итак, служебный тип ValidateUniqueThing<TS, T> принимает объединение существующих Thing типов TS и кандидата на новое добавление T. Если T допустимое дополнение, то ValidateUniqueThing<TS, T> оценивается как T. В противном случае свойство T, которое нарушает правило, будет сопоставлено с несовместимым типом, отображаемая информация которого, как мы надеемся, даст разработчику некоторую информацию о том, как решить проблему.
Здесь возможны два основных возможных нарушения:
Если свойство имеет тип string, а не литеральный тип , то компилятор понятия не имеет, каким на самом деле будет значение, и не сможет гарантировать уникальность. Обычно это происходит, когда вы инициализируете свойство строковым литералом, не помечая свойство как только для чтения, поэтому несовместимый тип упоминает readonly (но вы можете изменить это на что хотите).
Если свойство уже существует в объединении существующих литеральных типов для того же свойства в других зарегистрированных классах, компилятор обнаружил нарушение уникальности. Несовместимый тип пытается выяснить, какой класс уже использовал это имя, используя служебный тип Extract<T, U>.
Функция registerThing() принимает объект registry в качестве первого аргумента, свойство name экземпляров нового класса в качестве второго аргумента и регистрируемый класс newThing в качестве третьего аргумента. Это общий в R, сопоставление классов name с их типами экземпляров и T тип экземпляра нового класса. Типы registry и name представляют собой прямые отображения и индексации в R и T. Тип newThing зависит от ValidateUniqueThing<R[keyof R], T> (где R[keyof R] — это объединение уже зарегистрированных типов экземпляров IThing) таким образом, что если newThing допустим, то тип соответствует ему (он просто становится new () => T), но если он недействителен, тогда тип не соответствует (он становится new () => X, где X — несовместимый тип из ValidateUniqueThing выше), и вы получаете сообщение об ошибке.
А поскольку registerThing() является функцией утверждения, ее тип возвращаемого значения asserts registry is ... приведет к сужению типа объекта реестра после вызова функции. Тип, к которому он сужается, — это просто существующий тип объекта реестра с добавленным новым свойством, соответствующим аргументу newThing.
Ладно, посмотрим в действии.
Сначала мы создаем пустой объект реестра:
const _Things = {};
// const _Things: {}
Я начал это имя с _, потому что, как только все будет сделано, мы скопируем его в новый const, чтобы быть уверенными, что оно готово для использования везде. Зарегистрируем класс:
registerThing(_Things, "ThingOne", class ThingOne {
readonly name = "ThingOne";
readonly prop = "Prop";
generate() {
return this.name + " " + this.prop
}
}); // okay
_Things;
/* const _Things: {
ThingOne: new () => ThingOne;
} */
Это удалось, и теперь мы видим, что тип _Things был изменен, чтобы отразить добавление. Давайте сделаем это снова:
registerThing(_Things, "ThingTwo", class ThingTwo {
readonly name = "ThingTwo";
readonly prop = "PropTwo";
generate() {
return this.name + " " + this.prop
}
}); // okay
_Things;
/* const _Things: {
ThingOne: new () => ThingOne;
ThingTwo: new () => ThingTwo;
} */
Все еще хорошо выглядит. Хорошо, а теперь давайте ошибемся:
registerThing(_Things, "ThingThree", class ThingThree { // error!
// '[Error, "Conflicting ", "prop", "Property with class named ", "ThingTwo"]
readonly name = "ThingThree";
readonly prop = "PropTwo";
generate() {
return "wha"
}
});
Ой, у нас ошибка. Если вы прищуритесь на сообщение об ошибке, то увидите, что prop конфликтует с ThingTwo. Давайте исправим это:
registerThing(_Things, "ThingThree", class ThingThree {
readonly name = "ThingThree";
readonly prop = "PropThree";
generate() {
return "wha"
}
}); // okay
И совершим еще одну ошибку:
registerThing(_Things, "ThingFour", class ThingFour { // error!
// [Error, "Make", "name", "readonly PLEASE"]
name = "ThingFour";
readonly prop = "PropFour";
generate() {
return ""
}
});
Ой, у нас ошибка. Прищуривание показывает, что name не является readonly и, следовательно, имеет тип string, а не "ThingFour". Давайте исправим это:
registerThing(_Things, "ThingFour", class ThingFour {
readonly name = "ThingFour";
readonly prop = "PropFour";
generate() {
return ""
}
}); // okay
Большой. Теперь копируем заполненный _Things в Things:
const Things = _Things;
/*
const Things: {
ThingOne: new () => ThingOne;
ThingTwo: new () => ThingTwo;
ThingThree: new () => ThingThree;
ThingFour: new () => ThingFour;
}*/
который имеет все реквизиты, которые мы хотим. И чтобы использовать классы, мы просто индексируем в Things:
function foo() {
const thing2 = new Things.ThingTwo();
// const thing2: ThingTwo
console.info(thing2.generate())
}
Итак, это работает! Причина, по которой я скопировал _Things в Things, заключается в том, что, как и все эффекты анализа потока управления, сужение, сделанное в функциях утверждений, не сохраняется за границами функций. Итак, внутри foo() вы обнаружите, что _Things выглядит совершенно пустым:
function foo() {
const thing2 = new _Things.ThingTwo(); // error!
// Property 'ThingTwo' does not exist on type '{}'.
}
Это прискорбно, но чтобы предотвратить это, компилятору пришлось бы приложить много дополнительных усилий, чтобы попытаться выяснить, когда был вызван foo() относительно того, когда _Things был сконфигурирован, а это невозможно (см. microsoft/TypeScript#9998 для обсуждение общего вопроса). Достаточно легко добраться до места, где компилятор знает, что суженный тип действителен, а затем скопировать в const, тип которого одинаков независимо от того, где вы его используете.
Итак, ура, все работает. Это сложно, и, возможно, слишком сложно, чтобы быть полезным для вас. Возможно, вам больше понравится какой-то другой подход, но все, что я могу придумать (включая этот подход), будет включать какую-то цикличность, избыточность, уродство или «нелокальность» ошибок.
Под этим я подразумеваю, что вы будете ловить нарушения уникальности, но сообщение об ошибке не будет близко к тому месту, где произошло нарушение. Например, если вы построили какое-то поддерживаемое вручную объединение всех классов (что избыточно), а затем написали тип, который проверял бы его на предмет нарушений уникальности, ошибка была бы где-то рядом с этим типом и не обязательно рядом с классом с проблема.
Поэтому я думаю, что в конечном итоге это сведется к вопросу вкуса и применимости к вариантам использования, как это часто бывает с обходными путями.
TS не пытается отслеживать, какие классы имеют предложение
implements IThing, поэтому вам нужно будет вручную поддерживать союз, напримерtype Things = ThingOne | ThingTwo, прежде чем вы даже попытаетесь получить проверку. Будет ли это неприемлемым или вы заинтересованы в таком подходе?