the-dr-lazy / deox

Functional Type-safe Flux Standard Utilities
https://deox.js.org
MIT License
206 stars 12 forks source link

Is there a way to create request action generator? #176

Closed marceleq27 closed 3 years ago

marceleq27 commented 3 years ago

Hello, I want to generate a few side actions and I wrote a generator function but TS still complaining :) Here is a small example:

const createRequestAction = (name: string) => {
    const actionName = name
        .split("_")
        .map((item, i) => {
            if (i === 0) {
                return item.toLowerCase()
            }
            return item.charAt(0).toUpperCase() + item.slice(1).toLowerCase()
        })
        .join("")
    return {
        [`${actionName}Success`]: createActionCreator(`${name}_SUCCESS`),
        [`${actionName}Error`]: createActionCreator(`${name}_ERROR`),
        [`${actionName}Done`]: createActionCreator(`${name}_DONE`),
    }
}

For example: createRequestAction("SET_AS_FAVOURITE") generate object with { setAsFavouriteSuccess: ActionType, setAsFavouriteError: ActionType, setAsFavouriteDone: ActionType }

and I'm exporting actions like this:

const setAsFavourite = createActionCreator("SET_AS_FAVOURITE", (resolve) => (id: string, isFavourite: boolean) => resolve({ id, isFavourite }))

const exampleActions = {
    setAsFavourite,
    ...createRequestAction("SET_AS_FAVOURITE"),
}

export default exampleActions

And now in reducer, TS doesn't recognize names like actions.setAsFavouriteSuccess, is there a way to type this function correctly?

Thanks :)

the-dr-lazy commented 3 years ago

Hi, The problem arise from conversion of name to actionName. As you see the type of name and actionName is string not a literal string type. You need something like this:

function createRequestAction<T extends string>(name: T) {
  const actionName: F<T> = ...

  return {
        [`${actionName}Success`]: createActionCreator(`${name}_SUCCESS`),
        [`${actionName}Error`]: createActionCreator(`${name}_ERROR`),
        [`${actionName}Done`]: createActionCreator(`${name}_DONE`),
    }
}

The F type level function converts input literal type to your desired output literal type.

marceleq27 commented 3 years ago

Hi, thank you for your suggestions, I've tried to type this but it still doesn't work and I don't know how to fix this

type ActionStringType<S extends string, D extends string> = string extends S
    ? ""
    : S extends ""
    ? ""
    : S extends `${infer T}${D}${infer U}`
    ? `${Lowercase<T>}${Capitalize<ActionStringType<U, D>>}`
    : Capitalize<Lowercase<S>>
const createRequestAction = <T extends string>(name: T) => {
    const actionName = name
        .split("_")
        .map((item, i) => {
            if (i === 0) {
                return item.toLowerCase()
            }
            return item.charAt(0).toUpperCase() + item.slice(1).toLowerCase()
        })
        .join("") as ActionStringType<T, "_">
    return {
        [`${actionName}Success`]: createActionCreator(`${name}_SUCCESS`),
        [`${actionName}Error`]: createActionCreator(`${name}_ERROR`),
        [`${actionName}Done`]: createActionCreator(`${name}_DONE`),
    }
}

I've made a ActionStringType and for example:

type S1 = ActionStringType<"SET_AS_FAVOURITE">
// returns "setAsFavourite" which is good

So this type is working fine but TS is still not seeing the names (keys of object) of action creators.

marceleq27 commented 3 years ago

And for example now something like ...createRequestAction("SET_AS_FAVOURITE") returns:

const createRequestAction: <"SET_AS_FAVOURITE">(name: "SET_AS_FAVOURITE") => {
    [x: string]: ExactActionCreator<"SET_AS_FAVOURITE_SUCCESS", () => {
        type: "SET_AS_FAVOURITE_SUCCESS";
    }> | ExactActionCreator<"SET_AS_FAVOURITE_ERROR", () => {
        ...;
    }> | ExactActionCreator<...>;
}

I want to change this [x: string] to something like S1 type returns

the-dr-lazy commented 3 years ago

Unfortunately, TS doesn't have a nice sync between type-level and term-level, which will becomes its biggest regret soon. :) So in simple terms, the compiler is dumb and the good news is that you as a human being are smart. Just shut the compiler mouth up with any type and calculate the output type on your own bare foot. (SPOILER ALERT! Maintenance burden; but it works!)

function f<a extends string>(a: a): Output<a> {
  // ...

  return { ... } as any
}
marceleq27 commented 3 years ago

Hmm, i think it won't work because I can't use ActionStringType type as a value in object, I mean can't do something like this:

type Output<T> = {
    [key: `${ActionStringType}Success`<T, "_"> ]: AnyAction
    [key: `${ActionStringType}Error`<T, "_"> ]: AnyAction
    [key: `${ActionStringType}Done`<T, "_"> ]: AnyAction
}

Please attach more code if you have some idea, i can test it ;)

the-dr-lazy commented 3 years ago
type Types = "Success" | "Error" | "Done"

type Output<a> = {
    [key in `${SnakeCaseToCamelCase<a>}${Types}`]: AnyAction
}
marceleq27 commented 3 years ago

I figured it out and thats work :)

type Types = "Success" | "Error" | "Done"

type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}` ? `${Lowercase<T>}${Capitalize<Lowercase<SnakeToCamelCase<U>>>}` : S

type Output<T extends string> = {
    [key in `${SnakeToCamelCase<T>}${Types}`]: AnyAction
}

I've attached code, maybe somebody also need it. Thank you very much again for help :)