eslint-plugin-ts-type-preferences
v1.0.6
Published
ESLint plugin for TypeScript with two rules: prefer interface extends over type intersection and prefer merged type literal over intersection
Maintainers
Readme
eslint-plugin-ts-type-preferences
An ESLint plugin for TypeScript with two rules:
- Prefer
interface extendswhen object inheritance is expressed via intersections. - Prefer merging object-only intersections into a single type literal.
Why
This plugin is based on Matt Pocock's article "Type vs Interface: Which Should You Use?" The article recommends using interface specifically for object inheritance because extends lets TypeScript reuse cached interface information and is slightly more optimal than & intersections.
It also recommends defaulting to type for everything else due to interface declaration merging pitfalls. These rules follow that guidance by only targeting object inheritance and object-only intersections expressed via &.
Reference: https://www.totaltypescript.com/type-vs-interface-which-should-you-use
Rules
prefer-interface-extends-over-type-intersection
Reports type aliases that intersect at least one type reference with object literals and suggests an equivalent interface declaration.
It will:
- Convert
type A = B & { ... }tointerface A extends B { ... }. - Only emit an
interfacewhen the intersection includes at least one type reference. - Leave intersections that include unsupported types (like unions, primitives, or indexed access types) unchanged.
The rule is fixable and will automatically rewrite the type alias to an interface.
prefer-merged-type-literal-over-intersection
Reports object-only intersections and suggests merging them into a single type literal.
It will:
- Convert
type A = { ... } & { ... }totype A = { ... }. - Keep the declaration as a
typeinstead of converting to aninterface.
Examples
prefer-interface-extends-over-type-intersection
Invalid
type WithId = {
id: string;
};
type User = WithId & {
name: string;
};Valid (after fix)
type WithId = {
id: string;
};
interface User extends WithId {
name: string;
}Invalid (multiple bases)
type Audited = { createdAt: Date };
type SoftDelete = { deletedAt?: Date };
type Record = Audited &
SoftDelete & {
id: string;
};Valid (after fix)
type Audited = { createdAt: Date };
type SoftDelete = { deletedAt?: Date };
interface Record extends Audited, SoftDelete {
id: string;
}Invalid (generic base)
type Paginated<T> = {
items: T[];
total: number;
};
type Users = Paginated<User> & {
page: number;
};Valid (after fix)
type Paginated<T> = {
items: T[];
total: number;
};
interface Users extends Paginated<User> {
page: number;
}prefer-merged-type-literal-over-intersection
Invalid (multiple object intersections)
type A = {
fieldA: string;
} & {
fieldB: number;
};Valid (after fix)
type A = {
fieldA: string;
fieldB: number;
};Invalid (methods and signatures)
type Handler = {
handle(value: string): void;
[key: string]: number;
(): void;
} & {
status: "ok" | "error";
};Valid (after fix)
type Handler = {
handle(value: string): void;
[key: string]: number;
(): void;
status: "ok" | "error";
};Invalid (nested intersection)
type Flags = ({
enabled: boolean;
} & {
level: number;
}) & {
label: string;
};Valid (after fix)
type Flags = {
enabled: boolean;
level: number;
label: string;
};Options
prefer-interface-extends-over-type-intersection
mergeObjects (boolean, default: true)
When true, object literal types in the intersection are merged into the interface body.
When false, the rule only converts intersections that contain at most one object literal type and at least one type reference. Intersections with multiple object literals are left unchanged.
Left untouched when mergeObjects: false
type A = {
fieldA: string;
} & {
fieldB: number;
};prefer-merged-type-literal-over-intersection
No options.
Installation
pnpm add -D eslint-plugin-ts-type-preferencesUsage
Flat config (ESLint v9+)
import tsTypePreferences from "eslint-plugin-ts-type-preferences";
export default [
{
plugins: {
"ts-type-preferences": tsTypePreferences,
},
rules: {
"ts-type-preferences/prefer-interface-extends-over-type-intersection":
"error",
"ts-type-preferences/prefer-merged-type-literal-over-intersection":
"error",
},
},
];Legacy config (.eslintrc)
{
"plugins": ["ts-type-preferences"],
"rules": {
"ts-type-preferences/prefer-interface-extends-over-type-intersection": "error",
"ts-type-preferences/prefer-merged-type-literal-over-intersection": "error"
}
}Recommended config
Flat config (ESLint v9+)
import tsTypePreferences from "eslint-plugin-ts-type-preferences";
export default [
tsTypePreferences.configs.recommended,
];Legacy config (.eslintrc)
{
"extends": ["plugin:ts-type-preferences/recommended"]
}Limitations
When merging object literals, the fixer preserves all member kinds and order, but it does not attempt to resolve duplicate or conflicting members. If you rely on advanced merging semantics, review the fix output before applying.
Development
pnpm install
pnpm test