piotrwitek / typesafe-actions

Typesafe utilities for "action-creators" in Redux / Flux Architecture
https://codesandbox.io/s/github/piotrwitek/typesafe-actions/tree/master/codesandbox
MIT License
2.41k stars 99 forks source link

Digged into the code, and tried to find the "magic" behind string variable to string literal code #180

Closed jarlah closed 4 years ago

jarlah commented 4 years ago

I have looked into the code, and i am trying to learn how this thing works. Its a lot of type waving i can see. You might say that i have tried to dissect this library and pluck out the beating heart. Which i succeeded with btw.

type TypeConstant = string;

export type Action<TType extends TypeConstant = TypeConstant> = {
    type: TType;
};

type ActionCreator<TType extends TypeConstant> = (
    ...args: any[]
) => Action<TType>;

type PayloadAction<TType extends TypeConstant, TPayload> = {
    type: TType;
    payload: TPayload;
};

type EmptyAction<TType extends TypeConstant> = {
    type: TType;
};

type EmptyAC<TType extends TypeConstant> = () => EmptyAction<TType>;

type PayloadMetaAction<TType extends TypeConstant, TPayload, TMeta> = {
    type: TType;
    payload: TPayload;
    meta: TMeta;
};

type PayloadMetaAC<TType extends TypeConstant, TPayload, TMeta> = (
    payload: TPayload,
    meta: TMeta
) => PayloadMetaAction<TType, TPayload, TMeta>;

type PayloadAC<TType extends TypeConstant, TPayload> = (
    payload: TPayload
) => PayloadAction<TType, TPayload>;

type ActionBuilderConstructor<TType extends TypeConstant,
    TPayload extends any = undefined,
    TMeta extends any = undefined> = [TMeta] extends [undefined]
    ? [TPayload] extends [undefined]
        ? unknown extends TPayload
            ? PayloadAC<TType, TPayload>
            : unknown extends TMeta
                ? PayloadMetaAC<TType, TPayload, TMeta>
                : EmptyAC<TType>
        : PayloadAC<TType, TPayload>
    : PayloadMetaAC<TType, TPayload, TMeta>;

interface ActionBuilder<T extends TypeConstant> {
    <P = undefined, M = undefined>(): ActionBuilderConstructor<T, P, M>;
}

function createCustomAction<T extends TypeConstant,
    AC extends ActionCreator<T> = () => { type: T }>(type: T, createHandler?: (type: T) => AC): AC {
    return createHandler != null ? createHandler(type) : ((() => ({type})) as AC);
}

function createStandardAction<T extends TypeConstant>(type: T): ActionBuilder<T> {
    return <P, M = undefined>(): ActionBuilderConstructor<T, P, M> => {
        return createCustomAction(type, _type => (payload: P, meta: M) => ({
            type: _type,
            payload,
            meta,
        })) as ActionBuilderConstructor<T, P, M>;
    }
}

const dddsd = createStandardAction("sdsada")<string>();

const action = dddsd("sdds");

type DD = typeof dddsd
// type DD: { type: "sdsada" }

switch (action.type) {
    case "sdsada":
        break;
    default:
        break;
}

I really believes the greatest magic lies in

type ActionBuilderConstructor<TType extends TypeConstant,
    TPayload extends any = undefined,
    TMeta extends any = undefined> = [TMeta] extends [undefined]
    ? [TPayload] extends [undefined]
        ? unknown extends TPayload
            ? PayloadAC<TType, TPayload>
            : unknown extends TMeta
                ? PayloadMetaAC<TType, TPayload, TMeta>
                : EmptyAC<TType>
        : PayloadAC<TType, TPayload>
    : PayloadMetaAC<TType, TPayload, TMeta>;

which selects the type of which the code returns for the action. But if I wanted to lets say remove meta from the equation, it would all be a lot more simpler to read and understand. And, I have never used meta information before anyway so.

I am thinking about how this can presented as a way to learn typescript type waving. And i am most propably going to use these conditional type selection mechanism myself. Heck i might also use the above code for fun and profit in micro web framework situations

So, the question is, is this magic meant to be hidden and kept safe, and I should just burn this code and kill myself ? ;D (Kidding course, but anyway, there is nothing that documents this on the great Internet :/ )

jarlah commented 4 years ago

Aha, found it. The beating heart within the beating heart.

type TypeConstant = string;

export function createAction<T extends TypeConstant>(type: T) {
    return <P>() => (p: P) => ({
        type: type,
        payload: p
    })
}

const dddsd = createAction("sdsada")<string>();

const action = dddsd("sdds");

type Bla = ReturnType<typeof dddsd>
// type Bla: { type: "sdsada", payload: string }

type DD = typeof dddsd
// type DD: (p: string) => { type: "sdsada", payload: string }

type nan = typeof action
// type nan: { type: "sdsada", payload: string }

switch (action.type) {
    case "sdsada":
        break;
    default:
        break;
}
jarlah commented 4 years ago

The conclusion, there is no magic ... other than T extends TypeConstant ;) .. everything else is plain code

piotrwitek commented 4 years ago

Hey @jarlah I'm happy to hear you're learning but you're also simply spamming the issues board, there are more appropriate places suggested in the issue templates when creating a new issue so please be respectful and behave correctly.

And yes the simplest form is T extends TypeConstant, but the extra code is needed not only for handling the meta case but also a number of other edge cases and better DX.

jarlah commented 4 years ago

@piotrwitek ok ;) good for me. But when i have said A, i must say B, the following below is what is really needed to have typesafe actions (not the library):

export function createAction<T extends string>(type: T) {
    return <P, M = void>() => (payload: P, meta: M) => ({
        type,
        payload,
        meta
    })
}

const addStuff = createAction("ADD_STUFF")<string>();
const addStuffWithMea = createAction("ADD_STUFF_WITH_META")<string, number>();

type Action =
    ReturnType<typeof addStuff> |
    ReturnType<typeof addStuffWithMea>

function reducer(state: number = 0, action: Action): number {
    switch (action.type) {
        case "ADD_STUFF_WITH_META":
            return state + action.meta;
        case "ADD_STUFF":
            return state + action.payload.length;
        default:
            break;
    }
}

let state: number;

state = reducer(undefined, addStuff("jojo"));
state = reducer(state, addStuffWithMea("hopla", 15));

I am definitely sure that this will help others think carefully about just dragging in wildcard number of npm libraries just to make a simple reducer architecture. Its a sickness that we should need to add package after package to solve simple problems.

Anyway, i hope closed issues are not popping up in other inboxes than yours @piotrwitek

piotrwitek commented 4 years ago

And now try to do:

const addStuff = createAction("ADD_STUFF")(); 

//or
const addStuff = createAction("ADD_STUFF")<undefined>();

addStuff() // Ups there is an error.

Just trying to illustrate there will be more of such simple problems when you'll try to use this snippet across your codebase in different scenarios and with time you'll end up exactly where this library is now.

jarlah commented 4 years ago

Yes, obviously. If you say undefined is type, then you must pass undefined. But i have always used void.

const addStuff = createAction("ADD_STUFF")<void>();
addStuff() // compiles

if i want to pass undefined, its normally to say that i want to allow sending either a type or undefined, lets say setPerson(p: Person | undefined) which will unset person if p is undefined

const addStuff = createAction("ADD_STUFF")<undefined>();
addStuff(undefined); // compiles

type Stuff = string | undefined
const stuff: Stuff = undefined;
const addStuff2 = createAction("ADD_STUFF")<Stuff>();
const action = addStuff2(stuff); // compiles
const length = action.payload.length; // its type can be undefined, Error

Sorry, i am not trying to say that typesafe-actions is useless or bad. I am just trying to find the roots of the project. Somewhere someone like me is wondering where the magic lies. They are just in need of typesafe action creators with typesafe pattern matching of possible types in reducer. I dont even need the getType funtion because the code below creates an ActionType that enforces it for me in the reducer.

export const setStartTime = createAction("SET_START_TIME")<string | undefined>();
export const setEndTime = createAction("SET_END_TIME")<string | undefined>();

export type ActionType =
    ReturnType<typeof setStartTime> |
    ReturnType<typeof setEndTime>

and by creating three lines of single action creators versus five lines (!) of the async creator function, the example says it all. Yes could have onelinered the last example, but then its more unreadable.

export const loadStuffStart = createAction("LOAD_STUFF_START")<void>();
export const loadStuffSuccess = createAction("LOAD_STUFF_SUCCESS")<Array<Stuff>>();
export const loadStuffFailure = createAction("LOAD_STUFF_FAILURE")<Error>();

vs

const loadStuff = createAsyncAction(
    "LOAD_STUFF_START",
    "LOAD_STUFF_SUCCESS",
    "LOAD_STUFF_FAILURE"
)<void, Array<Stuff>, Error>();

To sum up, I am not trying to fuel a discussion with this answer, but merely shed some light on the fact that yes it is actually usable without all the typescript trickery :) My seven lines of code (could have been like three if i really wanted) does the job.

Again, sorry, not directly trying to agro, but i see that i have sort of managed it anyway.

Corrected version of ultra slim function, with support for not supplying types for empty actions.


export function createAction<T extends string>(type: T) {
    return <P = void, M = void>() => (payload: P, meta: M) => ({
        type,
        payload,
        meta
    })
}

And if I wanted to group an object for async actions

const loadAsyncStuff = {
  request: createAction(«start»)(),
  success: createAction(«sucess»)<string>(),
  failure: createAction(«failure»)<Error>()
}

The ongoing theme is type interference.

https://github.com/jarlah/typesafe-action-creator

piotrwitek commented 4 years ago

You know that's fine, I understand if that is your way.

And actually you can still use this popular and battle-tested library using only a partial functionality that you need without any downsides like performance degradation (it is rigorously tested for performance) or increased bundle size as it's tree shakeable and only 1KB gzipped.

But of course, it's your choice.