Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/prefer-type-definitions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@incentro-ic/config-eslint": major
---

Enforce type aliases over interfaces with `@typescript-eslint/consistent-type-definitions`, matching the style guide.
19 changes: 18 additions & 1 deletion STYLE-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,24 @@ function readPackageJson() {

### 🧬 Interfaces, types & enums

We don't use interfaces, we use types. Types can do everything interfaces can and more.
We use types instead of interfaces. Interfaces only describe object shapes, while types also cover unions, tuples, mapped types and conditional types, so using types everywhere leaves one way to declare a type. Types also can't be merged by accident: declaring the same interface twice silently merges both declarations, while declaring the same type twice is an error. And a type describing an object is assignable to `Record<string, unknown>`, an interface isn't. The ESLint config enforces this with `@typescript-eslint/consistent-type-definitions`.

There are two exceptions where an interface is the better choice:

- **Module augmentation.** Extending a type declared by a library (like `Window`, or React Router's `Register`) relies on declaration merging, which only interfaces support.
- **Large hierarchies of object types.** When an object type is built by extending several others, prefer `interface … extends` over intersections (`&`). `extends` reports conflicting properties as an error, while an intersection silently turns them into `never`. Typescript also caches `extends` relationships, which keeps type checking fast in large codebases.

In both cases, [diverge from the linting rule](#-diverging-from-linting-rules) for that declaration only:

```typescript
declare module "react-router" {
// REASON: Module augmentation requires an interface.
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface Register {
params: Params;
}
}
```

Types and enums use `PascalCase`. Type properties should be `camelCase` and enum keys should be `UPPER_CASE`. Enum values should be `UPPER_CASE`.

Expand Down
7 changes: 7 additions & 0 deletions packages/eslint/src/partials/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ export const typescriptConfigs: Linter.Config[] = [
},
],

// The stylistic preset prefers interfaces, but our style guide prefers
// types: they also cover unions, tuples, mapped and conditional types,
// can't be merged by accident and are assignable to
// `Record<string, unknown>`. Module augmentation and large `extends`
// hierarchies are valid reasons to disable this rule for one line.
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
Comment on lines +69 to +71

// When using Typedoc, interfaces are preferred over types because they
// are read and documented natively by Typedoc. Type aliases require the
// `@interface` comment
Expand Down
Loading