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
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>'
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
}