Skip to content

Class Rules

The classes() entry point operates on class declarations. Use it for inheritance, decorators, methods, properties, and body analysis.

When to Use

Use classes() when your architectural rules target object-oriented constructs -- inheritance hierarchies, decorators, method signatures, or property shapes. If your rule is about standalone functions or type aliases, reach for functions() or types() instead.

  • Enforce inheritance patterns (repositories extend BaseRepository)
  • Check decorators on classes and methods
  • Verify naming conventions for classes
  • Inspect method bodies (see Body Analysis)
  • Enforce export rules

Basic Usage

typescript
import { project, classes } from '@nielspeter/ts-archunit'

const p = project('tsconfig.json')

classes(p).that().extend('BaseRepository').should().beExported().check()

Available Predicates

Predicates narrow down which classes a rule applies to. Chain them with .and() to combine multiple filters. All identity predicates (haveNameMatching, resideInFolder, areExported, etc.) work on classes. In addition, class-specific predicates let you filter by inheritance, decorators, and members:

PredicateDescriptionExample
extend(name)Class extends the given base class.that().extend('BaseRepository')
implement(name)Class implements the given interface.that().implement('Serializable')
haveDecorator(name)Class has the given decorator.that().haveDecorator('Injectable')
haveDecoratorMatching(re)Class has a decorator matching regex.that().haveDecoratorMatching(/^Api/)
areAbstractClass is abstract.that().areAbstract()
haveMethodNamed(name)Class has a method with the name.that().haveMethodNamed('execute')
haveMethodMatching(re)Class has a method matching the regex.that().haveMethodMatching(/^handle/)
havePropertyNamed(name)Class has a property with the name.that().havePropertyNamed('logger')

Available Conditions

Conditions define what the matched classes must satisfy. They go after .should() in the rule chain. Conditions are grouped into structural checks, class-specific assertions, and body analysis.

Structural Conditions

Structural conditions apply to any declaration type and cover basic concerns like export visibility and naming.

ConditionDescription
beExported()Class must be exported
notExist()No classes should match the predicates
conditionHaveNameMatching(re)Class name must match the regex

Class-Specific Conditions

These conditions assert on class structure -- inheritance, methods, properties, and parameter types. Use them to enforce patterns like "all repositories must extend BaseRepository" or "services must not accept a raw database client."

ConditionDescriptionExample
shouldExtend(name)Class must extend the named base class.should().shouldExtend('BaseRepository')
shouldImplement(name)Class must implement the named interface.should().shouldImplement('Disposable')
shouldHaveMethodNamed(name)Class must have a method with the name.should().shouldHaveMethodNamed('dispose')
shouldNotHaveMethodMatching(re)Class must not have methods matching regex.should().shouldNotHaveMethodMatching(/^_/)
shouldHavePropertyNamed(...names)All named properties must exist.should().shouldHavePropertyNamed('logger')
shouldNotHavePropertyNamed(...names)None of the named properties may exist.should().shouldNotHavePropertyNamed('offset')
havePropertyMatching(pattern)At least one property matches regex.should().havePropertyMatching(/^id$/)
notHavePropertyMatching(pattern)No property matches regex.should().notHavePropertyMatching(/^data$/)
haveOnlyReadonlyProperties()All properties must be readonly.should().haveOnlyReadonlyProperties()
maxProperties(n)Property count must not exceed n.should().maxProperties(10)
acceptParameterOfType(matcher)At least one parameter matches TypeMatcher.should().acceptParameterOfType(matching(/DatabaseClient/))
notAcceptParameterOfType(matcher)No parameter matches TypeMatcher.should().notAcceptParameterOfType(matching(/DatabaseClient/))

Body Analysis Conditions

Body analysis conditions inspect the AST inside class method bodies for specific call expressions, constructor invocations, or other patterns. Use these to ban unsafe APIs or require specific helper calls.

ConditionDescription
contain(matcher)Class methods must contain the expression
notContain(matcher)Class methods must not contain the expression
useInsteadOf(banned, replacement)Replace banned expression with an alternative

See Body Analysis for full details on matchers and conditions.

Real-World Examples

Repositories Must Extend BaseRepository

typescript
classes(p)
  .that()
  .haveNameEndingWith('Repository')
  .and()
  .resideInFolder('**/repositories/**')
  .should()
  .shouldExtend('BaseRepository')
  .rule({
    id: 'repo/extend-base',
    because: 'BaseRepository provides transaction support and shared query helpers',
    suggestion: 'Add `extends BaseRepository` to the class declaration',
  })
  .check()

Controllers Must End with Controller

typescript
classes(p)
  .that()
  .resideInFolder('**/controllers/**')
  .should()
  .conditionHaveNameMatching(/Controller$/)
  .rule({
    id: 'naming/controller-suffix',
    because: 'Consistent naming makes the codebase navigable',
  })
  .check()

No any-Typed Properties

typescript
import { noAnyProperties } from '@nielspeter/ts-archunit/rules/typescript'

classes(p)
  .that()
  .areExported()
  .should()
  .satisfy(noAnyProperties())
  .because('any bypasses the type checker')
  .check()

No Type Assertions in Services

typescript
import { noTypeAssertions } from '@nielspeter/ts-archunit/rules/typescript'

classes(p)
  .that()
  .haveNameEndingWith('Service')
  .should()
  .satisfy(noTypeAssertions())
  .because('use type guards instead of as casts')
  .check()

Services Must Use Typed Errors

typescript
classes(p)
  .that()
  .extend('BaseService')
  .should()
  .notContain(newExpr('Error'))
  .rule({
    id: 'error/typed-errors',
    because: 'Generic Error loses context and prevents consistent API error responses',
    suggestion: 'Use NotFoundError, ValidationError, or DomainError instead',
  })
  .check()

Domain Entities Must Be Exported

typescript
classes(p)
  .that()
  .resideInFolder('**/domain/**')
  .should()
  .beExported()
  .because('domain entities are used by application layer')
  .check()

Named Selections for Reuse

typescript
const repositories = classes(p).that().extend('BaseRepository')

repositories.should().notContain(call('parseInt')).check()
repositories.should().notContain(newExpr('Error')).check()
repositories.should().beExported().check()

Property Conditions

Property conditions assert on the properties of class declarations. The should prefix is used on shouldHavePropertyNamed and shouldNotHavePropertyNamed to avoid collision with the havePropertyNamed predicate.

Forbidden Properties on Classes

typescript
import { project, classes } from '@nielspeter/ts-archunit'

const p = project('tsconfig.json')

classes(p)
  .that()
  .haveNameEndingWith('Service')
  .should()
  .shouldNotHavePropertyNamed('offset', 'pageSize')
  .because('services should not handle raw pagination parameters')
  .check()

Immutable Value Objects

typescript
classes(p)
  .that()
  .resideInFolder('**/domain/values/**')
  .should()
  .haveOnlyReadonlyProperties()
  .because('value objects must be immutable')
  .check()

Property Count Limits

typescript
classes(p)
  .should()
  .maxProperties(10)
  .because('classes with many properties indicate a missing abstraction')
  .check()

Parameter Type Conditions

Parameter type conditions scan constructor parameters, method parameters, and setter parameters to enforce DI boundaries. They compose with the existing TypeMatcher system.

Services Must Not Accept Database Client

typescript
import { project, classes, matching } from '@nielspeter/ts-archunit'

const p = project('tsconfig.json')

classes(p)
  .that()
  .haveNameEndingWith('Service')
  .should()
  .notAcceptParameterOfType(matching(/Knex/))
  .rule({
    id: 'di/no-direct-db-in-services',
    because: 'Services should depend on repositories, not database clients',
    suggestion: 'Inject a repository instead of Knex',
  })
  .check()

Repositories Must Accept Database Client

typescript
classes(p)
  .that()
  .haveNameEndingWith('Repository')
  .should()
  .acceptParameterOfType(matching(/DatabaseClient/))
  .because('repositories are the database access layer')
  .check()

Standard Rules

Pre-built class conditions from sub-path imports:

typescript
import {
  noAnyProperties,
  noTypeAssertions,
  noNonNullAssertions,
} from '@nielspeter/ts-archunit/rules/typescript'
import {
  noEval,
  noFunctionConstructor,
  noConsoleLog,
  noProcessEnv,
} from '@nielspeter/ts-archunit/rules/security'
import { noGenericErrors, noTypeErrors } from '@nielspeter/ts-archunit/rules/errors'
import { mustMatchName } from '@nielspeter/ts-archunit/rules/naming'

classes(p).should().satisfy(noEval()).check()
classes(p).should().satisfy(noGenericErrors()).check()
classes(p)
  .that()
  .resideInFolder('**/controllers/**')
  .should()
  .satisfy(mustMatchName(/Controller$/))
  .check()

Released under the MIT License.