🛠 A comprehensive ESLint and Prettier configuration for TypeScript projects, with support for monorepos and ESM. Optimized for Next.js and React applications. ⚡️
- 🎯 TypeScript & JavaScript Linting: Latest standards-based linting for both TypeScript and JavaScript
- 🔧 Multiple Config Presets: Ready-to-use configurations for
react,hooks,next, and more - 📝 Shared TypeScript Config: Pre-configured
tsconfig.jsonfor consistent TypeScript settings - 💅 Prettier Integration: Automatic code formatting with sensible defaults
- ♿️ Accessibility: Built-in rules for JSX accessibility
- 📦 Monorepo Support: Optimized for monorepo setups
- 🔄 Module Support: Full compatibility with both ESM and CommonJS
- 🎨 Tailwind CSS: Built-in support for Tailwind CSS class sorting
- 🔍 SonarJS: Optional integration for code quality rules
-
Initialize your project (if needed):
npm init
-
Install the ESLint config:
npm i -D @hyperse/eslint-config-hyperse
-
Create ESLint configuration:
- Create
eslint.config.js(oreslint.config.mjsfor CommonJS) in your project root - Add the base configuration:
import { base, defineConfig } from '@hyperse/eslint-config-hyperse'; export default defineConfig([ ...base, { rules: { '@typescript-eslint/no-explicit-any': 'off', }, }, ]);
- Create
The shared config targets ES2023 and pins lib explicitly to
["ES2023", "DOM", "DOM.Iterable", "DOM.AsyncIterable", "WebWorker.ImportScripts", "ScriptHost"],
so the built-in type declarations no longer depend on the target default. Projects needing
newer built-ins can opt in without forking the base config:
{
"extends": "@hyperse/eslint-config-hyperse/tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2024", "DOM", "DOM.Iterable"]
}
}{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@hyperse/eslint-config-hyperse/tsconfig.base.json",
"compilerOptions": {
"baseUrl": "./",
"rootDir": ".",
"outDir": "dist",
"types": ["vitest/globals"],
"paths": {}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"baseUrl": "./",
"incremental": false,
"paths": {}
},
"exclude": ["**/*.stories.tsx", "**/*.stories.mdx", ".storybook/**", "dist"]
}Add to your package.json:
{
"scripts": {
"lint": "tsc --noEmit && eslint .",
"lint:fix": "npm run lint -- --fix"
}
}Or for JavaScript-only projects:
{
"scripts": {
"lint": "eslint .",
"lint:fix": "npm run lint -- --fix"
}
}import { defineConfig, nextjs } from '@hyperse/eslint-config-hyperse';
export default defineConfig([
...nextjs,
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
},
},
]);import { defineConfig, reactjs } from '@hyperse/eslint-config-hyperse';
export default defineConfig([
...reactjs,
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
},
},
]);import { defineConfig, sonarjs } from '@hyperse/eslint-config-hyperse';
export default defineConfig([
...sonarjs,
{
rules: {
'sonarjs/fixme-tag': 'warn',
'sonarjs/todo-tag': 'warn',
},
},
]);-
Install the ESLint extension
-
Configure VS Code settings (
.vscode/settings.json):
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "never"
},
"[jsonc]": { "editor.formatOnSave": false },
"[json]": { "editor.formatOnSave": false },
"eslint.workingDirectories": [
"./apps/docs",
"./apps/web",
"./packages/core",
"./packages/utils"
],
"files.associations": {
"*.css": "tailwindcss"
},
"editor.quickSuggestions": {
"strings": "on"
},
"tailwindCSS.classFunctions": ["tw", "clsx", "twMerge", "extendVariants"],
"tailwindCSS.classAttributes": ["className", "classNames"],
"tailwindCSS.experimental.configFile": {
"apps/web/src/app/globals.css": "apps/web/src/**",
"apps/docs/src/app/globals.css": "apps/docs/src/**"
}
}Use definePrettierConfig() so Prettier CLI matches ESLint prettier/prettier (base options and prettier-plugin-tailwindcss, with the plugin always last when you add more plugins). The name mirrors this package’s defineConfig helper for ESLint.
Create prettier.config.mjs:
import { definePrettierConfig } from '@hyperse/eslint-config-hyperse';
/**
* @see https://prettier.io/docs/configuration
* @see https://github.com/tailwindlabs/prettier-plugin-tailwindcss#installation
*/
export default definePrettierConfig({
// ...your overrides (e.g. Tailwind v4)
});Optional slim import (no ESLint stack): @hyperse/eslint-config-hyperse/definePrettierConfig (the legacy path @hyperse/eslint-config-hyperse/getPrettierConfig still resolves to the same module).
The previous name getPrettierConfig remains exported but is deprecated; prefer definePrettierConfig.
Configure Tailwind via definePrettierConfig (paths are resolved relative to this config file):
import { definePrettierConfig } from '@hyperse/eslint-config-hyperse';
export default definePrettierConfig({
tailwindStylesheet: './src/app/globals.css',
tailwindFunctions: ['tw', 'clsx', 'twMerge', 'extendVariants'],
tailwindAttributes: ['className', 'classNames'],
});- TypeScript settings:
isolatedModules: true (default)verbatimModuleSyntax: true (default)
- JSON/JSONC files: VS Code formatting is disabled by default
- React ESLint rules: See source code
The following ESLint rules may impact performance during linting/formatting:
prettier/prettier: Can be slow on large files due to full reformattingsonarjs/arguments-order: Complex analysis of function argument usage patterns
Consider selectively disabling these rules for specific files if performance becomes an issue.
This rule is the only type-aware rule the preset ships, and it is disabled by
default because enabling it is what pushes eslint . past the default Node
heap on larger repositories.
Measured on a ~2k-file monorepo package spanning 4 TS projects, from repeated runs against the default Node heap (~4GB):
| configuration | outcome |
|---|---|
projectService: true + no-deprecated: warn |
eslint . exits with Ineffective mark-compacts near heap limit |
projectService: true + no-deprecated: off |
completes; needs >3GB of heap |
TIMING=1 also shows the rule consuming ~27.8s of rule time — 62% of the
total, and ~2.3x the next most expensive rule (prettier/prettier at ~12.1s).
Re-enable it per project when the repository is small enough to afford it:
import { base, defineConfig } from '@hyperse/eslint-config-hyperse';
export default defineConfig([
...base,
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'@typescript-eslint/no-deprecated': 'warn',
},
},
]);Keep the
filesselector, and don't enable it with the CLI--ruleflag.--ruleapplies to every file ESLint visits, not just TypeScript. Files matched by other configs —.jsonamong them — carry noparserOptions, so the run aborts with:Error while loading rule '@typescript-eslint/no-deprecated': You have used a rule which requires type information, but don't have parserOptions set to generate type information for this file.Configure it in
eslint.config.jsas above instead.
projectService builds a TypeScript program covering every linted file, and
that program is where the remaining >3GB of heap goes. It is kept enabled
because @typescript-eslint/consistent-type-imports depends on it for
correctness, not just accuracy.
The rule reads emitDecoratorMetadata and experimentalDecorators from parser
services and, when both are set, skips any file containing a decorator
(typescript-eslint#5468).
Imports in those files are still referenced at runtime through design:type
metadata, so rewriting them with import type changes behaviour.
Without the program that guard is silently disabled. On the package above,
turning projectService off made the rule report 615 new errors — e.g. in
a TypeORM entity where the import is used only in a decorated property:
import { StocksEtlJobStatus } from './stocks-enums.js';
@Entity('stocks_etl_job')
export class StocksEtlJob {
@Column({ type: 'varchar' })
status: StocksEtlJobStatus; // `emitDecoratorMetadata` emits this at runtime
}The suggested autofix would turn that into import type, replacing the emitted
metadata with Object. That is why this preset keeps projectService on by
default.
If you want the ~512MB heap instead and do not rely on
consistent-type-imports, turn both off together:
import { base, defineConfig } from '@hyperse/eslint-config-hyperse';
export default defineConfig([
...base,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: { projectService: false },
},
rules: {
'@typescript-eslint/consistent-type-imports': 'off',
},
},
]);