如何在TypeScript的嵌套对象中键入具有函数参数类型的函数参数?

时间:2019-05-13 21:14:06

标签: typescript

我正在尝试键入一个函数,该函数应从嵌套对象中的值推断出参数类型。如何推断深层嵌套对象内部的函数参数类型?

示例:

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

给定特定的roleability,我想在data中键入can()作为ABILITIES中定义的函数参数类型(如果存在)。如果它不存在,那么我就不需要data

当角色为data而能力为{ currentUserId: string; userId: string }时,我希望Role.USER的类型是'users:edit'的必需类型。

1 个答案:

答案 0 :(得分:2)

您可以做一些条件类型魔术来提取适当的参数类型,只要ABILITIES是用对象文字的实际类型(不仅仅是Abilities)键入的。我们可以使用额外的功能来帮助编译器推断正确的类型。

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

function createAbilities<A extends Abilities>(a: A) {
    return a;
}
export const ABILITIES = createAbilities({
    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'],
    },
});
type ExtractDynamicParameter<R extends Role, A extends Ability> = typeof ABILITIES[R] extends { dynamic: Record<A, (p: infer P) => boolean> } ? { data : P } : { data?: undefined}
export const can = <R extends Role, A extends Ability>({
    role,
    ability,
    data,
}: {
    role: R;
    ability: A;
} & ExtractDynamicParameter<R, A>): boolean => {
    const permissions = ABILITIES[role as Role] as Abilities[Role]; // Needed assertions 

    // 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;
};

can({ role: Role.USER, ability: "users:edit", data: { currentUserId: "", userId: "" } }) // ok 
can({ role: Role.USER, ability: "users:edit", data: {} }) // err
can({ role: Role.USER, ability: "users:edit" }) // err