So I'll try to explain a little better here.
TypeScript vs JavaScript
When using TypeScript you have to remember almost nothing of it remains at runtime. So you can define all your pretty types and generics, but they're gone when you run the code!
Only thing you can stick to are Classes. Those are mapped down to JavaScript, but even in this case the "smart casting" is gone, so:
class MyClass {
constructor(readonly value: string){};
}
// the definitions below both work in TS
const correctInstantiation = new MyClass('the value');
const wrongInstantiation = { value: 'the other value' };
console.log(correctInstantiation instanceof MyClass); // this logs true
console.log(wrongInstantiation instanceof MyClass); // this logs false
As you can see I defined the class with a constructor and added readonly
(but you can use public
and private
as well), this way the constructor parameters work as class fields and you can pass them directly when you create the instance.
Instanceof Generics
This is something you can't do even in fully typed languages like Java.
Since at runtime the Generic is gone, you need to pass the actual type somehow explicitly, so:
// this just won't compile
function wrongFunction<T>(payload: T) {
return payload instanceof T; // can't do this!!
}
Wrap it up
So the only solution I can think of for your case is to define Actions
within a wrapper that also states the payload
type like:
class FunnyPayload {
constructor(readonly pun: string) {}
}
class BoringPayload {
constructor(readonly remark: string) {}
}
type ActionDefinition<PayloadType, OutputType> = {
payloadType: any,
action: Action<PayloadType, OutputType>,
}
const actionsDictionary = {
be_funny: {
payloadType: FunnyPayload,
action: (payload: FunnyPayload) => {
console.log(`Funny Guys says: ${payload.pun}`);
console.log(`ONG so funny!`);
}
},
be_boring: {
payloadType: BoringPayload,
action: (payload: BoringPayload) => {
console.log(`Boring Guys says: ${payload.remark}`);
console.log(`ONG so boring!`);
}
},
} as { [key: string]: ActionDefinition<any, any> };
Here I created specific classes for each possible payload
(you can also use raw types such as number
and string
, but for more complex objects you need a class, remember the smart casting issue!)
Then if you want to generate a list of Action
s with payload check you can do:
const generatedActions: Action<any, any>[] = Object.keys(actionsDictionary).map(key => {
const definition = actionsDictionary[key]
return (payload: any) => {
if(!(payload instanceof definition.payloadType)) throw new Error(`Payload mismatch!`);
return definition.action(payload);
}
});
Testing it
Now you'll have the fallowing scenario:
generatedActions[0](new FunnyPayload('** something silly! **')); // works
generatedActions[1](new BoringPayload('** something serious... **')); // works
generatedActions[0](new BoringPayload('** something serious... **')); // nope! wrong playload!
generatedActions[0]({ pun: '** something hilarious! **' }); // nope! no instantiation!
Here is the working example
Finally just a personal taste hint: when you define Generics, just give them a more explanatory name like PayloadType
and OutputType
so you'll always know what you're referring to, plain letters like T
, V
etc. get confusing the more generics you add!