惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
H
Help Net Security
V
Visual Studio Blog
J
Java Code Geeks
Stack Overflow Blog
Stack Overflow Blog
Microsoft Security Blog
Microsoft Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
MyScale Blog
MyScale Blog
The Cloudflare Blog
Martin Fowler
Martin Fowler
D
Docker
腾讯CDC
F
Fortinet All Blogs
雷峰网
雷峰网
GbyAI
GbyAI
G
Google Developers Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Blog — PlanetScale
Blog — PlanetScale
Engineering at Meta
Engineering at Meta
博客园 - 聂微东
博客园 - 叶小钗

The Practical Developer

The Libuv Thread Pool Trap: Why Node.js Async APIs Stall Under Load Postgres Covering Indexes with INCLUDE: Eliminate Heap Fetches on Read-Heavy Workloads Postgres DISTINCT ON: The Fastest Way to Get the Latest Row Per Group Postgres Transaction Isolation: The Anomalies Your App Actually Faces in Production Linux TCP Tuning for Node.js Microservices: The Kernel Settings That Stop Silent Connection Drops Under Load Postgres HOT Updates and Fillfactor: Why Not All Writes Are Created Equal Database Connection Pool Leaks: Finding the Promise That Never Returns Its Seat Linux OOM Killer in Production: Why Your Node.js Containers Die Without a Stack Trace Postgres Materialized Views: Refresh Strategies That Do Not Lock Your Dashboards API Dependency Health Checks: Why /health Is Not Enough Authorization with Zanzibar Tuples: How Google Manages Permissions and How To Build the Same Check in Node.js Postgres Advisory Locks: The 20-Character Primitive That Replaces Redis for Coordination Dead Letter Queues: The Message Queue Pattern That Saves You at 2 a.m. File Descriptor Exhaustion: The Kernel Limit That Silently Drops Node.js Connections Graceful Degradation: The Pattern That Turns Total Outages into Partial Success PostgreSQL Full-Text Search: Dropping Elasticsearch for 90% of Use Cases S3 Presigned Multipart Uploads: Stop Your API Server from Being a File Upload Bottleneck MessagePack vs JSON: The Binary Serialization Switch That Cut Our Internal RPC Overhead by 40% DNS Caching in Node.js: The Silent Cause of Production Latency Spikes Reliable Cron Jobs: The Pattern That Stops Double Runs, Missed Executions, And The 2 AM Page GraphQL Query Complexity: Stop the OOM Query Before It Reaches Your Resolver Node.js Event Loop Lag: The Hidden Metric Behind Random Latency Spikes API Request Validation with Zod: The Schema That Catches Bad Input Before It Corrupts Your Database Load Shedding in Node.js: How to Reject Traffic Before You Drown Request Hedging: Cut Tail Latency In Half Without Overprovisioning Git Bisect: The Automated Binary Search That Finds Breaking Commits in Minutes Node.js Garbage Collection Tuning: Stop Letting V8 Pause Your Event Loop Node.js Server Timeouts: The Settings That Stop Slow Clients from Holding Sockets Hostage Postgres BRIN Indexes: The Time-Series Secret That Shrinks Indexes by 99% Event Sourcing with PostgreSQL: The Pragmatic 80% Solution
ESLint Rules That Earn Their Keep: The Twelve I Enable On...
The Practica · 2023-04-28 · via The Practical Developer

A default ESLint config catches semicolon mistakes and unused variables. That is fine but not life-changing. The rules that actually earn their CI cost are the ones that catch bugs no test will catch: async functions silently swallowed, missing switch cases, type casts that hide an error. They turn lint from a style check into a quiet bug detector.

This post is twelve ESLint rules I enable on every TypeScript project, the four I disable because they fight productivity more than they help, and the ESLint-config approach that survives a quarterly framework update.

The twelve rules that pay rent

1. @typescript-eslint/no-floating-promises

async function notifyUser() { /* ... */ }

// Lint error: floating promise.
notifyUser();

Forgetting await on an async call is one of the most common Node.js bugs. The promise resolves later, the function returns immediately, errors disappear into the void. This rule forces every promise to be awaited or explicitly discarded with void:

await notifyUser();
// or, fire-and-forget:
void notifyUser();

Single best rule on the list.

2. @typescript-eslint/await-thenable

The opposite mistake, awaiting something that isn’t a promise:

const x = await 42;  // lint error: not a thenable

Catches typos like await user when you meant await getUser(user).

3. @typescript-eslint/no-misused-promises

// Lint error: passing async function to a void callback.
button.addEventListener('click', async () => { await save(); });

The async function returns a promise the callback site does not handle. Errors disappear. The rule forces you to either wrap the call (() => { void save(); }) or fix the API (use a handler that knows about promises).

4. @typescript-eslint/switch-exhaustiveness-check

type Status = 'pending' | 'running' | 'done';

function describe(s: Status): string {
  switch (s) {
    case 'pending': return 'waiting';
    case 'running': return 'in progress';
    // missing 'done', lint error.
  }
}

When a new variant is added to the union, every switch using that union becomes a lint error until you handle it. Force-multiplies refactor confidence.

5. @typescript-eslint/no-unnecessary-condition

if (user) { ... }   // user's type is `User`, not `User | undefined`, always true.

Catches dead branches caused by mis-typed code. Common when a function’s return type changes and the callers no longer need null checks.

6. @typescript-eslint/strict-boolean-expressions

Forbid coercing values to boolean implicitly:

if (name) { ... }   // lint error if name is `string`. Empty string vs undefined?

Forces you to be explicit: if (name !== ''), if (name != null). Annoying for two days, prevents the “0 is falsy” bug for life.

7. eqeqeq (built-in)

if (x == null) { ... }   // ok with allow-null exception; otherwise force ===.

== does coercion. === does not. Use === everywhere except the one case x == null (which matches both null and undefined).

8. @typescript-eslint/no-explicit-any

function f(x: any) { ... }   // lint error.

Treat any as a code smell. When it is truly needed, the comment-disable is loud and reviewable. Pair with @typescript-eslint/no-unsafe-* rules for completeness.

9. no-console (with exceptions)

console.log('debug');   // lint error in non-test files.

Use a real logger. Save console.log for tests and scripts via overrides:

{
  "overrides": [
    { "files": ["**/*.test.ts", "scripts/**"], "rules": { "no-console": "off" } }
  ]
}

10. @typescript-eslint/prefer-nullish-coalescing

const port = config.port || 3000;

|| falls through on 0, '', false. If config.port = 0 is intentional, you get 3000. Use ??:

const port = config.port ?? 3000;   // only falls through on null/undefined.

11. import/no-cycle

// a.ts imports b.ts which imports a.ts. Circular.

Circular imports cause non-deterministic behavior at module init time. The rule catches them. Painful in a large legacy codebase; worth it.

12. unicorn/no-array-for-each (or similar)

arr.forEach(x => transform(x));   // suggest: for (const x of arr) ...

forEach is harder to debug, harder to break out of, and slightly slower. for...of is a strict upgrade. Lots of teams disagree on this one; it is the most opinionated of the twelve. Take it or leave it.

The four rules to turn off

Some popular rules waste time more than they save it.

max-lines-per-function. A 60-line function is fine if it is doing one thing clearly. The rule encourages chopping into worse abstractions.

@typescript-eslint/explicit-function-return-type. TypeScript already infers; explicit returns clutter local helpers without preventing bugs. Enable only at exported API boundaries via overrides.

prettier/prettier mixed with formatting rules. Pick Prettier or ESLint formatting, not both. Modern setups run Prettier separately.

sort-keys. Forces alphabetical key ordering, which fights logical grouping. The cost outweighs the benefit.

A working config

{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "parserOptions": { "project": "./tsconfig.json" },
  "plugins": ["@typescript-eslint", "import", "unicorn"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking",
    "plugin:import/recommended",
    "plugin:import/typescript"
  ],
  "rules": {
    "@typescript-eslint/no-floating-promises": "error",
    "@typescript-eslint/await-thenable": "error",
    "@typescript-eslint/no-misused-promises": "error",
    "@typescript-eslint/switch-exhaustiveness-check": "error",
    "@typescript-eslint/no-unnecessary-condition": "error",
    "@typescript-eslint/strict-boolean-expressions": ["error", { "allowNullableObject": true }],
    "eqeqeq": ["error", "always", { "null": "ignore" }],
    "@typescript-eslint/no-explicit-any": "error",
    "no-console": ["error", { "allow": ["warn", "error"] }],
    "@typescript-eslint/prefer-nullish-coalescing": "error",
    "import/no-cycle": "error",
    "max-lines-per-function": "off",
    "@typescript-eslint/explicit-function-return-type": "off"
  },
  "overrides": [
    {
      "files": ["**/*.test.ts", "**/__tests__/**", "scripts/**"],
      "rules": {
        "no-console": "off",
        "@typescript-eslint/no-explicit-any": "off"
      }
    }
  ]
}

The two recommended-* extends do most of the heavy lifting; the explicit rules above are the ones I know are worth flipping above defaults.

Type-aware rules need parserOptions.project

Half of the most useful rules (no-floating-promises, await-thenable, no-unnecessary-condition) require type information. They only work if parserOptions.project points at your tsconfig.json. Without it, the rules silently disable themselves and you get no errors.

Cost: ESLint runs slower because it does a TypeScript compile. About 5× slower. For most projects this is fine; lint runs in seconds, not minutes. If you hit a wall, projectService (newer ESLint plugin feature) caches the project graph between files.

Where to run lint

Three places, three different priorities:

  • Editor. Fast feedback while typing. Most rules enabled. Errors as you save.
  • Pre-commit hook (lint-staged). eslint --fix on staged files only. Fast.
  • CI. Full lint pass on every file. Strict mode. Failure blocks merge.

Don’t lint in production. Don’t lint at runtime. Lint at code-write time, code-commit time, and code-review time. After that, the artifact is the artifact.

When to disable a rule

The right disable is local and explained:

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- third-party type def is wrong
const result = (lib as any).callUntyped();

The comment explains why, not that. Reviewers can decide whether the workaround is real. A disable-next-line with no comment is a smell. Fix the underlying issue or improve the type.

/* eslint-disable */ for the whole file is almost always wrong. The exception is generated code where rules do not apply.

The takeaway

A small, sharp ESLint config catches a class of bugs that the type checker and tests do not. Async-related rules are the highest-value group; no-floating-promises alone catches more bugs than the rest of the list combined. Switch exhaustiveness, strict boolean checks, and nullish-coalescing close most of the remaining gaps.

Avoid the productivity-fighter rules (function length, alphabetical ordering, mandatory return types); they take more than they give. Run lint in your editor, pre-commit, and CI. Disable locally with a comment explaining the why.

The team that adopts this config notices, within a month, that one whole class of bugs has disappeared from production. That is what a good lint pass earns you.


A note from Yojji

The kind of code-quality discipline that catches a class of bugs at lint time (async-without-await, missing switch cases, accidental any) is the kind of long-haul engineering hygiene that decides whether a codebase still feels good at year three. It is the kind of detail Yojji’s teams put into the products they ship for clients.

Yojji is an international custom software development company founded in 2016, with teams across Europe, the US, and the UK. They specialize in the JavaScript ecosystem (React, Node.js, TypeScript), cloud platforms (AWS, Azure, GCP), and full-cycle product engineering, including the developer-tooling decisions that compound into long-term codebase health.