Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Encapsulate WebSocket Message in Typescript

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')?

like image 320
civic.sir Avatar asked Aug 31 '26 00:08

civic.sir


1 Answers

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);
}
like image 187
Sherif Elmetainy Avatar answered Sep 02 '26 13:09

Sherif Elmetainy