使状态管理系统类型安全时的循环引用
Circular reference when making state management system typesafe
我在一个旧项目中的状态管理系统使用的是 MobX。最近我想让它与 SSR 一起工作(因为我在较新的项目中取得了成功)。
我们的想法是让一个商店经理来管理所有商店,商店也可以访问它以便能够读取和修改其他商店。这适用于 JavaScript,但 TypeScript 使它成为一个问题。
我已经设法将问题隔离到一个可重现的示例中。您可以 运行 在 TypeScript playground 中查看此问题。
/**
* The manager holds all the stores in the application
*/
class StoreManager<T extends Record<string, InitializableStore>> {
public stores: T = {} as any
constructor(
public instantiators: { [K in keyof T]: (manager: any) => T[K] },
) {
for (const [name, creator] of Object.entries(instantiators)) {
this.stores[name as keyof T] = creator(this)
}
}
public async init() {
console.info("Initializing stores")
await Promise.all(Object.values(this.stores).map((x) => x.init()))
}
}
export type Manager = StoreManager<Stores>
/**
* This class represents a store which should have access to the manager
*/
class InitializableStore {
constructor(protected manager: Manager) {}
public init(): void | Promise<void> {}
}
/**
* Helper function for creating a store factory
*/
const createStoreFactory = <S extends InitializableStore>(
storeClass: new (manager: Manager) => S,
) => (manager: Manager) => new storeClass(manager)
/**
* Example store set up
*/
class StoreA extends InitializableStore {
public init() {}
public meow() {
console.log("Meow")
}
}
class StoreB extends InitializableStore {
public init() {
const { storeA } = this.manager.stores
storeA.meow()
}
public woof() {
console.log("Woof!")
}
}
const storeA = createStoreFactory(StoreA)
const storeB = createStoreFactory(StoreB)
/**
* Defining the stores for the manager here
* */
const stores = { storeA, storeB }
export type StoreMapReturn<
T extends Record<string, (manager: Manager) => InitializableStore>
> = {
[K in keyof T]: ReturnType<T[K]>
}
/**
* This errors, because there's a circular reference
*/
export type Stores = StoreMapReturn<typeof stores>
由于商店需要访问经理,因此类型非常复杂并且实际上不起作用,因为存在循环引用。在完美的情况下,它会像这样工作:
- 经理可以在任何商店访问
- 管理器不是从文件导入的全局对象(因此它可以动态创建并且完全封装)
- 从管理器访问时,这些商店是完全类型安全的
基本上,编译器必须为 Stores
类型推断以下链:
type Stores = typeof stores > createStoreFactory > Manager > StoreManager<Stores> > Stores
// ^ circular ↩
以上循环引用无法解决。如果将鼠标悬停在 const storeA
初始值设定项上,您会得到:
'storeA' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
这个错误声明做了很好的解释工作:我们可以用显式类型注释其中一个 const
变量来结束循环类型解析 (sample):
type StoreFactory<T extends InitializableStore> = (manager: Manager) => T
const storeA: StoreFactory<StoreA> = createStoreFactory(StoreA)
const storeB: StoreFactory<StoreB> = createStoreFactory(StoreB)
如果每个商店重复太多,您可以先以自上而下的方式定义 Stores
(sample):
export type Stores = {
storeA: StoreA;
storeB: StoreB;
}
export type StoreFactories = { [K in keyof Stores]: (manager: Manager) => Stores[K] }
const storeA = createStoreFactory(StoreA)
const storeB = createStoreFactory(StoreB)
const stores: StoreFactories = { storeA, storeB }
我在一个旧项目中的状态管理系统使用的是 MobX。最近我想让它与 SSR 一起工作(因为我在较新的项目中取得了成功)。
我们的想法是让一个商店经理来管理所有商店,商店也可以访问它以便能够读取和修改其他商店。这适用于 JavaScript,但 TypeScript 使它成为一个问题。
我已经设法将问题隔离到一个可重现的示例中。您可以 运行 在 TypeScript playground 中查看此问题。
/**
* The manager holds all the stores in the application
*/
class StoreManager<T extends Record<string, InitializableStore>> {
public stores: T = {} as any
constructor(
public instantiators: { [K in keyof T]: (manager: any) => T[K] },
) {
for (const [name, creator] of Object.entries(instantiators)) {
this.stores[name as keyof T] = creator(this)
}
}
public async init() {
console.info("Initializing stores")
await Promise.all(Object.values(this.stores).map((x) => x.init()))
}
}
export type Manager = StoreManager<Stores>
/**
* This class represents a store which should have access to the manager
*/
class InitializableStore {
constructor(protected manager: Manager) {}
public init(): void | Promise<void> {}
}
/**
* Helper function for creating a store factory
*/
const createStoreFactory = <S extends InitializableStore>(
storeClass: new (manager: Manager) => S,
) => (manager: Manager) => new storeClass(manager)
/**
* Example store set up
*/
class StoreA extends InitializableStore {
public init() {}
public meow() {
console.log("Meow")
}
}
class StoreB extends InitializableStore {
public init() {
const { storeA } = this.manager.stores
storeA.meow()
}
public woof() {
console.log("Woof!")
}
}
const storeA = createStoreFactory(StoreA)
const storeB = createStoreFactory(StoreB)
/**
* Defining the stores for the manager here
* */
const stores = { storeA, storeB }
export type StoreMapReturn<
T extends Record<string, (manager: Manager) => InitializableStore>
> = {
[K in keyof T]: ReturnType<T[K]>
}
/**
* This errors, because there's a circular reference
*/
export type Stores = StoreMapReturn<typeof stores>
由于商店需要访问经理,因此类型非常复杂并且实际上不起作用,因为存在循环引用。在完美的情况下,它会像这样工作:
- 经理可以在任何商店访问
- 管理器不是从文件导入的全局对象(因此它可以动态创建并且完全封装)
- 从管理器访问时,这些商店是完全类型安全的
基本上,编译器必须为 Stores
类型推断以下链:
type Stores = typeof stores > createStoreFactory > Manager > StoreManager<Stores> > Stores
// ^ circular ↩
以上循环引用无法解决。如果将鼠标悬停在 const storeA
初始值设定项上,您会得到:
'storeA' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
这个错误声明做了很好的解释工作:我们可以用显式类型注释其中一个 const
变量来结束循环类型解析 (sample):
type StoreFactory<T extends InitializableStore> = (manager: Manager) => T
const storeA: StoreFactory<StoreA> = createStoreFactory(StoreA)
const storeB: StoreFactory<StoreB> = createStoreFactory(StoreB)
如果每个商店重复太多,您可以先以自上而下的方式定义 Stores
(sample):
export type Stores = {
storeA: StoreA;
storeB: StoreB;
}
export type StoreFactories = { [K in keyof Stores]: (manager: Manager) => Stores[K] }
const storeA = createStoreFactory(StoreA)
const storeB = createStoreFactory(StoreB)
const stores: StoreFactories = { storeA, storeB }