3

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().

  1. 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

  2. Also curious on how i'll make this call in TypeScript - would it be protoype.call('className.method')?

4
  • Are these methods static? Commented Aug 24, 2020 at 23:59
  • And do these have to be classes? Commented Aug 25, 2020 at 0:18
  • Can it not work without being forced into Static? Commented Aug 25, 2020 at 1:09
  • Yes they are different classes that will handle particular messages from the websocket connection Commented Aug 25, 2020 at 1:09

1 Answer 1

3
+50

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);
}
Sign up to request clarification or add additional context in comments.

8 Comments

This is superb!!! Just one quick question regrading this. What if the className was something like Class.A is there a way to map ClassA to message { className : "ClassA"; methodName : "foo"} other than having a mapping file?
In that case you would have to manually specify the names in the Classes object: const Classes = { 'ClassA': Class.A, 'ClassB': Class.B, 'ClassC': Class.C, };
Francis's suggestion is would work in that case. He also made some nice edits to improve my answer. Thanks francis.
Great thanks a lot both SherifElmetainy and @francis duvivier !!! much appreciated!
Sorry for the ignorant question is it possible to append / remove from let Classes = { 'ClassA': Class.A, 'ClassB': Class.B, 'ClassC': Class.C, }; I thought it'll work just like a set where i could go Classes.push or Classes.add but doesn't seem to work since it's an object. Not sure if there is a way to dynamically add to this Classes variable
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.