Skip to content

Use template literal type as object key #45541

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
5 tasks done
marceleq27 opened this issue Aug 22, 2021 · 2 comments
Closed
5 tasks done

Use template literal type as object key #45541

marceleq27 opened this issue Aug 22, 2021 · 2 comments
Labels
Duplicate An existing issue was already created

Comments

@marceleq27
Copy link

marceleq27 commented Aug 22, 2021

Suggestion

Use template literal type as a key in object

πŸ” Search Terms

template literal string as object key

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Hello, I have a problem and maybe this thing will be a good feature. I would like to use template literal types as a key in for example interface. Let me show an example.

πŸ“ƒ Motivating Example

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`), 
    }
}
// createActionCreator is a function from 'deox' npm package

I've made a ActionStringType and for example:

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

I was trying to make a action generator in redux and I found a problem where I can't type keys in object that createRequestAction returns. So this type is working fine but TS is still not seeing the names (keys of object) of action creators.

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.

So, that is my idea, idk how to implement this, maybe it's impossible for now, but i think it will be a good feature. Let me know your thoughts, maybe I'm doing something wrong :)

πŸ’» Use Cases

  • creating action generators etc.
  • typing keys in object
@jcalz
Copy link
Contributor

jcalz commented Aug 23, 2021

Looks like a duplicate of #13948 and/or #21030. Your example is kind of complicated for what amounts to a demonstration of using a computed key of a template literal type, but if I use the "unionize" type from here to distribute records over unions, like this:

type DistribRecord<K extends PropertyKey, V> = { [P in K]: { [Q in P]: V } }[K]
const kv = <K extends PropertyKey, V>(k: K, v: V): DistribRecord<K, V> => ({ [k]: v } as any)

Then instead of {[foo]: bar} we can write kv(foo, bar) to get the behavior you want. Your return value would probably look like

  return {
    ...kv(`${actionName}Success`, createActionCreator(`${name}_SUCCESS`)),
    ...kv(`${actionName}Error`, createActionCreator(`${name}_ERROR`)),
    ...kv(`${actionName}Done`, createActionCreator(`${name}_DONE`)),
  }

And then you get

const x = createRequestAction("SET_AS_FAVOURITE")
x.setAsFavouriteDone // ExactActionCreator<"SET_AS_FAVOURITE_DONE", () => { type: "SET_AS_FAVOURITE_DONE"; }> 
x.setAsFavouriteError // ExactActionCreator<"SET_AS_FAVOURITE_ERROR", () => { type: "SET_AS_FAVOURITE_ERROR"; }>

Playground link

@andrewbranch andrewbranch added the Duplicate An existing issue was already created label Aug 23, 2021
@marceleq27
Copy link
Author

Hi, yes, that's that I'am talking about. For now I figured out this problem like this:

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

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

type Output<T extends string> = {
    [key in `${SnakeToCamelCase<T>}${Types}`]: ExactActionCreator<
        T,
        () => {
            type: T
            payload: any
        }
    >
}

And function looks like this:

const createDeoxRequestAction = <T extends string>(name: T): Output<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 SnakeToCamelCase<T>
    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
    return {
        [`${actionName}Success`]: createActionCreator(`${name}_SUCCESS`),
        [`${actionName}Error`]: createActionCreator(`${name}_ERROR`),
        [`${actionName}Done`]: createActionCreator(`${name}_DONE`),
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } as any
}

And that work properly, so if it's duplicated topic - I'm closing :)

Thanks for your help @jcalz ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

3 participants