2

I am trying to create a type from a readonly object, i.e.

const Actions = {

  'user.crud': ['user.create', 'user.read', 'user.update', 'user.delete'],

} as const

type ActionsType = keyof typeof Actions | typeof Actions[keyof typeof Actions][number]

The above works nicely and sets up the type ActionsType to the string literals ('user.crud', 'user.create', etc..).

However, the Actions object above is very simplistic, instead, I really need to generate the Actions via functions. When I port the above over to being generated by a function, i.e.

// set up a function to generate all actions for the passed role
function getActions (role: string): Record<string, string[]> {

    return {
        [`${role}.crud`]: [`${role}.create`, `${role}.read`, `${role}.update`, `${role}.delete`],
    }

}

// generate the Actions from a function
const ActionsFromFunction = {

  ...getActions('user'),

} as const

// set up the Actions from a readonly object with values generated by getActions()
type ActionsFromFunctionType = keyof typeof ActionsFromFunction | typeof ActionsFromFunction[keyof typeof ActionsFromFunction][number]

the type ActionsFromFunctionType is no longer set to the string literals. Instead it is set to: string | number and in turn type tests fail as any string is accepted.

I've put together a demo of the above:

Playground

Is there a way of generating the Actions object via a function, whilst still maintaining the string literals within the type?

0

2 Answers 2

2

Your goal is only achievable through typescript Template literal types. They are not supported in typescript 4.0, but will be available in 4.1 version.

This is how you could do it with typescript 4.1

type CrudOperations<ROLE extends string> = [`${ROLE}.create`, `${ROLE}.read`, `${ROLE}.update`, `${ROLE}.delete`];

type GetActionsResult<ROLE extends string> = string extends ROLE // check if we can infer type
  ? { [k: string]: string[] } // if type is not inferable
  : { [K in `${ROLE}.crud`]: CrudOperations<ROLE> };

function getActions<ROLE extends string>(role: ROLE): GetActionsResult<ROLE> {
    return {
      [`${role}.crud`]: [`${role}.create`, `${role}.read`, `${role}.update`, `${role}.delete`]
    } as GetActionsResult<ROLE>;
}

// falls back to { string: string[] } structure
const actions = getActions('admin' as string);

// generate the Actions from a function
const ActionsFromFunction = {
  ...getActions('user'),
  ...getActions('orders'),
}

Playground link

Sign up to request clarification or add additional context in comments.

Comments

1

You can also write getActions without explicit types by inference:

function getActions<K extends string>(role: K) {
  const actions = {
    [`${role}.crud`]: [`${role}.create`, `${role}.read`, `${role}.update`,
    `${role}.delete`],
  } as const
  return actions as Record<K,typeof actions[K]>
}
Test it:
const ActionsFromFunction = {
  ...getActions('user'),
} 
// { user: readonly ["user.create", "user.read", "user.update", "user.delete"];}

const ActionsFromFunction2 = {
  ...getActions('user' as string),
} 
// { [x: string]: readonly [`${string}.create`, ..., `${string}.delete`]; }

type ActionsFromFunctionType =
  | keyof typeof ActionsFromFunction
  | typeof ActionsFromFunction[keyof typeof ActionsFromFunction][number]
// "user" | "user.create" | "user.read" | "user.update" | "user.delete"

Note, that getActions won't be accurate, if your role has a union string type:

const ActionsFromFunction3 = {
  ...getActions('user' as 'user' | 'admin'),
} // { user: ...; admin: ...; }

You might write getActions as distributed conditional type, if needed:

return actions as K extends any ? Record<K,typeof actions[K]> : never
const ActionsFromFunction3 = {
  ...getActions('user' as 'user' | 'admin'),
}
/* 
| { user: readonly ["user.create", "user.read", "user.update", "user.delete"];} 
| { admin: readonly ["admin.create", "admin.read", "admin.update", "admin.delete"]; } 
*/

Playground

2 Comments

like that this is pretty clean, am I right in thinking this is something for future as it doesn't seem to work outside the nightly 4.2 builds?
This is already possible with upcoming TS 4.1 (today/tomorrow release?). You currently need to use the Nightly Playground, as TS 4.1 beta didn't include some narrowing features for string literals yet. I just tested locally with TS 4.1 RC - 👌.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.