Encapsulate WebSocket Message in Typescript - javascript

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);
}

Related

Typescript check if a type is a generic param?

I have a bunch of model classes that each have a model type. I also have a "TypeToModel" helper to get the model class given a model type. I.e.:
type BaseModel = { id: number };
type Models = {
user: BaseModel & { name: string },
post: BaseModel & { title: string },
};
type ModelType = 'user' | 'post';
type TypeToModel<T extends keyof Models> = ModelType extends T
? BaseModel
: Models[T];
function getModel<T extends ModelType>(
modelType: T,
): TypeToModel<T> {
return {} as unknown as TypeToModel<T>;
}
If a single model type is passed to getModel, I want the return type to be the model's type. If the entire ModelType union gets passed to getModel, I want it to return BaseModel. This works most of the time:
const userA = getModel('user'); // userA = BaseModel & { name: string }
const userB = getModel('user' as ModelType); // userB = BaseModel
However, if a variable with a generic type gets passed to getModel, it returns a union of all the models. E.g.:
function foo<T extends ModelType>(modelType: T) {
const userC = getModel(modelType);
userC.id;
/*
userC = BaseModel
| (BaseModel & { name: string })
| (BaseModel & { title: string })
*/
}
I expected userC to be BaseModel, but it's a union of all the models. I want to detect if it would return a union of all the models and make it return BaseModel instead. I tried using IsUnion<>, but it was false for both T and userC.
How can I check if getModel's argument has a generic type? In other words, is it possible to write an IsGeneric<> utility type?
TS Playground
To my knowledge there is no way to detect that state procedurally from within your utility type and react to it. I have no clue what this is called by the way.
You can also witness control flow analysis giving up:
function foo<T extends ModelType>(modelType: T) {
if(modelType == 'user') {
modelType; // modelType: T extends ModelType
}
}
T is not ModelType. As you say, it's undetermined. so ModelType extends T cannot be resolved and the output of TypeToModel is the whole conditional ModelType extends T ? BaseModel : Models[T].
I would not try to understand why indexing id changes anything because I'm pretty sure TS goes through different code paths to handle this and it just so happens that their behaviour is inconsistent: TypeToModel<T>['id'] would not be resolved to a union, it would just remain as it is.

method that takes parameter of multiple type and decide the behavior at runtime

I have a method like this
square(num: number | string): number {
//
}
In this method I want to check the parameter's data type and invoke the logic accordingly. How do to that in TS?
Edit:
Specific types in question
export type KV<T extends string | number> = {
key: string;
val: T;
}
export type LogicalKV<T extends string | number> = {
logicalOperator: LogicalOperator
filters?: Filter<T>[];
logicalFilters?: LogicalFilter<T>[];
}
Function
class Foo {
read(filter: KV<string | number> | LogicalKV <string | number>): Promise<void> {
// if filter is of KV do this, else do that
}
}
Types are stripped at runtime unfortunately. But you could go the good old way of typeof
if (typeof num === "string") {/*it's string here*/} else {/*it's number here*/}
Typescript will also understand that and coerce to an appropriate type in each branch.
In case of custom types, like yours, it's going to be a bit different. Algebraic data types are involved. It's also not too pretty when you instantiate it, but you have to deal with passing runtime string "kinds" one way or another.
export type KV = {
kvField: string;
kind: "kv";
};
export type LogicalKV = {
logicalKvField: number;
kind: "logicalKv";
};
const KVInstance: KV = {
kind: "kv",
kvField: "foo"
};
const LogicalKVInstance: LogicalKV = {
kind: "logicalKv",
logicalKvField: 1
};
type FilterArg = KV | LogicalKV;
const fn = (f: FilterArg) => {
switch (f.kind) {
case "kv":
// ts knows it's KV
f.kvField;
return;
case "logicalKv":
// ts knows it's LogicalKV
f.logicalKvField;
return;
}
}

How can I generate a type of string literals from an objects keys and values returned by a function?

I am trying to create a type from a readonly object, i.e.
const Actions = {
'user.crud': ['user.create', 'user.read', 'user.update', 'user.delete'],
} as const
type ActionsType = keyof typeof Actions | typeof Actions[keyof typeof Actions][number]
The above works nicely and sets up the type ActionsType to the string literals ('user.crud', 'user.create', etc..).
However, the Actions object above is very simplistic, instead, I really need to generate the Actions via functions. When I port the above over to being generated by a function, i.e.
// set up a function to generate all actions for the passed role
function getActions (role: string): Record<string, string[]> {
return {
[`${role}.crud`]: [`${role}.create`, `${role}.read`, `${role}.update`, `${role}.delete`],
}
}
// generate the Actions from a function
const ActionsFromFunction = {
...getActions('user'),
} as const
// set up the Actions from a readonly object with values generated by getActions()
type ActionsFromFunctionType = keyof typeof ActionsFromFunction | typeof ActionsFromFunction[keyof typeof ActionsFromFunction][number]
the type ActionsFromFunctionType is no longer set to the string literals. Instead it is set to: string | number and in turn type tests fail as any string is accepted.
I've put together a demo of the above:
Playground
Is there a way of generating the Actions object via a function, whilst still maintaining the string literals within the type?
Your goal is only achievable through typescript Template literal types. They are not supported in typescript 4.0, but will be available in 4.1 version.
This is how you could do it with typescript 4.1
type CrudOperations<ROLE extends string> = [`${ROLE}.create`, `${ROLE}.read`, `${ROLE}.update`, `${ROLE}.delete`];
type GetActionsResult<ROLE extends string> = string extends ROLE // check if we can infer type
? { [k: string]: string[] } // if type is not inferable
: { [K in `${ROLE}.crud`]: CrudOperations<ROLE> };
function getActions<ROLE extends string>(role: ROLE): GetActionsResult<ROLE> {
return {
[`${role}.crud`]: [`${role}.create`, `${role}.read`, `${role}.update`, `${role}.delete`]
} as GetActionsResult<ROLE>;
}
// falls back to { string: string[] } structure
const actions = getActions('admin' as string);
// generate the Actions from a function
const ActionsFromFunction = {
...getActions('user'),
...getActions('orders'),
}
Playground link
You can also write getActions without explicit types by inference:
function getActions<K extends string>(role: K) {
const actions = {
[`${role}.crud`]: [`${role}.create`, `${role}.read`, `${role}.update`,
`${role}.delete`],
} as const
return actions as Record<K,typeof actions[K]>
}
Test it:
const ActionsFromFunction = {
...getActions('user'),
}
// { user: readonly ["user.create", "user.read", "user.update", "user.delete"];}
const ActionsFromFunction2 = {
...getActions('user' as string),
}
// { [x: string]: readonly [`${string}.create`, ..., `${string}.delete`]; }
type ActionsFromFunctionType =
| keyof typeof ActionsFromFunction
| typeof ActionsFromFunction[keyof typeof ActionsFromFunction][number]
// "user" | "user.create" | "user.read" | "user.update" | "user.delete"
Note, that getActions won't be accurate, if your role has a union string type:
const ActionsFromFunction3 = {
...getActions('user' as 'user' | 'admin'),
} // { user: ...; admin: ...; }
You might write getActions as distributed conditional type, if needed:
return actions as K extends any ? Record<K,typeof actions[K]> : never
const ActionsFromFunction3 = {
...getActions('user' as 'user' | 'admin'),
}
/*
| { user: readonly ["user.create", "user.read", "user.update", "user.delete"];}
| { admin: readonly ["admin.create", "admin.read", "admin.update", "admin.delete"]; }
*/
Playground

Javascript function without curly braces

A good function is declared as follows:
export declare class SOMETHING implements OnDestroy {
sayHello() {
// some code to say hello
}
}
But in node_modules (angular material specifically) I found this function code in typesript:
export declare class SOMETHING implements OnDestroy {
sayHello(parA: string, parB?: string: parC: MatSnackBarConfig): MartSnackBarRef<SimpleSnackBar>;
}
But.... where is the {} in the function sayHello?
Where I can find information about this topic?
Thanks!
It's called a method declaration. You are stating to typescript that this method will be implemented and that it will be it's type.
It's useful in Interfaces and Abstract classes, and also method overload.
For example, the overload, in here I declare that the method findOneAndUpdate have two different way to be called, which leads to two different results.
public findOneAndUpdate<U = T>(data: {
where?: unknown | {};
action?: unknown | {};
option?: MongooseOptionsReq;
session?: false | mongoose.ClientSession;
createObject: true;
mustExist?: boolean;
noLean?: boolean;
schemaPosition?: number;
}): Promise<CollectionDocument<U>>;
public findOneAndUpdate<U = T>(data: {
where?: unknown | {};
action?: unknown | {};
option?: MongooseOptionsReq;
session?: false | mongoose.ClientSession;
createObject?: false;
mustExist?: boolean;
noLean?: boolean;
schemaPosition?: number;
}): Promise<U>;
In addition to the type declaration, of course you need to implement the method :
public findOneAndUpdate<U = T>({
where = {},
action = {},
option = {
new: true,
},
createObject = false,
session = false,
mustExist = false,
noLean = false,
schemaPosition,
}: {
where?: unknown | {};
action?: unknown | {};
option?: MongooseOptionsReq;
session?: false | mongoose.ClientSession;
createObject?: boolean;
mustExist?: boolean;
noLean?: boolean;
schemaPosition?: number;
}): Promise<CollectionDocument<U>> | Promise<U> {
// ...
}
This is closely related to abstract method deceleration - but more flexible without the abstract keyword. This way it is possible to expose the shape of the parent class in a more type conscious way.
class Parent {
hello(x: number): number;
}
class Child extends Parent {
hello() {
console.log('hello');
}
}
c = new Child();
c.hello()
Read here for more information:
https://www.typescriptlang.org/docs/handbook/classes.html#abstract-classes

TypeScript: Dynamically declared methods in class

I've some code like:
const methodsList = [
'foo',
'bar',
// ... 20 other items ...
]
export class Relayer {
constructor() {
for (const methodName of methodsList) {
this[methodName] = (...args) => {
// console.log('relaying call to', methodName, args)
// this is same for all methods
}
}
}
}
const relayer = new Relayer()
relayer.foo('asd') // TS error
relayer.bar('jkl', 123) // TS error
Now when I use the class instance, TypeScript complains when I call relayer.foo() or relayer.bar(). To make the code compile, I've to cast it as any or similar.
I've an interface that declares foo, bar and the other methods:
interface MyInterface {
foo: (a: string) => Promise<string>
bar: (b: string, c: number) => Promise<string>
// ... 20 other methods
}
How do I get TypeScript to learn the dynamically declared foo and bar class methods? Can the declare syntax be useful here?
First step is to create a type or interface where when indexed by a value in methodsList, the result will be a function:
// The cast to const changes the type from `string[]` to
// `['foo', 'bar']` (An array of literal string types)
const methodsList = [
'foo',
'bar'
] as const
type HasMethods = { [k in typeof methodsList[number]]: (...args: any[]) => any }
// Or
type MethodNames = typeof methodsList[number] // "foo" | "bar"
// k is either "foo" or "bar", and obj[k] is any function
type HasMethods = { [k in MethodNames]: (...args: any[]) => any }
Then, in the constructor, to be able to assign the keys of methodsList, you can add a type assertion that this is HasMethods:
// General purpose assert function
// If before this, value had type `U`,
// afterwards the type will be `U & T`
declare function assertIs<T>(value: unknown): asserts value is T
class Relayer {
constructor() {
assertIs<HasMethods>(this)
for (const methodName of methodsList) {
// `methodName` has type `"foo" | "bar"`, since
// it's the value of an array with literal type,
// so can index `this` in a type-safe way
this[methodName] = (...args) => {
// ...
}
}
}
}
Now after constructing, you have to cast the type still:
const relayer = new Relayer() as Relayer & HasMethods
relayer.foo('asd')
relayer.bar('jkl', 123)
You can also get rid of the casts when constructed using a factory function:
export class Relayer {
constructor() {
// As above
}
static construct(): Relayer & HasMethods {
return new Relayer() as Relayer & HasMethods
}
}
const relayer = Relayer.construct()
Another way around it is to create a new class and type-assert that new results in a HasMethods object:
class _Relayer {
constructor() {
assertIs<HasMethods>(this)
for (const methodName of methodsList) {
this[methodName] = (...args) => {
// ...
}
}
}
}
export const Relayer = _Relayer as _Relayer & { new (): _Relayer & HasMethods }
const relayer = new Relayer();
relayer.foo('asd')
relayer.bar('jkl', 123)
Or if you are only using new and then methods in methodsList, you can do:
export const Relayer = class Relayer {
constructor() {
assertIs<HasMethods>(this)
for (const methodName of methodsList) {
this[methodName] = (...args) => {
// ...
}
}
}
} as { new (): HasMethods };
You can also use your MyInterface interface instead of HasMethods, skipping the first step. This also gives type-safety in your calls.
Use the following syntax:
export class Relayer {
constructor() {}
public foo(){
// your foo method
this.executedOnEachFunction();
}
public bar(){
// your bar method
this.executedOnEachFunction();
}
executedOnEachFunction(){
// what you want to do everytime
}
}
https://repl.it/repls/LawfulSurprisedMineral
To me, this sounds like a need for an interface.
interface MyInterface {
foo(): void; // or whatever signature/return type you need
bar(): void;
// ... 20 other items ...
}
export class Relayer implements MyInterface {
constructor() {}
foo(): void {
// whatever you want foo to do
}
// ... the rest of your interface implementation
}
What it looks like you are doing is implementing some interface of sorts. In your constructor you are defining what the method implementations are instead of defining them in the class body. Might help to read Class Type Interfaces

Categories

Resources