Is it possible to have something like this?
{
roles: contains "admin",
admin_details: string
} && {
roles: contains "vendor",
vendor_details: string
}
?
I figure probably not. What would be the cleanest way to get this sort of behavior, where a type has details depending on what roles they have?
The only way I could think of it was having something like:
({
is_admin: true,
admin_details: string
} |
{ is_admin: false,
admin_details: undefined
})
&& ({
is_vendor: true,
vendor_details: string
} |
{ is_vendor: false,
vendor_details: undefined
});
And adding those additional is_role field on role creation/change. But this is pretty verbose. Is it a good solution, or would you recommend something else?
Thanks!
edit: maybe a union between Admin and Vendor interfaces, with a type guard that examines array membership would be cleaner?
edit: I attempted to use the code in the first answer, but am having issues. See the updated playground here
another edit (too long for comments): playground here for failure when adding a third type to the RolesMap when using the Type Guard solution, see line 35
CodePudding user response:
We could generate a union of all possible combinations which would look like this in the end:
{
roles: "admin"[];
admin_details: string;
} | {
roles: "vendor"[];
vendor_details: string;
} | {
roles: ("admin" | "vendor")[];
admin_details: string;
vendor_details: string;
}
To make things simple later on, we need a mapping between the role
and its associated properties and types.
type RoleMap = {
admin: { admin_details: string }
vendor: { vendor_details: string }
}
To create this union, we need to generate all combinations of the roles
tuple.
type AllCombinations<T extends string> = {
[K in T]: K[] | AllCombinations<Exclude<T, K>> extends infer U extends any[]
? U extends any ?
(K | U[number])[]
: never
: never
}[T]
type T = AllCombinations<keyof RoleMap>
// type T = "admin"[] | "vendor"[] | ("admin" | "vendor")[]
Now we can create the Roles
union by distributing the permutation union and by creating a mapped type where each element of the roles
tuple is mapped to its corresponding type in the RoleMap
.
type UnionToIntersection<U> =
(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
type Roles = AllCombinations<keyof RoleMap> extends infer U extends string[]
? U extends []
? never
: ({
roles: U
} & UnionToIntersection<{
[K in U[number]]: RoleMap[K & keyof RoleMap]
}[U[number]]>) extends infer O ? { [K in keyof O]: O[K] } : never
: never
The resulting type passes these simple test cases.
const a: Roles = {
roles: ["admin"],
admin_details: "0"
}
const b: Roles = {
roles: ["vendor"],
vendor_details: "0"
}
const c: Roles = {
roles: ["vendor", "admin"],
vendor_details: "0",
admin_details: "0"
}
We can discriminate these union elements with a type guard.
type ExtractRoles<T extends { roles: any[] }> = Roles extends infer U extends { roles: any[] }
? U extends any
? T["roles"] extends U["roles"]
? U
: never
: never
: never
function isRole<K extends keyof RoleMap>(roles: K[], obj: Roles): obj is ExtractRoles<{ roles: K[] }> {
return (obj as any).roles.sort().join(',')=== roles.sort().join(',')
}
if (isRole(["admin"], b)) {
b.admin_details
}
if (isRole(["vendor"], b)) {
b.vendor_details
}
if (isRole(["vendor", "admin"], b)) {
b.vendor_details
b.admin_details
}
CodePudding user response:
This examle matches your interface
// define all possible roles
const roles = ["admin", "vendor", "foo", "bar"] as const
type AllRoles = typeof roles
type RoleEnum = AllRoles[number] //"admin"| "vendor"| "foo"| "bar"
type RoleFlags<Role extends RoleEnum = RoleEnum> = `is_${Role}`
type RoleContextKeys<Role extends RoleEnum = RoleEnum> = `${Role}_details`
// flag key to context key and vice versa
type RoleFlagToContextKey<R extends RoleFlags> = R extends `is_${infer Role extends RoleEnum}` ? RoleContextKeys<Role> : never
type ContextKeyToRoleFlag<R extends RoleContextKeys> = R extends `${infer Role extends RoleEnum}_details` ? RoleFlags<Role> : never
//type constraint for CreateRole<~>
type RoleInput = { [K in RoleFlags<RoleEnum>]?: boolean }
// wraps role input with context
type CreateRole<T extends RoleInput> =
keyof T extends RoleFlags ?
{ [K in RoleFlagToContextKey<keyof T>]:
T[ContextKeyToRoleFlag<K>] extends true
? string
: undefined
} & T : never
type A = CreateRole<{ "is_admin": true, "is_vendor": false }> //{admin_details: string; vendor_details: undefined; } & { is_admin: true; is_vendor: false;}
type B = CreateRole<{ "is_admin": true, "is_foo": false }> //{admin_details: string; foo_details: undefined; } & { is_admin: true; is_foo: false;}
type C = CreateRole<{ "is_vendor": true, "is_vendor": false }> // error
type D = CreateRole<{ "is_bar": true, "is_foo": false }> //{bar_details: string; foo_details: undefined; } & { is_bar: true; is_foo: false;}