TypeScript Support

CASL is written in TypeScript and this brings several benefits:

  • better safety as you can control what actions and subjects can be used
  • better IDE integration as you can get hints on what classes you can use and arguments you need to pass inside
  • easier library support, we can forget about synchronization issues between .d.ts and .js files

So, let's play around with them

Minimum supported TypeScript version is 3.8.3. CASL uses export type syntax

Permissions inference

Ability class and createMongoAbility factory function accepts 2 optional generic parameters:

import { createMongoAbility, Subject, MongoQuery } from '@casl/ability';

type PossibleAbilities = [string, Subject];
type Conditions = MongoQuery;

const ability = createMongoAbility<PossibleAbilities, Conditions>();

Subject is a special type that represents all possible subjects that Ability can accept. So, it's object | string | Function | undefined.

Don't be scared by the complexity, createMongoAbility uses that types by default, so the example above is the same as the one below:

import { createMongoAbility } from '@casl/ability';

const ability = new createMongoAbility();

These types are enough to protect you from passing wrong arguments but you can go further and make them even stricter. To illustrate how, let's consider a blog application, which has User, Article and Comment entities with the next user's permissions:

  • can create, update, delete own Article or Comment
  • can read any Article, any Comment and any User

So, let's translate this to CASL by specifying all possible actions and all possible subjects as generic parameters:

import { createMongoAbility } from '@casl/ability';

type Actions = 'create' | 'read' | 'update' | 'delete';
type Subjects = 'Article' | 'Comment' | 'User';

const ability = createMongoAbility<[Actions, Subjects]>();

If we try to type ability.can( in VSCode (or other TypeScript supported IDEs), it provides hints:

CASL TypeScript action hints

The same happens when you try to specify the 2nd argument:

CASL TypeScript subject hints

The same behavior works for AbilityBuilder and defineAbility function:

CASL TypeScript AbilityBuilder hints

Infer subject types from interfaces and classes

You can also specify interfaces as subjects:

import { createMongoAbility } from '@casl/ability';

interface Article {
  id: number
  title: string
  content: string
  authorId: number
}

interface User {
  id: number
  name: string
}

interface Comment {
  id: number
  content: string
  authorId: number
}

type Action = 'create' | 'read' | 'update' | 'delete';
type Subject = Article | Comment | User | 'Article' | 'User' | 'Comment';

const ability = createMongoAbility<[Action, Subject]>();

ability.can('read', 'Article');
ability.can('write', 'Article'); // error because non-existing action name
ability.can('update', 'Coment') // error because of typo

and classes:

import { createMongoAbility } from '@casl/ability';

class Article {
  id: number
  title: string
  content: string
  authorId: number
}

type Action = 'create' | 'read' | 'update' | 'delete';
type Subject = typeof Article | Article;

const ability = createMongoAbility<[Action, Subject]>();

ability.can('read', Article);
ability.can('update', new Article());

This may be a bit routine to specify all possible subject types, especially if you have more than 3 of them. To make it easier, CASL provide InferSubjects typescript helper which can infer subjects from tagged unions (this helper checks kind and special __caslSubjectType__ properties in order to determine the tag)

__caslSubjectType__ is set by subject helper, to learn more check

CASL TypeScript infer tagged union subject

classes

CASL TypeScript infer class subject

and even discriminated classes (you need to pass true to the 2nd generic parameter of InferSubjects).

CASL TypeScript infer discriminated class subject

The same parameter allows to infer modelName static property from classes (in case you want to use strings and not classes to check on subject type)

CASL TypeScript infer class modelName

Moreover, the same behavior also works in complementary packages! So, you will get hints for React's Can component, Vue's $can function, Mongoose's plugins and others.

To learn more, read docs for a complementary package for of your choice

But even this is not the end and you can go even further!

Configuring subject resolution

The third generic parameter of Ability and MongoAbility is optional. Omit it when using CASL's standard subject type detection; it defaults to DefaultSubject, which preserves the inference described above. You only need to specify it when customizing subject type detection and its TypeScript inference.

For custom detection based on a discriminator property, use TaggedSubject<Key>. You can specify the object types without repeating their names:

import {
  AbilityBuilder, createMongoAbility, MongoAbility, MongoQuery, TaggedSubject
} from '@casl/ability';

interface Article {
  kind: 'Article';
  title: string;
  authorId: number;
}

interface User {
  kind: 'User';
  name: string;
}

type AppAbility = MongoAbility<
  ['read' | 'update', Article] | ['read', User],
  MongoQuery,
  TaggedSubject<'kind'>
>;

const builder = new AbilityBuilder<AppAbility>(createMongoAbility);
builder.can('read', 'Article', { authorId: 1 });
builder.can('read', 'User', { name: 'Alice' });
// builder.can('update', 'User'); // error: User only supports read
// builder.can('read', 'Article', { name: 'Alice' }); // error: Article has no name

const ability = builder.build({
  detectSubjectType: object => object.kind
});

ability.can('read', 'Article');
ability.can('read', { kind: 'Article', title: 'Example', authorId: 1 });

The resolver is a TypeScript type and does not configure runtime detection. Supply a matching detectSubjectType option when using a custom discriminator. TaggedSubject without a key uses __caslSubjectType__, the property set by the subject() helper.

The configured resolver also applies to React and Vue Can props, Angular services and pipes, and Mongoose's accessibleBy and accessibleFieldsBy helpers. These integrations accept the same subject names and instances as the ability's permission checks.

For Prisma, PrismaAbility<Abilities, PrismaQuery, Resolver> accepts the same optional third generic parameter. Pass this ability type to createPrismaAbility<AppAbility>() or AbilityBuilder<AppAbility>; accessibleBy(ability).ofType(...) infers model names using its resolver. You can also pass all three generic parameters directly to createPrismaAbility.

When writing your own integrations, SubjectResolver is the resolver constraint and ResolveSubjects<Subjects, Resolver> produces the accepted subject names and instances. Both types are exported from @casl/ability. Use Generics<AppAbility>['subjectResolver'] to retrieve an ability's configured resolver.

A union such as TaggedSubject<'kind' | '__typename' | BuiltinSubjectKey> supports multiple discriminator properties. BuiltinSubjectKey is exported from @casl/ability and represents __caslSubjectType__. Names from all configured properties are included; the union does not define runtime precedence when an object has multiple tags.

If you want to customize inference to derive instances from class types alone, use ClassSubject. This is optional: the default resolver already supports classes and their instances as described above. ClassSubject also supports classes with protected or private constructors and abstract classes:

import {
  AbilityBuilder, ClassSubject, createMongoAbility, MongoAbility, MongoQuery
} from '@casl/ability';

class Article {
  private constructor(readonly title: string) {}

  static create(title: string) {
    return new Article(title);
  }
}

type AppAbility = MongoAbility<['read', typeof Article], MongoQuery, ClassSubject>;
const builder = new AbilityBuilder<AppAbility>(createMongoAbility);
builder.can('read', Article, { title: 'Public' });

const ability = builder.build();
ability.can('read', Article.create('Public')); // true

Class rules use CASL's existing runtime class detection, so this example needs no custom detector.

If a static factory defines the public object type, use FactorySubject<'create'> to infer that factory's return type instead of the class prototype. The method name defaults to create; pass another key for factories such as FactorySubject<'from'>.

import {
  AbilityBuilder, createMongoAbility, FactorySubject, MongoAbility, MongoQuery
} from '@casl/ability';

interface UserView {
  name: string;
}

class User {
  protected constructor(readonly name: string, readonly internalId: number) {}

  static create(name: string): UserView {
    return new this(name, 1);
  }
}

type AppAbility = MongoAbility<['read', typeof User], MongoQuery, FactorySubject<'create'>>;
const builder = new AbilityBuilder<AppAbility>(createMongoAbility);
builder.can('read', User, { name: 'Alice' });
// builder.can('read', User, { internalId: 1 }); // error: UserView has no internalId

const ability = builder.build();
ability.can('read', User.create('Alice')); // true

The class remains the rule's subject type. If the factory returns an object with a different constructor, supply a detectSubjectType function that maps the returned object back to that class. The resolver only infers types; it does not call the factory or configure runtime detection.

For other relationships, define a custom resolver. For example, objects might store their subject name in nested metadata:

import {
  AbilityBuilder, Container, createMongoAbility, MongoAbility, MongoQuery,
  SubjectResolverFactory
} from '@casl/ability';

type ResourceName<S> = S extends { meta: { resource: infer N extends string } }
  ? N
  : never;

type ResourceResolution<S, Name> = {
  subject: Extract<S, object> | ResourceName<S>;
  subjectType: ResourceName<S>;
  instance: Extract<S, { meta: { resource: Name } }>;
};

interface ResourceResolver extends SubjectResolverFactory {
  produce: ResourceResolution<this[0], this[1]>;
}

interface Article {
  meta: { resource: 'Article' };
  title: string;
}

interface User {
  meta: { resource: 'User' };
  name: string;
}

type AppAbility = MongoAbility<
  ['read', Article | User], MongoQuery, Container<ResourceResolver>
>;

const builder = new AbilityBuilder<AppAbility>(createMongoAbility);
builder.can('read', 'Article', { title: 'Public' });
builder.can('read', 'User', { name: 'Alice' });
// builder.can('read', 'Article', { name: 'Alice' }); // error: Article has no name

const ability = builder.build({
  detectSubjectType: object => object.meta.resource
});

ability.can('read', { meta: { resource: 'Article' }, title: 'Public' }); // true

Safer permissions inference

For the most cases the suggested approach above should be enough but if you prefer to ensure extreme type safety, you can define dependencies between actions and subjects. For example, user can only read information about users in your app and nothing more but can manage articles:

import { createMongoAbility } from '@casl/ability';

type CRUD = 'create' | 'read' | 'update' | 'delete';
type Abilities = ['read', 'User'] | [CRUD, 'Article'];

const ability = createMongoAbility<Abilities>();

ability.can('read', 'User');
ability.can('create', 'User'); // build time error! because it's not allowed to create users

Application Ability

From the first sight, it looks like that in order to use safer generic parameters, your app's code will become more complicated and this is true. But there is an escape hatch - Companion object pattern:

import { MongoAbility, createMongoAbility, CreateAbility } from '@casl/ability';

type CRUD = 'create' | 'read' | 'update' | 'delete';
type Abilities = ['read', 'User'] | [CRUD, 'Article'];
export type AppAbility = MongoAbility<Abilities>;

export const createAppAbility = createMongoAbility as CreateAbility<AppAbility>;
// or you can just call whenever you need
const ability = createMongoAbility<AppAbility>();

This simple pattern comes to TypeScript from Scala, and it's a way to pair together types and objects. In TypeScript, values and types live in a separate namespaces, this allows to use the same name for a type and a class. TypeScript understands which one to use based on the context.

AbilityBuilder type inference

AbilityBuilder constructor accepts the single argument which is a type of Ability we want to build:

import { AbilityBuilder, Ability, createMongoAbility } from '@casl/ability';

// we can pass custom Ability class
const builder = new AbilityBuilder(Ability);

// or we can pass factory function
const builder = new AbilityBuilder(createMongoAbility);

Starting from v5, AbilityBuilder accepts required parameter, Ability class to build or ability factory function

Thanks to this AbilityBuilder can infer all needed types from Ability types. This is especially useful when we define AppAbility because then we will have IDE hints and type safety for:

  • specified action
  • specified subject type
  • properties used in conditions
  • specified fields

We can use either tagged interfaces (supports kind, __typename and __caslSubjectType__ tag fields) or classes:

CASL TypeScript: AbilityBuilder conditions hints

CASL TypeScript: AbilityBuilder fields hints

The cool thing is that all that safety is subject type dependent! Try to pass User subject type in place of Post and see it by yourself!

Nested fields with dot notation

If we need to define conditions based on nested fields, we can do this by defining a separate interface:

import { MongoAbility, createMongoAbility } from '@casl/ability';

interface User {
  kind: 'User'
  id: number
  name: string
  address: {
    street: string
    building: string
  }
}

type AppAbility = MongoAbility<['read', User | 'User']>;

const { can } = new AbilityBuilder<AppAbility>(createMongoAbility);

type FlatUser = User & {
  'address.street': User['address']['street']
};

can<FlatUser>('read', 'Post', { 'address.street': 'test' });
// It also works for fields
can<FlatUser>('read', 'Post', ['address.street'], { 'address.street': 'test' })

Useful type helpers

RawRule

Sometimes you may need to create RawRules manually (or fetch them from db). In that case, you will need to type them explicitly. Use RawRuleOf<AppAbility> in case if you have type for AppAbility or RawRuleFrom<Abilities, Conditions> otherwise.

import { MongoAbility, RawRuleOf, RawRuleFrom, MongoQuery } from '@casl/ability';

type AppAbilities = ['read' | 'update', 'Article'];
type AppAbility = MongoAbility<AppAbilities>;

const rawRules: RawRuleOf<AppAbility>[] = [
  { action: 'read', subject: 'Article' }
];

// or
type AppRawRule = RawRuleFrom<AppAbilities, MongoQuery>;
async function getRulesFromDb(): Promise<AppRawRule[]> {
  // implementation
}

RuleOf

Similar to RawRule helpers, there is a helper RuleOf<AppAbility> for Rule<Abilities, Conditions>. It's very unlikely that you will need to work with this types on application layer.

AbilityOptionsOf

Similar to RawRule, if you don't want to explicitly use AbilityOptions<Abilities, Conditions>, you can use AbilityOptionsOf<AppAbility>:

import { AbilityOptionsOf, MongoAbility, createMongoAbility } from '@casl/ability';

type AppAbilities = ['read' | 'update', 'Article'];
type AppAbility = MongoAbility<AppAbilities>;
const options: AbilityOptionsOf<AppAbility> = {
  detectSubjectType: (subject) => /* custom implementation */
};

const ability = createMongoAbility<AppAbilities>([], options);

AnyAbility and AnyMongoAbility

These 2 types represents any Ability instance and any MongoAbility instance. They are usually a good fit for restrictions in generic types. For example, this is how AnyAbility is used in AbilityBuilder:

export class AbilityBuilder<T extends AnyAbility = AnyAbility> {
  // implementation details
}

MongoQuery

There are 2 types that represents built-in mongo operators:

  • MongoQuery<T> is an actual mongo query.
    Is used as a conditions restriction in Ability created by createMongoAbility factory function.
  • MongoQueryOperators<T> represents supported MongoDB operators and it's a union of MongoQueryFieldOperators<T> and MongoQueryTopLevelOperators<T> that represent supported field and document level operators respectively

ForcedSubject

Represents an object (i.e., POJO) that has been casted to specific subject by using subject helper.

See for details