stalniy / casl

CASL is an isomorphic authorization JavaScript library which restricts what resources a given user is allowed to access
https://casl.js.org/
MIT License
5.74k stars 257 forks source link

[Bug] Invalid union type for subject when subjects have different actions #900

Open douglasg14b opened 3 months ago

douglasg14b commented 3 months ago

Describe the bug

The union type for Subject appears to be incorrect when different subjcts have different actions.

To Reproduce

type Abilities = ["create" | "manage", "campaign"] | ["create" | "delete", "user:invite"]

const ability = createMongoAbility<Abilities>();

// Intellisense suggests this, but TS actually errors indicating this is incorrect (which it is)
ability.can('delete', 'campaign')

// This is correct
ability.can('delete', 'user:invite')

image

Expected behavior

The union type for subject should be impacted by the action, and not suggest subjects that are not valid for a given action.

CASL Version

@casl/ability: 6.7.1

stalniy commented 3 months ago

I’m thinking whether it’s actually an issue with types.

Because if TS errors, then types are correct. And it seems as issue relates to IDE or typescript service inside IDE

stalniy commented 3 months ago
Screenshot 2024-03-30 at 11 16 49

VSCode correctly highlights incorrect usage. Though it gives invalid suggestion

stalniy commented 3 months ago

This is how I solved it in casl:

Screenshot 2024-03-30 at 11 24 49

as you can see type O is a union of possible arguments combinations but even when I try to create an array for this union of tuples, IDE provides wrong suggestions:

Screenshot 2024-03-30 at 11 26 24
stalniy commented 3 months ago

In many places to not repeat the same logic, I have this CanParameters<T> helper type which allows me to write method declaration as function test(...args: CanParameters<TAbility>): ... and this doesn't work with function generic types

But indeed this can be improved. For example by creating an object map action => subject type + generic parameter on method:

type ActionToSubjectTypeMap<T extends [string, unknown]> = {
    [K in T[0]]: T extends any
        ? K extends T[0]
            ? T[1]
            : never
        : never
}

type PossibleAbilities =
    | ['read', 'User']
    | ['create' | 'delete', 'BlogPost']

type Mapping = ActionToSubjectTypeMap<PossibleAbilities>

But I consider this as an improvement not a bug because TS fails to compile with the current behavior