TypeScript:在运行时带类型安全向类中添加动态字段



我正在将一些遗留的JS代码转换为TS,并且遇到了一个有趣的键入情况。有一个模块系统,在运行时将类实例加载到另一个类实例的命名属性上,我试图找出如何键入对象:

class Module {
public name: string;
}
class ThisModule extends Module {
public name = 'thisModule';
public log = () => {
console.log('ThisModule called!');
};
}
class ThatModule extends Module {
public name = 'thatModule';
public log = () => {
console.log('ThatModule called!');
};
}
interface IModule extends Module {}
type ModulesInput = Module[];
class Container {
public init = (modules: ModulesInput) => {
for (const module of modules) {
this[module.name] = module;
}
};
}
const thisModule = new ThisModule();
const thatModule = new ThatModule();
const container = new Container<ThisModule | ThatModule >();
const modules: ModulesInput = [
thisModule,
thatModule,
];
container.init(modules);
// Should not throw TS error
container.thisModule.log();
container.thatModule.log();
// Should throw error ("Property anythingElse does not exist on Container")
container.anythingElse.log();

问题是TypeScript没有识别出container.thisModulecontainer.thatModule应该存在。上面写着TS2339: Property 'thisModule' does not exist on type 'Container'.(thatModule也一样)

有没有办法输入这个系统?到目前为止,我已经有一些有限的成功添加多个泛型到Container类(例如type keys = 'thisModule' | 'thatModule'type ModulesInUse = ThisModule | ThatModule),但是TypeScript可以从类中发现名称并动态学习,它应该期望container对象上的那些键具有各自类的类型吗?

提前感谢!

为了消除this[module.name] = module;行的错误,您需要提供索引签名:[prop: string]: Module

请注意,除了一种情况,typescript不跟踪突变,因此您需要从init属性返回新的this

整个代码:

class Module {
public name: string = '' // need to be initialized
}
class ThisModule extends Module {
public name = 'thisModule' as const;
public log = () => {
console.log('ThisModule called!');
};
}
class ThatModule extends Module {
public name = 'thatModule' as const;
public log = () => {
console.log('ThatModule called!');
};
}

type Reduce<
T extends ReadonlyArray<Module>,
Acc extends Record<string, unknown> = {}
> =
(T extends []
? Acc
: (T extends [infer Head, ...infer Tail]
? (Tail extends Module[]
? (Head extends Module
? Reduce<Tail, Acc & Record<Head['name'], Head>>
: never)
: never)
: never)
)
class Container {
[prop: string]: Module; // you need to provide index signature to class in order to use  this[module.name] = module
public init = <M extends Module, Modules extends M[]>(modules: [...Modules]) => {
for (const module of modules) {
this[module.name] = module;
}
return this as Reduce<[...Modules]>
};
}
const thisModule = new ThisModule();
const thatModule = new ThatModule()
const container = new Container();

const newContainer = container.init([
thisModule,
thatModule,
]);

// Should not throw TS error
newContainer.thisModule.log()
newContainer.thatModule.log();
// Should throw error ("Property anythingElse does not exist on Container")
newContainer.anythingElse.log() // error

游乐场

我使用了Reduce工具类型,它的工作原理几乎像Array.prototype.reduce,它将元组中的所有元素折叠成一个对象。

此外,我已经使用可变元组类型从init参数<M extends Module, Modules extends M[]>(modules: [...Modules])中推断出每个模块。

这里还有一个版本,它使用asserts this is ...来原地执行断言,而不是返回新值。

Typescript受到一个概念的限制:类不能有"动态字段";由于在类中很难进行类型检查,但是正确字段和类的联合是有效的,并且在函数中编写正确的类型契约是非常可能的,因此有两种方法可以获得相关信息:

选项1:将name属性类型为文字

如果模块的属性name的类型不是string,而是作为它包含的字面值字符串,那么以后有更多的空间来利用它,

abstract class Module {
public abstract name: string;
}
class ThisModule extends Module {
public name:'thisModule' = 'thisModule';
//  ^^^^^ this is the important bit
public logThis = () => { console.log('ThisModule called!'); };
}
class ThatModule extends Module {
public name:'thatModule' = 'thatModule';
//  ^^^^^ this is the important bit
public logThat = () => { console.log('ThatModule called!'); };
}

你可以这样定义你要寻找的容器类型:

type TypesafeContainer<ModuleUnion extends Module> = {
[K in ModuleUnion["name"]]: ModuleUnion["name"] extends K ? ModuleUnion : never;
}

这接受一个泛型,它是多个模块的并集,并生成您要查找的类型:

declare function makeContainer<T extends Module>(modules: T[]): TypesafeContainer<T>
const thisModule = new ThisModule();
const thatModule = new ThatModule();
const container = makeContainer([thisModule, thatModule]);
// does not throw TS error
container.thisModule.logThis();
container.thatModule.logThat();
// Should throw error ("Property anythingElse does not exist on Container")
container.anythingElse.log();

一个问题是,如果你试图声明一个类Container来扩展这个类型,你会得到一个错误:"接口只能扩展对象类型或具有静态已知成员的对象类型的交集。">

class Container<T extends Module>  {
public constructor(modules: T[]) {
for (const [module] of modules) {
this[module.name] = module;
}
};
}
interface Container<T extends Module> extends TypesafeContainer<T>{}
//^^ this is not allowed :(

我认为这主要是typescript的限制,这个结构没有任何理论问题。

选项2:Pick从所有允许的模块的接口

在每个模块中携带泛型可能会很烦人,如果你在任何地方使用泛型for名称,你可能会遇到更模糊的键入问题,另一种选择是显式列出所有支持模块的接口:

interface CONTAINER_ALL_MODULES {
thisModule: ThisModule
thatModule: ThatModule
}
// still not valid unfortunately :(
interface Container2<K extends keyof CONTAINER_ALL_MODULES> extends Pick<CONTAINER_ALL_MODULES, K>{
new(modules: Array<CONTAINER_ALL_MODULES[K]>): this
}

这个用于容器的接口遇到了同样的限制,即不能用动态成员扩展类型,但是仍然可以使用具有正确类型签名的函数:

class RealContainer {
// actual stuff here
}
declare function makeContainer2<K extends keyof CONTAINER_ALL_MODULES>(modules: Array<CONTAINER_ALL_MODULES[K]>): RealContainer & Pick<CONTAINER_ALL_MODULES, K>
const container2 = makeContainer2([thisModule, thatModule]);
// does not throw TS error
container2.thisModule.logThis();
container2.thatModule.logThat();
// Should throw error ("Property anythingElse does not exist on Container")
container2.anythingElse.log();

希望这对你有帮助,希望你能找到适合你的东西:)

ts操场

最新更新