For the question as originally asked:
The compiler doesn't really track type mutations, so we'll have to tell it what we're doing inside the body of add(); specifically, we need to manually represent the type that we want to treat the returned this as. Here's one approach:
add<K extends Extract<keyof O, string | number>>(name: K, type: O[K]) {
this.objects.set(name, type);
(this as any)['get' + name] = () => {
return type;
}
return this as this & Record<`get${K}`, () => O[K]>
}
When you call add with a name of type K and with a type of type O[K] (where O is what you were calling objectsList), the return value will be of type this & Record<`get${K}`, ()=>O[K]>. That's an intersection type of both this along with an object with a single property whose key is `get${K}` (a template literal type you get by concatenating the string "get" with the key K) and whose value is a no-arg function that returns a value of type O[K]. We have to use a type assertion to say that the returned this value is of this augmented type, because the compiler can't track such things.
Anyway you can verify that it works as desired:
const app = new App<{ Test: string, Test2: number }>().
add('Test', 'this is my test string').
add('Test2', 5);
app.getTest(); // no error
app.getTest2(); // no error
On the other hand, if you want to skip the builder pattern and instead pass the whole object of type O into the App constructor, then you need the App class instances to have dynamic keys which are not statically known; that is, App<O> has keys that depend on O. This is possible to describe, but not directly with class or interface declarations. First let's describe what the App constructor type should look like:
new <O>(initObjects: O) => App<O>
It should have a construct signature that takes an initObjects parameter of type O, and return a value of type App<O>, which is defined like this:
type App<O> = {
[K in keyof O as `get${Extract<K, string | number>}`]:
() => O[K]
};
That's a mapped type with remapped keys so that for each key K in O, App<O> has a key with the same name with "get" prepended to it. And the value of the property at that key is a no-arg function that returns a value of the property type from O.
Again, we want to say that the App constructor has that shape, but the compiler can't verify it. So we'll need to use another type assertion (and it's easier to use a class expression to avoid having to use a dummy class name):
const App = class App {
constructor(initObjects: any) {
Object.keys(initObjects).forEach(
k => (this as any)["get" + k] = () => initObjects[k]
);
}
} as new <O>(initObjects: O) => App<O>;
See how the implementation of the inner App class just copies each member of initObjects into this in the appropriate way. Let's test it:
const inst = new App({ Test: "this is my test string", Test2: 5 });
console.log(inst.getTest().toUpperCase());
Looks good!
Playground link to code