1
type GlobalState = {
  yourDetails: {
    firstName: string,
    lastName: string,
  }
}

const globalState = {
  yourDetails: {
    firstName: 'test',
    lastName: 'test1',
  }
}

type Action<T> = (state: GlobalState, payload: any) => T;

type Actions<T> = Record<string, Action<T>>;

type ActionOutput<T extends (...arg: any[]) => any> = T extends (arg: any, arg1: infer G) => any ? (payload: G) => any : (payload: any) => any;

type ActionsOutput<T> = Record<keyof T, ActionOutput<T>>; // how to pass acording function type?

function generator<T, G extends Actions<T>>(payload: G): {
  actions: ActionsOutput<G>
} {
  return {
    actions: Object.entries(payload).reduce((prev, [key, callback]) => {
      return {
        ...prev,
        [key]: (actionPayload) => {
          callback(globalState, actionPayload);
        },
      }
    }, {} as ActionsOutput<G>),
  };
}

function updateNameAction(state: GlobalState, payload: {
  firstName: string,
  lastName: string,
}) {
  return {
    ...state,
    yourDetails: {
      ...payload,
    }
  }
}

const { actions } = generator<GlobalState, { updateNameAction: any }>({
  updateNameAction
})

actions.updateNameAction({
  test: '123'
})

How to infer a function's argument. so when the above example's action is invoked it will throw an error when the payload's type is not matched.

working in progress link.

Bill
  • 17,872
  • 19
  • 83
  • 131
  • 1
    COuld you please share reproducable code in TS playground? – captain-yossarian from Ukraine Nov 16 '20 at 08:06
  • 1
    `invoked` and `type` don't go along very well in TypeScript... I mean, when the `action` is invoked TypeScript is no longer there, it's just plain JavaScript – Carlo Moretti Nov 16 '20 at 10:52
  • 1
    that's true... good point @Onheiron I guess the only option is to declare a second generic? – Bill Nov 16 '20 at 11:01
  • There's no clean straight forward way to do this (it's quite the same with Java itself actually). You can't get the type from a generic and just go `instanceof T`... you need to pass the type itself as a parameter, but that's quite bad with TypeScript. only way you can try is actually creating `class`es. TypeScript `class` maps to plain JS `class` and you can call `instanceof` on those, but you'll need to instantiate your classes with a constructor, you can't just go around casting JSON. – Carlo Moretti Nov 16 '20 at 11:20

1 Answers1

1

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 Actions 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!

Carlo Moretti
  • 2,213
  • 2
  • 27
  • 41