Skip to content

Instantly share code, notes, and snippets.

@gund
Created January 16, 2018 11:21
Show Gist options
  • Save gund/a75600ce01952ca19dfbac7e1c8c1275 to your computer and use it in GitHub Desktop.
Save gund/a75600ce01952ca19dfbac7e1c8c1275 to your computer and use it in GitHub Desktop.
Action creator factories made easy with decorators using Typescript

Actions Helpers

This is a draft idea to reduce boilerplate code when writing action creators for redux.

It simplifies the proces to bare minimum and provides shourtcuts for comminly used patterns like async action (where for one action you need to have 2 more that represent success or failure).

Draft

Interface Definitions

export interface NamespacedAsyncActionCreator {
  (namespace: string): AsyncActionCreators;
}

export interface NamespacedActionCreator {
  (namespace: string): ActionCreator;
}

export interface AsyncActionCreators {
  createAsyncAction: <T = any, R = any, D = T, S = R>(
    type: string,
    payloadMainSetter?: PayloadSetter<T | undefined, D>,
    payloadSuccessSetter?: PayloadSetter<R | undefined, S>,
  ) => AsyncActionObj<T, D, R, S>;

  createAsyncActionWithPayload: <T = any, R = any, D = T, S = R>(
    type: string,
    payloadMainSetter?: PayloadSetter<T, D>,
    payloadSuccessSetter?: PayloadSetter<R, S>,
  ) => AsyncActionWithPayloadObj<T, D, R, S>;
}

export interface PayloadSetter<T = any, D = any> {
  (payload: T): D;
}

export interface TypedAction<T> {
  type: string;
  payload: T;
}

export interface AsyncActionObj<T = any, D = T, R = any, S = R> {
  initial: ActionCreator<T, D>;
  success: ActionCreator<R, S>;
  failure: ActionWithPayloadCreator<string>;
}

export interface AsyncActionWithPayloadObj<T = any, D = T, R = any, S = R> {
  initial: ActionWithPayloadCreator<T, D>;
  success: ActionWithPayloadCreator<R, S>;
  failure: ActionWithPayloadCreator<string>;
}

export interface ActionCreator<T = any, D = T> {
  TYPE: string;
  (payload?: T): TypedAction<D>;
}

export declare function getActionCreator<T = any, D = T>(type: string): ActionCreator<T, D>;

export interface ActionWithPayloadCreator<T = any, D = T> {
  TYPE: string;
  (payload: T): TypedAction<D>;
}

declare var getActionCreatorNs: NamespacedActionCreator;
declare var getAsyncActionCreatorNs: NamespacedAsyncActionCreator;

Small string helpers

type TokenizedString = string[];

interface StringTokenizer {
  (string: string): TokenizedString;
}

interface StringDeTokenizer {
  (tokenizedString: TokenizedString): string;
}

interface StringOperation<T = any> {
  (tokenizedString: TokenizedString, extras?: T): TokenizedString;
}

const stringToToken: StringTokenizer = str => str.split('');
const stringFromToken: StringDeTokenizer = token => token.join('');

function compose<T extends Function>(...fns: T[]): T {
  return ((...args) => fns.reduce((res, fn) => fn(...args))) as any;
}

function stringTransformer(...operations: StringOperation[]) {
  return (string: string) => stringFromToken(
    operations.reduce(
      (token, op) => op(token),
      stringToToken(string))
  );
}

const addSpaceToUpperLetter: StringOperation = token => token
  .reduce(
  (newToken, t) => [
    ...newToken,
    ...(t === t.toUpperCase() ? [' ', t] : [t])
  ],
  [] as TokenizedString);

const toFirstUpperCase: StringOperation = token => token
  .reduce(
  (newToken, t, i, token) => [
    ...newToken,
    ...(i === 0
      ? [t.toUpperCase()]
      : t === ' ' && !!token[i + 1]
        ? [' ', token[i + 1].toUpperCase()]
        : [t])
  ],
  [] as TokenizedString)
  .filter((t, i, token) => t === ' ' || token[i - 1] !== ' ');

const spaceToUnderscore: StringOperation = token => token.map(
  t => t === '' ? '_' : t);

const capitalize: StringOperation = token => token.map(t => t.toUpperCase());

const addSpaceToUpperAndFirstUpper = compose(
  addSpaceToUpperLetter,
  toFirstUpperCase);

const strSpacedAndCaps = stringTransformer(addSpaceToUpperAndFirstUpper);

Decorators

function Action(nameSpace: string): PropertyDecorator {
  return (target, propName) => {
    // Convert propName from lowerCamelCase to First Upper Case
    const actionType = strSpacedAndCaps(propName.toString());

    // Append [nameSpace] to converted propName
    const type = `[${nameSpace}] ${actionType}`;

    // Set prop to result of call getActionCreator(convertedPropName)
    if (delete target.constructor[propName]) {
      Object.defineProperty(target.constructor, propName, {
        enumerable: true,
        configurable: true,
        value: getActionCreator(type)
      });
    }
  };
}

function AsyncAction(nameSpace: string): PropertyDecorator {
  return (target, propName) => {
    // Convert propName from lowerCamelCase to First Upper Case
    const actionType = strSpacedAndCaps(propName.toString());

    // Append [nameSpace] to converted propName
    const type = `[${nameSpace}] ${actionType}`;

    // Set prop to result of call createAction(convertedPropName)
    if (delete target.constructor[propName]) {
      Object.defineProperty(target.constructor, propName, {
        enumerable: true,
        configurable: true,
        value: createAsyncAction(type)
      });
    }
  };
}

function createActionNs(namespace: string): () => PropertyDecorator {
  return () => Action(namespace);
}

function createAsyncActionNs(namespace: string): () => PropertyDecorator {
  return () => AsyncAction(namespace);
}

Use-case Example

With decorators

const MyAction = createActionNs('MyNamespace');
const MyAsyncAction = createAsyncActionNs('MyNamespace');

export class MyActions {
  @MyAsyncAction()
  static getApplications: AsyncActionObj<number | undefined, string>;
  
  @MyAction()
  static someSingleAction: ActionCreator; 
}

// These are autogenerated strings of action type
MyActions.getApplications.initial.TYPE; // [MyNamespace] Get Applications
MyActions.getApplications.success.TYPE; // [MyNamespace] Get Applications Success
MyActions.getApplications.failure.TYPE; // [MyNamespace] Get Applications Failure
MyActions.someSingleAction.TYPE;        // [MyNamespace] Some Single Action

// These are action creator functions
MyActions.getApplications.initial();
MyActions.getApplications.success('result');
MyActions.getApplications.failure('error');
MyActions.someSingleAction();

As an action factory functions

const {
  createAsyncAction,
} = getAsyncActionCreatorNs('MyNamespace');

const createAction = getActionCreatorNs('MyNamespace');

export class MyActions2 {
  static getApplications = createAsyncAction<number | undefined, string>('Get Applications');

  static someSingleAction = createAction('Some Single Action');
}

// These are autogenerated strings of action type
MyActions2.getApplications.initial.TYPE; // [MyNamespace] Get Applications
MyActions2.getApplications.success.TYPE; // [MyNamespace] Get Applications Success
MyActions2.getApplications.failure.TYPE; // [MyNamespace] Get Applications Failure
MyActions2.someSingleAction.TYPE;        // [MyNamespace] Some Single Action

// These are action creator functions
MyActions2.getApplications.initial();
MyActions2.getApplications.success('result');
MyActions2.getApplications.failure('error');
MyActions2.someSingleAction();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment