Skip to content

A class with no behavior beyond storing fields — no methods other than a constructor that only assigns fields, no inheritance, no implements clause, no decorators — is a candidate for a plain type/interface instead of a class.

ts
class Point {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}
ts
// fix
type Point = { x: number; y: number };

Escape hatches — none of these are flagged, even though they might otherwise look like a data bag:

ts
// Parameter properties: the standard DI pattern without decorators.
class Point {
  constructor(private readonly x: number, private readonly y: number) {}
}

// implements clause: a structural contract, not just storage.
class Point implements Shape {
  constructor(public x: number, public y: number) {}
}

// Inheritance (including built-ins like Error): legitimate non-FP use.
class NotFoundError extends Error {
  constructor(public id: string) {
    super(`not found: ${id}`);
  }
}

// A decorator.
@Injectable()
class Config {
  constructor(public apiUrl: string) {}
}

// Constructor does more than assign fields — that's behavior.
class Point {
  x: number;
  constructor(x: number) {
    if (x < 0) throw new Error("x must be non-negative");
    this.x = x;
  }
}

Scope note: a class with no constructor at all is only flagged if it declares at least one plain field — a genuinely empty class (class Marker {}) is a nominal-type marker pattern, a different concern this check doesn't address.

toml
# cofferdam.toml
[checks."Design.ClassAsDataBag"]
severity = "medium"   # raise if your project leans heavily on plain types over classes

MIT License