我真的不明白 class 如何帮助您使用这样的动态代理,所以我将不使用 class 写一些东西,它仍然可以提供我认为可能是所需的行为。
将app 视为expando 对象的问题在于TypeScript 的静态类型系统并不想让您这样做。如果事先不知道 app 有一个名为 version 的类型,那么 TypeScript 的编译器不会让你读取这样的属性。您可以使用string index signature 来允许任何属性,但这对于您的用例来说可能过于宽松。理想情况下,我认为,您想调用app.bind(key, value),然后之后编译器知道app 在索引key 处有一个属性,其值类型为typeof value。 p>
这几乎可以通过将app 对象的bind() 方法设为assertion function,从而缩小app 的类型。断言函数有一个很大的警告。要使用它们,您必须显式注释 app 的类型(请参阅 microsoft/TypeScript#33580)。
这是一种方法:
interface _App<T> {
bind<K extends PropertyKey, V>(key: K, val: V): asserts this is App<Id<T & Record<K, V>>>;
}
type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
type App<T = {}> = Readonly<T> & _App<T>;
function makeApp(): App {
const items: any = {};
const bind = (name: any, data: any) => items[name] = data;
return new Proxy({} as any, {
get(target, prop) {
if (prop === 'bind') return bind;
return items[prop];
}
})
}
const app: App = makeApp();
// ~~~~~ <----- this annotation is required
App<T> 类型被视为Readonly<T>(意味着您可以读取与T 相同的所有属性)和_App<T>。 _App<T> 是一个带有 bind<K, V>(key: K, val: V) 方法的接口,该方法充当断言函数,该函数将带有键 K 和值 V 的属性添加到 T。
makeApp() 函数返回一个App<{}>,或一个没有额外扩展属性的App。它通过Proxy 执行此操作。请注意,get() 处理程序必须是传递给new Proxy() 的第二个对象的一部分。我有 items 作为位于 makeApp() 函数主体内的闭包隐藏对象,当您读取和写入扩展属性时,您正在处理这个 items 对象而不是 this.items。
让我们看看它是否有效:
// const app: App<{}>
app.bind('version', '1.0.0');
// const app: App<{ version: string; }>
app.bind('configs', { path: '/', env: 'local' });
// const app: App<{ version: string; configs: { path: string; env: string; };}>
console.log(app.version.length); // 5
console.log(app.configs.path.length); // 1
let is_local = app.configs.env === 'local';
console.log(is_local); // true
看起来不错。调用app.bind('version', '1.0.0') 后,app 的类型从App<{}> 更改为App<{ version: string; }。所以编译器理解了这一点,然后编译器允许你从app.version 中读取string 值。
我不知道这是否满足您所有的用例,但希望它至少是一条前进的道路。
Playground link to code