I'm trying to type a function where an argument type should be inferred from a value in a nested object. How do I infer the function arguments type that is inside a deeply nested object?
Example:
export enum Role {
USER = 'user',
ADMIN = 'admin',
OWNER = 'owner',
PRIMARY_OWNER = 'primaryOwner',
}
// Add as needed. Formatted as 'resource:action'?
export type Ability =
| 'users:create'
| 'users:edit'
| 'reports:view'
| 'settings:view';
type StaticAbilities = readonly Ability[];
type DynamicAbility = (data: any) => boolean;
type DynamicAbilities = { readonly [key in Ability]?: DynamicAbility };
export type Abilities = {
readonly [R in Role]?: {
readonly static?: StaticAbilities;
readonly dynamic?: DynamicAbilities;
}
};
/**
* A configuration object containing allowed abilities for specific roles.
*/
export const ABILITIES: Abilities = {
user: {
dynamic: {
// THIS IS AN EXAMPLE OF DYNAMIC RULES
'users:edit': ({
currentUserId,
userId,
}: {
/** Current users ID */
currentUserId: string;
/** User ID trying to be edited */
userId: string;
}) => {
if (!currentUserId || !userId) return false;
return currentUserId === userId;
},
},
},
admin: {
static: ['reports:view', 'settings:view'],
},
owner: {
static: ['reports:view', 'settings:view'],
},
primaryOwner: {
static: ['reports:view', 'settings:view'],
},
};
export const can = ({
role,
ability,
data,
}: {
role: Role;
ability: Ability;
data?: any;
}): boolean => {
const permissions = ABILITIES[role];
// Return false if role not present in rules.
if (!permissions) {
return false;
}
const staticPermissions = permissions.static;
// Return true if rule is in role's static permissions.
if (staticPermissions && staticPermissions.includes(ability)) {
return true;
}
const dynamicPermissions = permissions.dynamic;
if (dynamicPermissions) {
const permissionCondition = dynamicPermissions[ability];
// No rule was found in dynamic permissions.
if (!permissionCondition) {
return false;
}
return permissionCondition(data);
}
// Default to false.
return false;
};
Given a specific role and ability, I want to type data in can() to be the function arguments type defined in ABILITIES if it exists. If it doesn't exist, then I don't want data to be required.
I expect the type of data to be a required type of { currentUserId: string; userId: string }, when role is Role.USER and ability is 'users:edit'.