Home > Back-end >  Define a nested Record type that narrows as you traverse
Define a nested Record type that narrows as you traverse

Time:12-22

My system has permissions defined as a string[] like so

const stringVals = [
  'create:user',
  'update:user',
  'delete:user',
  'create:document',
  'update:document',
  'delete:document',
  'delete:workflow',
  'run:workflow',
] as const

I have the following types

type StringVals = typeof stringVals[number]
type PermissionVerb<T extends string> = T extends `${infer Action}:${string}` ? Action : never
type PermissionKey<T extends string, V extends PermissionVerb<T>> = T extends `${V}:${infer Key}` ? Key : never

which work great for things like

type Verbs = PermissionVerbs<StringVals> // 'create' | 'update' | 'delete' | 'run'
type CreateKeys = PermissionKey<StringVals, 'create'> // 'user' | 'document'
type RunKeys = PermissionKey<StringVals, 'run'> // 'workflow'

I'm hoping to create a single permission object that would look like this

{
  create: {
    user: true,
    document: true
  },
  update: {
    user: true,
    document: true
  },
  delete: {
    user: true,
    document: true,
    workflow: true,
  },
  run: {
    workflow: true,
  }
}

It's defining the right type for this object that I'm struggling with.

type PermissionsObject<T extends string> = Record<PermissionVerb<T>, Record<PermissionKey<T, PermissionVerb<T>>, boolean>>

function createPermissionsObject<T extends string>(permissions: Readonly<T[]>): PermissionsObject<T> {
  throw 'not implemented'
}

The problem with this is the first Record doesn't narrow the inner Record. So outer object key of run, returns everything, not just workflow like I want.

const permissions = createPermissionsObject(stringVals)
permissions.run.user // expect error

typescript playground

CodePudding user response:

We have to use a mapped type so that each key K can be used to compute its corresponding type.

type PermissionsObject<T extends string> = {
  [K in PermissionVerb<T>]: 
    Extract<T, `${K}${string}`> extends `${string}:${infer Key}`
      ? Record<Key, boolean>
      : never
}

K can be used with Extract to get the full path from T where K is the Action. We can now infer the Key from this result and use it in Record<Key, boolean>.

const permissions = createPermissionsObject(stringVals)
permissions.run.workflow
permissions.create.user

permissions.run.user
//              ^^^^ Property 'user' does not exist on type 
//                   'Record<"workflow", boolean>'

Playground


Bonus:

A few characters shorter but same result.

type PermissionsObject<T extends string> = {
  [K in T as PermissionVerb<K>]: 
    [K] extends [`${string}:${infer Key}`]
      ? Record<Key, boolean>
      : never
}
  • Related