简体   繁体   中英

Destructuring a generic type

I have a function where some of the input's properties depend on the value of one of its props. So I've defined a type as follow, and it works as expected:

enum OrganizationPermission {
    UPDATE = 'organization:update',
    INVITE = 'organization:invite',
}

enum WorkspacePermission {
    UPDATE = 'workspace:update',
    INVITE = 'workspace:invite',
}

enum OrganizationRole {
    MANAGER = 'manager'
}

enum WorkspaceRole {
    MANAGER = 'manager'
}

type RolePermission = {
    [OrganizationPermission.UPDATE]: {
        organizationRole: OrganizationRole,
    },
    [OrganizationPermission.INVITE]: {
        organizationRole: OrganizationRole,
    },
    [WorkspacePermission.UPDATE]: {
        workspaceRole: WorkspaceRole,
    },
}

type Permissions = WorkspacePermission | OrganizationPermission;

type CheckPermissionsArgs<P extends Permissions> = {
    perform: P,
    sustain?: boolean,
} & (P extends keyof RolePermission ? RolePermission[P] : Record<string, never>);

function checkPermissions<P extends Permissions>(props: CheckPermissionsArgs<P>): void {
    // omitted for brevity
}

The problem arises when I have to extend this props, like in the following example:

type CheckRolePermissionsArgs<P extends Permissions> = CheckPermissionsArgs<P> & {
    role: string[],
}

function checkRolePermissions<P extends Permissions>({role, ...props}: CheckRolePermissionsArgs<P>): void {
    // Typing error
    checkPermissions(props);
}

Typescript complains that:

TS2345: Argument of type 'Pick<CheckRolePermissionsArgs

, "perform" | "sustain" | Exclude<keyof (P extends OrganizationPermission | WorkspacePermission.UPDATE? RolePermission[P]: Record<...>), "role">>' is not assignable to parameter of type 'CheckPermissionsArgs<CheckRolePermissionsArgs

["perform"]>'. Type 'Pick<CheckRolePermissionsArgs

, "perform" | "sustain" | Exclude<keyof (P extends OrganizationPermission | WorkspacePermission.UPDATE? RolePermission[P]: Record<...>), "role">>' is not assignable to type 'CheckRolePermissionsArgs

["perform"] extends OrganizationPermission | WorkspacePermission.UPDATE? RolePermission[CheckRolePermissionsArgs<...>["perform"]]: Record<...>'

How can I refactor these typings to avoid casting the props argument in the second example?

I think this is because of contravariance , but it is only my guess. Feel free to criticize me.

See this answer.

Consider next example:


/**
 * VARIANCE
 */
declare var y: Omit<CheckRolePermissionsArgs<WorkspacePermission.UPDATE>, 'role'>

let x: CheckPermissionsArgs<WorkspacePermission.UPDATE> = y // ok
y = x // ok
checkPermissions(x)

Since, checkRolePermissions is higher order function:

function checkRolePermissions<P extends Permissions_>(props: CheckRolePermissionsArgs<P>): void {
  // Typing error
  const { role, ...rest } = props
  type Keys = keyof typeof rest // "perform" | "sustain" | Exclude<keyof (P extends keyof RolePermission ? RolePermission[P] : Record<string, never>), "role">
  checkPermissions(rest);
}

TS is unable to figure out rest type because it is kinda dynamic type in this place. It is only known in runtime.

I think the best solution here to split CheckPermissionsArgs type.

enum OrganizationPermission {
  UPDATE = 'organization:update',
  INVITE = 'organization:invite',

}

enum WorkspacePermission {
  UPDATE = 'workspace:update',
  INVITE = 'workspace:invite',
}


enum OrganizationRole {
  MANAGER = 'manager'
}

enum WorkspaceRole {
  MANAGER = 'manager'
}

type RolePermission = {
  [OrganizationPermission.UPDATE]: {
    organizationRole: OrganizationRole,
  },
  [OrganizationPermission.INVITE]: {
    organizationRole: OrganizationRole,
  },
  [WorkspacePermission.UPDATE]: {
    workspaceRole: WorkspaceRole,
  }
}

type Permissions_ = WorkspacePermission | OrganizationPermission;


type CheckPermissionsArgs<P extends Permissions_> = {
  perform: P,
  sustain?: boolean,
};

type Union<P extends Permissions_> = P extends keyof RolePermission ? RolePermission[P] : Record<string, never>

function checkPermissions<P extends Permissions_>(props: CheckPermissionsArgs<P>, data: Union<P>) {
  // this object has exactly the same type as you have in your question
  const merged = { ...props, ...data } 
}


type CheckRolePermissionsArgs<P extends Permissions_> = CheckPermissionsArgs<P> & {
  role: string[],
}



function checkRolePermissions<P extends Permissions_>({ role, ...rest }: CheckRolePermissionsArgs<P>) {
  // Now, TS is able to infer the type
  return (data: Union<P>) => checkPermissions(rest, data);

}


checkRolePermissions({
  perform: OrganizationPermission.UPDATE,
  sustain: true,
  role: ['sdf']
})({
  organizationRole: OrganizationRole.MANAGER,
}) // ok


checkRolePermissions({
  perform: WorkspacePermission.INVITE,
  sustain: true,
  role: ['sdf']
})({
  organizationRole: OrganizationRole.MANAGER,
}) // expected error


Playground

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM