Sechno
Typescript

Catch the TypeScript Type Holes ESLint Misses — Practical Fixes and the satisfies Operator

Concrete patterns to find and fix TypeScript type-safety gaps that linters often miss: compiler flags, runtime checks, the new satisfies operator, and type-testing tools for reliable, maintainable code.

SSechno Team 5 min read 179 views
Catch the TypeScript Type Holes ESLint Misses — Practical Fixes and the satisfies Operator

Why ESLint isn't enough (and what to do instead)

ESLint + @typescript-eslint is a great first line of defense, but many classes of TypeScript problems come from type-system assumptions, inference quirks, or runtime inputs — and those are not always caught by linter rules. This guide walks through common "type holes", pragmatic fixes you can apply immediately, and how to use the satisfies operator to preserve literal/shape information without losing type safety.

Checklist: quick things to enable

  • Enable TypeScript strict mode in tsconfig.json: "strict": true.
  • Turn on specific flags: noImplicitAny, exactOptionalPropertyTypes, useUnknownInCatchVariables, noUncheckedIndexedAccess.
  • Add type-level tests with tsd or run a coverage tool such as type-coverage.
  • Prefer unknown over any; avoid unchecked assertions like as any.

Seven common TypeScript type holes and how to close them

1) Implicit any from untyped JSON / external inputs

Problem: JSON.parse or external APIs produce any so subsequent code silently assumes shapes.

const data = JSON.parse(body); // any
console.log(data.user.name); // runtime crash possible, no type error

Fixes:

  1. Parse as unknown and validate (Zod, io-ts, runtypes) or use a runtime guard.
  2. Use as const or satisfies when you have fixed fixtures/configs (examples below).
import { z } from 'zod';
const BodySchema = z.object({ user: z.object({ name: z.string() }) });
 
const parsed = JSON.parse(body) as unknown;
const result = BodySchema.parse(parsed); // throws or returns typed value
console.log(result.user.name); // safe, typed

2) Overly permissive generics (excess properties slip through)

Problem: Generic wrappers can widen types and silently accept extra fields.

function wrap

Fixes:

  • Add constraints on generics: <T extends Expected>.
  • Use the satisfies operator to assert shape without collapsing literal types.
type Public = { id: string; name: string };
const payload = { id: '1', name: 'x', secret: 42 } satisfies Public; // compiler checks shape
// 'payload' keeps its literal types for other inference, but is validated against Public

3) Widening of literal types in large config objects

Problem: You want the compiler to keep literal values but also validate shape. Plain objects often widen to base types.

const config = { mode: 'production', retries: 3 };
// config.mode is string not the literal 'production'

Fix:

type Config = { mode: 'production' | 'development'; retries?: number };
const config = {
  mode: 'production',
  retries: 3
} satisfies Config; // keeps literal 'production' while checking structure

4) Nullability and non-null assertions

Problem: Using ! or assuming DOM values exist leads to runtime errors.

const el = document.getElementById('app')!; // crash if missing
el.innerHTML = '...';

Fixes:

  • Narrow with runtime checks: if (!el) throw new Error('missing');.
  • Use strict null checks in tsconfig and avoid blanket non-null assertions.

5) Unsafe use of any and as unknown as T

Problem: Casts bypass the type system and hide real problems.

const result = fetchSomething() as any as MyType; // bypasses checks

Fix:

  • Introduce small runtime validators for edges where types cross the boundary.
  • Replace casts with conversions that validate and map to the desired type.

6) Catch variables typed as any

Problem: Traditional TypeScript treated catch variables as any, letting unsafe assumptions propagate.

try { ... } catch (e) {
  console.log(e.message); // no error at compile-time if 'e' is any
}

Fix: enable useUnknownInCatchVariables and narrow inside the catch.

try { ... } catch (e: unknown) {
  if (e instanceof Error) console.log(e.message);
  else console.log('Non-Error thrown');
}

7) Missing type tests in your CI

Problem: Changes can accidentally weaken types; relying on dev machines isn't enough.

Fix: Add true type tests and coverage tooling to CI. Examples below.

// package.json scripts
"scripts": {
  "typecheck": "tsc --noEmit",
  "type-tests": "tsd"
}
 
// Install
npm install -D typescript tsd type-coverage
 
// CI step
npm run typecheck && npm run type-tests && npx type-coverage --strict

Practical ESLint + TS configuration recommendations

Use both linter rules and stricter compiler flags. Example minimal config additions:

// tsconfig.json (excerpt)
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "useUnknownInCatchVariables": true
  }
}
 
// .eslintrc.js (excerpt)
module.exports = {
  "extends": ["plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking"],
  "rules": {
    "@typescript-eslint/no-unsafe-assignment": "error",
    "@typescript-eslint/no-unsafe-call": "error",
    "@typescript-eslint/no-floating-promises": "error",
    "@typescript-eslint/strict-boolean-expressions": "warn"
  }
};

Tradeoffs

  • Stricter flags and runtime validators add upfront work and some runtime cost, but prevent costly runtime bugs and make refactors safer.
  • The satisfies operator buys safety without collapsing literal types — helpful for configs and fixtures — but it's a compile-time-only check, so pair it with runtime validation for external input.
  • Type tests and coverage increase CI time; balance the strictness you enforce automatically vs. developer ergonomics.

Conclusion — practical next steps

Start by enabling strict TypeScript flags, add a few linter rules that block unsafe assignments/calls, and introduce a small set of runtime validations where data crosses trust boundaries. Use the satisfies operator for configs/fixtures to keep literal typings while ensuring shape conformance. Finally, add lightweight type tests (tsd) and a type-coverage check to CI so type regressions are caught early.

Fast checklist: strict compiler, noImplicitAny, noUncheckedIndexedAccess, useUnknownInCatchVariables, satisfies for shapes, runtime validators for external inputs, and CI type tests.

Was this helpful?

Share this post

Comments (0)

Want to join the conversation?

Log in or sign up to leave a comment and share your thoughts.

Log in to Comment