Skip to content

Type Safety ​

By default, the action parameter in can() calls accepts any string. ng-ability offers two mechanisms to narrow this to the specific actions that are valid for each matcher.

Declaring Actions with AbilityActions ​

Use TypeScript declaration merging to register allowed actions per matcher key on the AbilityActions interface:

typescript
// e.g. src/ability-actions.d.ts
declare module 'ng-ability' {
  interface AbilityActions {
    Article: 'view' | 'create' | 'edit' | 'delete'
    AdminArea: 'view'
    Post: 'read' | 'write'
  }
}

With this declaration in place, can() calls using registered string matchers will:

  • Suggest only the declared actions in autocomplete
  • Still accept any string (to avoid breaking unregistered actions)
typescript
ability.can('Article', 'edit')    // ✓ suggests 'view' | 'create' | 'edit' | 'delete'
ability.can('AdminArea', 'view')  // ✓ suggests 'view'
ability.can('Article', 'publish') // still accepted — no compile error, just no suggestion

AbilityActionsOf for Class Matchers ​

When you use a class constructor as a matcher, TypeScript cannot infer which actions apply to that class without extra help. The AbilityActionsOf<K> interface links a class to its registered actions:

typescript
import { AbilityActionsOf } from 'ng-ability'

// 1. Declare actions
declare module 'ng-ability' {
  interface AbilityActions {
    Article: 'view' | 'create' | 'edit' | 'delete'
  }
}

// 2. Implement AbilityActionsOf on your class
export class Article implements AbilityActionsOf<'Article'> {
  constructor(
    public id: number,
    public title: string,
    public authorId: number,
  ) {}
}

Now TypeScript enforces the allowed actions when passing the Article constructor:

typescript
ability.can(Article, 'view')    // ✓ valid
ability.can(Article, 'edit')    // ✓ valid
ability.can(Article, 'delete')  // ✓ valid
ability.can(Article, 'publish') // ✗ TypeScript error: 'publish' is not assignable

With instances ​

Type narrowing also works when passing an Article instance — ng-ability recognises it as an Article via the matcher:

typescript
const article = new Article(1, 'Hello', 42)

ability.can(article, 'edit')    // ✓ valid, actions narrowed to Article actions
ability.can(article, 'publish') // ✗ TypeScript error

Benefits ​

Without AbilityActionsOfWith AbilityActionsOf
ability.can(Article, 'anything') compilesability.can(Article, 'unknown') is a TS error
No IDE suggestions for class matchersAutocomplete shows valid actions
Typos caught only at runtimeTypos caught at compile time

AbilityActionsOf is especially valuable when:

  • You have many entity classes with different allowed actions
  • Multiple teams work on different parts of the codebase
  • You want classes to be self-documenting about their permission model

Enforcement in Abilities ​

Declare actions in your ability implementations to take advantage of the same type information:

typescript
import { AbilityFor, Ability } from 'ng-ability'
import { Article } from './article.model'

type ArticleAction = AbilityActions['Article'] // 'view' | 'create' | 'edit' | 'delete'

@AbilityFor(Article, 'Article')
export class ArticleAbility implements Ability<User, Article> {
  can(currentUser: User | null, action: ArticleAction, article?: Article): boolean {
    // TypeScript narrows `action` to the declared union
    switch (action) {
      case 'view':   return true
      case 'create': return currentUser != null
      case 'edit':
      case 'delete': return currentUser?.id === article?.authorId
    }
  }
}

Released under the MIT License.