About your first part to have well defined type this is how I would implement it.
class ClassA {
foo() { }
bar() { }
}
class ClassB {
baz() { }
qux() { }
quux() { }
}
class ClassC {
corge() { }
notAMethod = 1;
}
// This will be a like a dictionary mapping name to class
// Will also be used in the second part.
const Classes = {
ClassA,
ClassB,
ClassC,
};
// This will be 'ClassA'|'ClassB'|'ClassC'
type IClassName = keyof typeof Classes;
type IClassOf<T extends IClassName> = InstanceType<typeof Classes[T]>;
type MethodFilter<T extends IClassName> = { [MN in keyof IClassOf<T>]: IClassOf<T>[MN] extends () => void ? MN : never }
type MethodName<T extends IClassName> = MethodFilter<T>[keyof MethodFilter<T>];
interface IGenericIncomingMessage<T extends IClassName> {
className: T;
methodName: MethodName<T>;
}
type IIncomingMessage = IGenericIncomingMessage<'ClassA'> | IGenericIncomingMessage<'ClassB'> | IGenericIncomingMessage<'ClassC'>;
let msg0: IIncomingMessage = {
className: 'ClassA',
methodName: 'foo', // valid
}
let msg1: IIncomingMessage = {
className: 'ClassC',
methodName: 'corge', // valid
}
let msg2: IIncomingMessage = { // compiler error. Type ... is not assignable to type 'IIncomingMessage'.
className: 'ClassA',
methodName: 'corge',
}
let msg3: IIncomingMessage = {
className: 'ClassD', // compiler error. ClassD Name is not not in 'ClassA' | 'ClassB' | 'ClassC'
methodName: 'corge',
}
let msg4: IIncomingMessage = {
className: 'ClassC',
methodName: 'notAMethod', // compiler error. Type '"notAMethod"' is not assignable to type '"foo" | "bar" | "baz" | "qux" | "quux" | "corge"'.
}
So about the second part I use the Classes dictionary I defined earlier to lookup class by name, create a new instance of the class. This means that a malicious message having a valid class name that is not in the dictionary will not work.
// I omit error handling here.
function invokeFunction<T extends IClassName>(message: IGenericIncomingMessage<T>): void {
// Look for the class instance in dictionary and create an instance
const instance = new Classes[message.className]() as IClassOf<T>;
// Find the method by name. The cast to any is to silence a compiler error;
// You may need to perform additional login to validate that the method is allowed.
const fn = instance[message.methodName] as unknown as () => void;
fn.apply(instance);
}