Skip to content

TypeScript code contains hundreds of references to any type, which disables type analysis #1296

@andrewkolos

Description

@andrewkolos

If we enabled @typescript-eslint/no-explicit-any: "error" and ran tsc --noImplicitAny, 381 errors would surface in production code that the currently are not flagged.

Each of these place where a type was either explicitly declared any (effectively turing off the type checker) or never specified at all (compiler couldn't infer). Each one is a hole through which a wrong type can flow into otherwise type-checked code.

Regardless of how many references of any turn out to actually be problematic, I suggest we turn on no-explicit-any, at least in non-test code. The whole purpose of the any super type, as far I am aware, is to allow gradual migration of JavaScript to TypeScript. This doesn't apply to this repo since the relevant code was written in TS to begin with. Even in places were the shape of an object cannot be known ahead of time, unknown can be used. TS's type system is very expressive--there's usually a way to keep things typed without great upfront cost even in scenarios where it is tempting to reach for any (e.g. discriminating union types, intersection types, mapped types, conditional types, template literal types, infer, etc.).

I suggest we turn on the rule sooner rather than later. The longer we put it off, the more expensive the issue becomes. I can personally say I've seen agents spending several inferences troubleshooting a bug that would have been immediately flagged with typechecking in my very brief time with this repo.

Use of any can also hide API design smells that. For example, being forced to write a very complicated or brittle type (example) often signals that an upstream type is loosely specified (e.g. a single blobby type with a bunch of optional fields was used where a discriminating union would more precisely capture that an object will match one of N known shapes at runtime).

Numbers

The following was gathered via an agent, but the full methodology is included afterwards to facilitate verification.

Category Errors
Published library code (renderers/web_core, renderers/{angular,lit,react}, renderers/markdown/markdown-it) 146
All other non-test code (samples, tools, spec eval) 235
Total 381

Test/spec code was excluded, though of course this could mask bugs as well.

Of the 381: 2 are implicit-any (compiler couldn't infer) and 379 are explicit any (written deliberately in source). Implicit-any is more or less fine here since we have strict: true in all our tsconfigs.

Library breakdown

Package Errors
renderers/web_core 99
renderers/angular 29
renderers/lit 12
renderers/react 4
renderers/markdown/markdown-it 2

Suggested fix

Enable @typescript-eslint/no-explicit-any: "error" in every workspace and gate CI on it. Fix or add // eslint-disable-next-line each existing site as a tracked decision rather than silent permission. Focus on library code first; samples/demos/tests can follow.

web_core has 99 explicit uses of any. The eslint config defends this as necessary for generic library code. However--while I have not personally reviewed all uses--I have to imagine they are replaceable with unknown + narrowing or proper generic constraints. Adding Type guards using any for the input parameter and removing as any casts that monkey-patch missing properties are probably the highest-leverage fixes we can do.

Reproduction

Repo at main commit c55a37dc, all workspaces npm installed first (otherwise tsc reports phantom errors from unresolved imports).

Implicit any per workspace (production code only):

cd <workspace>
tsc --noEmit --noImplicitAny -p tsconfig.json 2>&1 \
  | grep -vE '\.(test|spec)\.(ts|tsx)' \
  | grep -cE 'error TS(7005|7006|7008|7009|7010|7011|7015|7017|7018|7019|7022|7023|7024|7031|7032|7033|7034)'

The TS70xx codes are TypeScript's "implicitly has an 'any' type" diagnostic family.

Explicit any. The following code performs a repo-wide AST walk that counts every any keyword in production .ts/.tsx files. Save as count-any.js and run from the repo root:

// count-any.js
const ts = require('typescript');
const fs = require('fs');
const path = require('path');

const SKIP = new Set([
  'node_modules', 'dist', 'build', 'out', 'coverage',
  '.angular', '.next', '.wireit', '.git', 'worktrees',
]);
const TEST = /\.(test|spec)\.tsx?$|\/tests?\/|\/__tests__\//;

function* walk(dir) {
  for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
    if (SKIP.has(e.name) || e.name.startsWith('.')) continue;
    const p = path.join(dir, e.name);
    if (e.isDirectory()) {
      yield* walk(p);
    } else if (
      /\.tsx?$/.test(e.name) &&
      !e.name.endsWith('.d.ts') &&
      !TEST.test(p)
    ) {
      yield p;
    }
  }
}

let total = 0;
for (const file of walk(process.cwd())) {
  const sf = ts.createSourceFile(
    file,
    fs.readFileSync(file, 'utf8'),
    ts.ScriptTarget.Latest,
    /* setParentNodes */ false,
    file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
  );
  (function visit(node) {
    if (node.kind === ts.SyntaxKind.AnyKeyword) total++;
    ts.forEachChild(node, visit);
  })(sf);
}
console.log(total);
node count-any.js

Equivalent to running ESLint with @typescript-eslint/no-explicit-any: "error" and counting violations, but this script works without needing per-workspace ESLint configuration.

Metadata

Metadata

Type

No type

Projects

Status

Todo

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions