question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Typescript decorators

See original GitHub issue

Easy-peasy provides some nice abstractions over redux boilerplate but I think Typescript usage can be varied with the help of decorators. During my current project, I created a basic decorator implementation which simply collects some metada from class models and creates an easy-peasy store. I have yet to find any opportunity to use easy-peasy extensively since it takes a rather humble part of my project, still it did great for simple use cases with decorators. A full implementation -if possible- could be very useful.

Edit: Of course there are might be pitfalls in typings because my implementation is rather shallow.

example.store.ts

import { Computed, createStore, createTypedHooks, Model, Thunk } from "./easy-peasy-decorators";

@Model("counter")
export class CounterModel {
    public val = 0;

    public get nextCount(): Computed<number> {
        return this.val + 1;
    }

    @Thunk
    public async incrementThunk() {
        this.increment();
    }

    public increment() {
        ++this.val;
    }

    public reset() {
        this.val = 0;
    }
}

interface IStoreModel {
    counter: CounterModel;
}

const store = createStore<IStoreModel>();

easy-peasy.decorators.ts

import * as easyPeasy from "easy-peasy";

const model: any = {};
const instances: Record<string, any> = {};
const listeners: Record<string, Record<string, easyPeasy.TargetResolver<any, any>>> = {};
const thunks: Record<string, string[]> = {};
let store: easyPeasy.Store;

export function Model(modelName: string) {
    return (ctor: any) => {
        instances[modelName] = new ctor();
        model[modelName] = {};
        listeners[modelName] = {};
        thunks[modelName] = [];

        addState(modelName);
        addActionsAndComputeds(ctor.name, modelName);
    };
}

function addState(modelName: string) {
    const properties = Object.keys(instances[modelName]);

    properties.forEach(property => {
        const initialVal = instances[modelName][property];

        model[modelName][property] = initialVal;
    });
}

function addActionsAndComputeds(ctorName: string, modelName: string) {
    const prototype = instances[modelName].constructor.prototype;
    const descriptors = Object.getOwnPropertyDescriptors(prototype as object);

    Object.entries(descriptors)
        .filter(([methodName, desc]) => methodName !== "constructor")
        .forEach(([methodName, desc]) => {
            const { value, get } = desc;

            if (listeners[ctorName]?.[methodName]) {
                model[modelName][methodName] = easyPeasy.actionOn(
                    listeners[ctorName][methodName],
                    (state, target) => {
                        value.call(state, target);
                    },
                );
            } else if (thunks[ctorName] && thunks[ctorName].includes(methodName)) {
                model[modelName][methodName] = easyPeasy.thunk((actions, payload, { getState }) => {
                    value.call({ ...getState(), ...actions }, payload);
                });
            } else if (value) {
                model[modelName][methodName] = easyPeasy.action((state, payload) => {
                    value.call(state, payload);
                });
            } else if (get) {
                model[modelName][methodName] = easyPeasy.computed(state => {
                    return get.call(state);
                });
            }
        });
}

export function Listener<Model extends object, StoreModel extends object = {}>(
    actionFn: easyPeasy.TargetResolver<ToStoreType<Model>, ToStoreType<StoreModel>>,
) {
    return (ctor: any, methodName: string) => {
        listeners[ctor.constructor.name] = listeners[ctor.constructor.name] || {};
        listeners[ctor.constructor.name][methodName] = actionFn;
    };
}

export function Thunk(ctor: any, methodName: string) {
    thunks[ctor.constructor.name] = thunks[ctor.constructor.name] || [];
    thunks[ctor.constructor.name].push(methodName);
}

type ToStoreType<T extends object> = {
    [P in keyof T]: "computed" extends keyof T[P]
        ? T[P] extends Computed<infer U>
            ? U
            : T[P]
        : T[P] extends (...args: any[]) => any
        ? easyPeasy.Action<T, Parameters<T[P]>[0]>
        : T[P] extends object
        ? ToStoreType<T[P]>
        : T[P];
};

export function createStore<T extends object>() {
    store = easyPeasy.createStore<any>(model);

    return store as easyPeasy.Store<ToStoreType<T>>;
}

export function createTypedHooks<Model extends object>(): {
    useStoreActions: <Result>(
        mapActions: (actions: easyPeasy.Actions<ToStoreType<Model>>) => Result,
    ) => Result;
    useStoreDispatch: () => easyPeasy.Dispatch<ToStoreType<Model>>;
    useStoreState: <Result>(
        mapState: (state: ToStoreType<Model>) => Result,
        dependencies?: any[],
    ) => Result;
} {
    const hooks = easyPeasy.createTypedHooks<any>();

    return hooks as any;
}

export type Computed<T> = T & { computed?: undefined };

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:5
  • Comments:14 (6 by maintainers)

github_iconTop GitHub Comments

1reaction
CyriacBrcommented, Apr 15, 2020

@ctrlplusb Good to know! I got derailed from my initial plan and I’m not sure I’m going to use easy-peasy that way anymore, but I’m not sure yet. I initially wanted to leverage native web components to build framework agnostic libs, and I would have loved to use EP for the native implementation and framework layers.
I’m going to keep an eye on the plugin architecture. If it gets really easy to implement what I had in mind I wouldn’t mind pursuing what I started.
Good luck with that!

1reaction
ctrlplusbcommented, Apr 15, 2020

FYI, I have just added this to the docs for the v3.4.0 release.

@CyriacBr - when I have completed the plugin architecture refactor it should be way easier to adapt Easy Peasy to other frameworks.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Documentation - Decorators - TypeScript
A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use...
Read more >
How To Use Decorators in TypeScript - DigitalOcean
This tutorial will show you how create your own decorators in TypeScript for classes and class members, and also how to use them....
Read more >
A practical guide to TypeScript decorators - LogRocket Blog
In TypeScript, decorators are functions that can be attached to classes and their members, such as methods and properties.
Read more >
TypeScript Decorators - Javatpoint
A Decorator is a special kind of declaration that can be applied to classes, methods, accessor, property, or parameter. Decorators are simply functions...
Read more >
TypeScript Decorators Examples - GitHub Gist
TypeScript Decorators Examples. GitHub Gist: instantly share code, notes, and snippets.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found