I am trying to encapsulate websocket messages into well defined Type.
I have main IIncommingMessage which is the base interface for all incoming messages as such:
export interface IIncommingMessage {
className : IClassName;
methodName : IMethodName;
}
There are various types of class this websocket can call as follows:
export type IClassName = IClassA | IClassB | IClassC
as well as various method in associated classes
export type IMethodName = IfooinClassA | IbarinClassA | IbazinClassB | IquxinClassB | IquuxinClassB | IcorgeinClassC
Such that it looks like this
ClassA:
foo()
bar()
ClassB:
baz()
qux()
quux()
ClassC:
corge
The idea is that if a websocket message arrives. It'll come as
{
className : "ClassB"
methodName : "qux"
}
So this should call ClassB of function qux().
The approach I'm taking looks bad. Was wondering if there is a better way to tightly couple the web-socket message to a well defined type
Also curious on how i'll make this call in TypeScript - would it be protoype.call('className.method')?
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);
}