У меня есть реестр конструкторов классов. Реестр также используется для создания экземпляров этих классов. Чтобы создать реестр всего, что расширяет Base, я использую универсальный класс Registry, который определяет свои репозитории на основе дескрипторов, переданных в конструктор. Это работает хорошо.
Но каждый экземпляр, созданный с помощью реестра, также должен ссылаться на него, и я застрял в том, чтобы заставить TS сохранить точный вывод.
type RepositoryDescriptor = {
ctor: () => (new(options: RepositoryOptions<any, any>) => Base<any, any>)
// would like to have something like a generic class constructor to allow inference
// ctor: () => (new(options: RepositoryOptions<R, any>) => Base<R, any>)
}
type RepositoryCtor<T extends RepositoryDescriptor> = ReturnType<T['ctor']>
type Repository<T extends RepositoryDescriptor> = InstanceType<RepositoryCtor<T>>
// Think of this logic either that doesn't work
// type Repository<T extends RepositoryDescriptor, R> = (InstanceType<RepositoryCtor<T>>)<R>
type RepositoryOptions<R, V> = {
n: string,
v: V
r: R
}
class Registry<R extends {[key: string]: RepositoryDescriptor}> {
repositories: R
constructor(registry: R) {
this.repositories = registry
}
get<K extends string & keyof R, T extends Repository<R[K]>>(key: K, v: T['v']) : T {
let ctor = this.repositories[key].ctor();
return new ctor({r: this, v, n: key}) as T
// Alternatively of working on constructor, something like this may be great
// return new ctor({r: this, v, n: key}) as T<R>
}
}
class Base<R, V> {
registry: R
n: string
v: V
constructor({n, v, r}: RepositoryOptions<R, V>) {
this.registry = r
this.n = n
this.v = v
}
}
class A<R> extends Base<R, boolean> {}
class B<R> extends Base<R, number> {}
const registry = new Registry({
A: {
ctor: () => A
},
B: {
ctor: () => B
}
})
let a = registry.get('A', true) // It's typed as A<any>, registry type information is lost
let b = registry.get('B', 50) as B<typeof registry> // Cast can solved my issue but I prefer to avoid it if possible
let c = a.registry.get('C', true) // Should be an error
let d = b.registry.get('C', true) // Error is raised as b is casted
Насколько я понимаю, есть два мыслимых пути:
RepositoryDescriptor, чтобы он работал как универсальный конструктор классов, позволяющий выводить тип реестра.Repository, уметь использовать его с универсальным и приводить как Repository<R> в Registry::get.Я потерпел неудачу в обоих подходах.
Я не закрыт для другого шаблона для достижения той же цели, если он будет более удобен с помощью машинописного текста.
@jcalz Моя вина. Это была попытка решить проблемы, сделав конструктор RepositoryDescriptor универсальным. Теперь ошибок больше нет
Итак, как вы говорите, у вас есть две проблемы. Не могли бы вы отредактировать так, чтобы спрашивал только об одном из них? В противном случае он рискует закрыться с надписью «Требуется больше внимания: в настоящее время этот вопрос включает в себя несколько вопросов в одном. Он должен быть сосредоточен только на одной проблеме».
@jcalz Как я уже писал в своем вопросе, я почти уверен, что эти две проблемы тесно связаны и вызваны моей неспособностью правильно ввести обратные вызовы, предоставляемые конструктору реестра. Название вопроса возникло из этой идеи. Я немного отредактировал вопрос, чтобы, надеюсь, сделать его более очевидным.
Насколько я могу судить, они не особенно тесно связаны. Во-первых, вы не указали Base в возвращаемом типе ctor, а во-вторых, в TypeScript отсутствуют типы более высокого рода. Пожалуйста, выберите один из них, на котором стоит сосредоточиться. Ничто не мешает вам открыть второй вопрос для другого, как только первый будет решен, или решить его самостоятельно (похоже, что => Base<any, any> вместо => any быстро исправит первый вопрос), а затем сосредоточиться на другом. Я возобновлю участие, если это произойдет здесь.
@jcalz Большое спасибо за уделенное время. В ходе своих экспериментов я испортил это очевидное возвращаемое значение. Я сузился до того, что является сутью моего требования.
Чтобы легко представить то, что вы делаете, TypeScript потребуются типы более высокого порядка, как описано в ms/TS#1213 . Это не так, поэтому вам придется поработать над этим. Существуют различные подходы, но я не считаю ни один из них особенно удобным для пользователя. Если вы хотите добавить участника в «список классов» для каждого подкласса Base, вы можете сделать это , как показано по ссылке на игровую площадку. Это полностью решает вопрос? Если да, я напишу ответ с объяснением; если нет, то что мне не хватает?
Блин ! Тема 2014 года все еще открыта. XO Ваш обходной путь со слиянием объявлений действительно хорош, поскольку он позволяет избежать приведения каждого экземпляра (я также получил полезные знания по множеству деталей, когда читал вашу игровую площадку). В реальном случае первый интерфейс ClassList будет объявлен в библиотечном модуле рядом с Registry и Base. Каждый подкласс будет жить в своем собственном файле. В идеале слияние должно следовать той же логике. Можете ли вы также сказать пару слов о том, как правильно справиться с этим в своем ответе? Спасибо !
Я не знаю, что сказать здесь о многофайловых решениях, возможно, вы могли бы сделать расширение модуля, но мне не очень легко протестировать это в моей среде, и это по сути выходит за рамки заданного вопроса. Надеюсь, ничего страшного, если я смогу просто записать свой ответ как есть, а если вы не можете понять, как выполнить увеличение, вы откроете для него новый вопрос.
Это совершенно нормально. Спасибо






Для начала небольшое отступление. Будет гораздо проще сделать RepositoryDescriptorобобщенным в возвращаемом типе конструктора T:
type RepositoryDescriptor<T extends Base<any, any>> = {
ctor: () => (new (options: RepositoryOptions<any, any>) => T)
}
и тогда вы можете написать Registry в виде аргументов типа RepositoryDescriptor в его наборе repositories. Таким образом, вам не придется использовать условные типы для их последующего восстановления:
class Registry<R extends { [key: string]: Base<any, any> }> {
repositories: { [K in keyof R]: RepositoryDescriptor<R[K]> }
constructor(registry: { [K in keyof R]: RepositoryDescriptor<R[K]> }) {
this.repositories = registry
}
get<K extends string & keyof R>(key: K, v: R[K]['v']) { // return type?
let ctor = this.repositories[key].ctor();
return new ctor({ r: this, v, n: key });
}
}
Поэтому мы используем сопоставленный тип для преобразования R в тип repositories, и тогда вам не нужно будет отменять его позже. Просто напишите R[K] вместо Repository<R[K]>.
Это не обязательно, но с этим проще справиться.
Основная проблема, с которой вы столкнулись, — это попытка применить аргумент нового типа к произвольному универсальному классу. Но TypeScript не позволяет таким образом абстрагироваться от универсальных типов. Вы можете абстрагироваться только над обычными типами данных, самого низкого вида типа:
// you can do this
type Foo<T extends unknown> = ⋯T⋯
type Bar = Foo<string>;
Вы не можете поднять это на один уровень и поговорить о дженериках, например
// you can't do this
type Baz<F extends UnknownGeneric> = ⋯F<string>⋯
type Qux = Baz<Foo>
Вы не можете сказать, что F является «универсальным», а затем написать F<string> внутри тела Baz. И вы не можете перейти от Foo к Baz, потому что Foo сам по себе не является первоклассной вещью. Писать можно Foo<string>, но не только Foo.
Для этого потребуются типы более высокого рода, как того требует давняя открытая проблема GitHub https://github.com/microsoft/TypeScript/issues/1213.
Так что нет прямого способа сказать
class Registry<R extends { [key: string]: UnknownGeneric & Base<any, any> }> {
// that's not a thing ----^^^^^^^^^^^^^^
repositories: { [K in keyof R]: RepositoryDescriptor<R[K]> }
constructor(registry: { [K in keyof R]: RepositoryDescriptor<R[K]> }) {
this.repositories = registry
}
get<K extends string & keyof R>(key: K, v: R[K]['v']): R[K]<this> {
// that's not a thing -------------------------------------^^^^^^
let ctor = this.repositories[key].ctor();
return new ctor({ r: this, v, n: key });
}
}
потому что нельзя сказать, что R[K] будет «универсальным». Но если бы мы могли, идея состоит в том, что R[K] был бы общим подтипом Base, который принимает один аргумент типа для Registry<?>, и мы бы использовали полиморфный этот тип, чтобы представить, что мы помещаем this вместо r .
Существуют различные обходные пути и подходы, позволяющие выражать типы более высокого рода в TypeScript. Одна из версий, которую я видел, состоит в том, чтобы составить список универсальных типов, к которым вы хотите иметь возможность применять аргументы типа, сохраняя их в интерфейсе, чтобы вы могли объединять новые члены, когда захотите.
Вот как это может выглядеть на этом примере:
// list of generic subclasses of Base, initially empty
interface ClassList<R extends Registry<any>> { }
// produce the equivalent of T<R> if T were a higher kinded type
type Apply<T, R extends Registry<any>> =
{ [K in keyof ClassList<R>]:
T extends ClassList<any>[K] ? ClassList<R>[K] : never
}[keyof ClassList<R>]
class Registry<R extends { [key: string]: Base<any, any> }> {
// ⋮
get<K extends string & keyof R>(key: K, v: R[K]['v']): Apply<R[K], this> {
let ctor = this.repositories[key].ctor();
return new ctor({ r: this, v, n: key }) as Base<any, any> as Apply<R[K], this>;
}
}
Итак, если это сработает, то Apply<R[K], this> будет эквивалентно задуманному R[K]<this>. За исключением того, что R[K] будет каким-то конкретным подтипом Base<any, any> и не будет универсальным, поэтому это будет больше похоже на «заменить аргумент типа в R[K] на this» вместо «передать в общий R[K] аргумент типа this».
Давайте проверим это:
class A<R> extends Base<R, boolean> { a = 1 }
interface ClassList<R> { A: A<R> } // <-- merged in
class B<R> extends Base<R, number> { b = 2 }
interface ClassList<R> { B: B<R> } // <-- merged in
Итак, ClassList<R> содержит два члена: один типа A<R> и один типа B<R>. И теперь, когда мы используем это для создания реестра:
const registry = new Registry({
A: {
ctor: () => A
},
B: {
ctor: () => B
},
})
let a = registry.get('A', true)
// ^? let a: A<Registry<{ A: A<any>; B: B<any>; }>>
let b = registry.get('B', 50)
// ^? let b: B<Registry<{ A: A<any>; B: B<any>; }>>
let c = a.registry.get('C', true) // error!
let d = b.registry.get('C', true) // error!
Вы видите желаемое поведение. Тип возвращаемого значения registry.get('A', true) — A<Registry<{A: A<any>, B: B<any>}>>, и это вся необходимая информация. Эти любые ничему не повредят, потому что они будут заменены на this всякий раз, когда вы углубляетесь в get().
Итак, это работает. Это не обязательно лучший или даже хороший подход, есть и другие подходы, упомянутые в microsoft/TypeScript#1213, которые вы, возможно, захотите изучить. Однако до тех пор, пока мы не получим настоящие типы высшего рода в TypeScript, вам придется либо сдаться, либо обойти это.
Детская площадка, ссылка на код
Это прямо, ясно и полно полезных советов, помимо основного вопроса. Еще раз спасибо за ваше терпение и качество вашего ответа.
На игровой площадке есть несколько ошибок, но вы, похоже, не спрашиваете ни о одной из них. Пожалуйста, отредактируйте свой пример, чтобы разрешить их и не отвлекать.