Static flow analysis for TypeScript
anytypes. Finds the few sources responsible for most of your type erosion.
TypeScript’s any is a silent type-safety killer. Existing tools (type-coverage, ESLint’s no-explicit-any, tsc --noImplicitAny) tell you where any exists — few tell you where it originates, how far it spreads, or which few fixes would do the most good.
A single any in a utility can propagate through assignments, destructuring, and function returns, turning many downstream symbols into untyped code. any-map models the project as a directed graph of type-flow, traces any from sources to infected graph nodes, and ranks fix order with blast radius and a greedy set-cover pass.
Published on npm: any-map v1.4.0. Algorithm details and design notes: PLAN.md.
Current release: 1.4.0 preserves intermediate re-export hops in the flow graph, improves explicit : any function return propagation into downstream callers, and fixes untyped-import double-counting for local TypeScript exports. The 1.3.0 line added any-map diff <base> <head> for merge-base branch comparisons.
Recent usage: 235 npm downloads from 2026-03-27 through 2026-04-25.
| Command | Purpose |
|---|---|
any-map diff <base> <head> [path] |
Compare branch-introduced any deltas for touched files (table / JSON). |
any-map scan [path] |
Analyze a TS project; table / JSON / DOT; filters + CI thresholds. |
any-map trace <loc> [path] |
Print type-flow paths from each any source to the symbol at loc (file:line:col). |
any-map graph [path] |
Emit the project type-flow graph as Graphviz DOT (-o out.dot or stdout). |
any-map scan: --format table|json|dot (or legacy --json), --dump-graph (JSON graph snapshot), --top N (limits both the greedy fix-order table and the blast-ranked table, and the matching JSON arrays; --fail-coverage still uses the full greedy run), --source-kinds, --ignore (comma-separated picomatch globs), --fail-above N, --fail-coverage P (cumulative % from the greedy run must be ≥ P — see PLAN.md for edge cases). CI: .github/actions/any-map-scan/action.yml (npx any-map@… scan . ${{ inputs.args }}).
Direct intra-project flow currently includes import bindings, re-export chains, property/index reads, plain assignments, and resolved cross-file call edges, so any can propagate through chains like export default value -> export { value as renamed } -> import x -> box.payload -> consume(x).
Inference-only kinds (implicit-param, untyped-return, untyped-import, catch-binding) are not reported for plain .js/.jsx/.mjs/.cjs where TypeScript often infers any without the developer “choosing” it — so source counts and set-cover stay meaningful on mixed TS/JS repos. Written any and other non–inference-only classifiers still apply where applicable.
| Repo | Project files | any sources |
Infected nodes | Top blast* | Top-3 greedy cum. % |
|---|---|---|---|---|---|
| colinhacks/zod | 357 | 834 | 457 | 13 | 5% |
| pmndrs/zustand | 30 | 131 | 52 | 2 | 12% |
| immerjs/immer | 16 | 138 | 121 | 17 | 21% |
| sindresorhus/ky | 29 | 18 | 5 | 2 | 80% |
*Highest blast-radius among ranked sources (tie broken by sort order). These are snapshot runs against the default repo roots on April 26, 2026. Repos that extend shared tsconfig packages may need pnpm install before scanning so TypeScript can resolve the config chain.
Takeaway: Even among TS-native repos, top-3 greedy coverage ranges from 5% to 80%. Raw any count alone does not tell you whether a codebase “rewards” a few fixes; overlap and graph shape dominate.
Implication: “Fixing these 3 any sources would restore type safety for most of the repo” is sometimes true and often false. any-map is most useful when it shows you that the long tail is real, not when it flatters you with a single magic percentage. For a larger, more entangled stress-test, the TypeORM sample below remains a good illustration of the same point.
any-map trace src/foo.ts:12:5 prints forward hops (reason per edge) from each source to the traced binding; use --json for machine-readable TraceReport.
any-map diff main HEAD --format table computes the merge-base of main and HEAD, runs full scans for both revisions, and reports only the any deltas for touched files by default. If the diff touches tsconfig*.json, package.json, lockfiles, or .d.ts files inside the selected scan root, it automatically falls back to a full-project delta.
$ any-map scan ./typeorm --top 10
Scanning 3336 project files...
Found 1460 any sources, 908 infected graph nodes.
Fix order (greedy set-cover)
Pick Cum.% +Nodes Blast Bl# File Line:Col Kind Name
1 9 80 80 2 src/util/TreeRepositoryUtils.ts 70:40 explicit-any childEntity
2 13 38 38 25 src/util/ApplyValueTransformers.ts 5:12 untyped-return transformFrom
3 16 24 28 41 src/metadata/EntityMetadata.ts 574:13 explicit-any ret
4 17 12 12 42 src/metadata/EntityListenerMetadata.ts 81:5 untyped-return execute
5 18 9 9 44 src/driver/postgres/PostgresDriver.ts 486:38 explicit-any extensionsMetadata
6 19 5 5 46 src/query-builder/RelationLoader.ts 517:28 explicit-any value
7 19 4 4 47 src/driver/cockroachdb/CockroachQueryRunner.ts 3294:23 untyped-return getSchemaFromKey
8 19 4 4 49 src/driver/sap/SapQueryRunner.ts 2846:19 untyped-return getSchemaFromKey
9 20 4 4 50 src/driver/sqlserver/SqlServerQueryRunner.ts 3273:23 untyped-return getSchemaFromKey
10 20 4 4 51 test/github-issues/4219/shim.ts 1:5 explicit-any _Shim
By blast radius
Rank Blast File Line:Col Kind Name
1 80 src/query-builder/SelectQueryBuilder.ts 1764:15 as-any result
2 80 src/util/TreeRepositoryUtils.ts 70:40 explicit-any childEntity
3 79 src/entity-manager/MongoEntityManager.ts 1271:9 explicit-any idMap
4 79 src/metadata/ColumnMetadata.ts 917:24 explicit-any entity
5 79 src/persistence/tree/NestedSetSubjectExecutor.ts 339:9 explicit-any parent
6 79 src/util/TreeRepositoryUtils.ts 47:9 explicit-any entity
7 79 src/util/TreeRepositoryUtils.ts 86:9 explicit-any entity
8 78 src/driver/aurora-mysql/AuroraMysqlDriver.ts 544:28 explicit-any value
9 78 src/driver/aurora-postgres/AuroraPostgresDriver.ts 139:28 explicit-any value
10 78 src/driver/cockroachdb/CockroachDriver.ts 407:28 explicit-any value
(Tables match a real run. Install dependencies in the TypeORM clone before scanning.)
- Classify every
anysource (explicit: any,as any, untyped imports, untyped returns,catch (e), implicit params — see PLAN.md). - Build a directed graph where each edge represents type flow (
const a = b→b→a, plus direct intra-project call/import edges). - Propagate from each source with forward BFS; nodes track
infectedBysource ids. - Rank by blast radius; run greedy set-cover over infected nodes for fix order and cumulative %.
- Emit table, JSON, or DOT.
Details: PLAN.md §5.
| Tool | What it tells you | What any-map adds |
|---|---|---|
tsc --noImplicitAny |
Where any is inferred |
— |
@typescript-eslint/no-explicit-any |
Where any is written |
— |
type-coverage |
% of typed identifiers | — |
| any-map | Source → infection graph, blast, rank | Propagation + greedy order + trace |
CHANGELOG.md · PLAN.md (algorithms, scope, test strategy).
- No full inference through generics / conditionals / distributive types (surface at usage only).
- No full callsite-sensitive interprocedural analysis across complex re-export chains, dynamic dispatch, or overload/generic specialization; direct resolved callees plus import/re-export hops are followed across project files.
- No auto-fix, no LSP, no git history — see PLAN.md §9.
Future direction (post–v1 scope): broaden cross-module flow beyond direct import/re-export hops with deeper callsite sensitivity, overload/generic awareness, and more complex expression forms.
- NPM on CI:
.github/workflows/release.ymlpublishes through npm trusted publishing (OIDC) from GitHub Actions; no long-livedNPM_TOKENis required. Keep the npm trusted publisher config aligned with this repo and workflow filename. - Reusable action:
.github/actions/any-map-scannow supports bothscananddiff. Usecommand: diff,base-ref,head-ref, and optionalpath/argsto review branch deltas in PR workflows. - Releases: tag and GitHub Release should match the version published to npm (see the release workflow).
pnpm install
pnpm build
pnpm test
pnpm lint