The test suite verifies that the reference check resolves all of these — none of them produce false positives:
v.prop) and string-literal element access (v['prop'])({ count: shelf.count } = source) and ({ count } = source) both read source’s count. A pattern is written as a literal and has no contextual type to be read against, so the checker is asked what it matched the pattern to — which covers the nested and array forms (({ badge: { count } } = card), [{ count }] = cards) and the pattern a for…of bindsconst { plate } = await import('./recipes'), and the same pattern in the callback import('./recipes').then(({ plate }) => …) hands the module toPartial<T>, Pick<T, 'k'>) and interface inheritanceexport default { … }, export default class { … }, an arrow, a bare value. There is no identifier to search for, so the search runs from the symbol the binder left on the declaration, under the one name the module system gives it. A default import, an import { default as … }, and a barrel’s export { default as … } all count as usage, through as many re-exports as they take. The members of such a class answer for themselves too, on the same escape rules a named class lives byshelf.count = 1, shelf.count++ on a line of its own — earns the write-only verdict instead of counting as used, unless a read reaches it through some other declaration. A delete shelf.count reads nothing either, and counts the same way, and so does the far side of a destructuring assignment: ({ count: shelf.count } = source) puts a value there, it does not take one'name' in v counts as usage of exactly that propertyimplements or extends — TypeScript merges those reference groupscfg.outer.inner, cfg['outer'].inner, or const { outer } = cfg followed by outer.inner — counts on the nested literal, not on the object around itcfg.rows[0].id, cfg.rows.map(r => r.id), for (const r of cfg.rows), cfg.rows.filter(…).map(…) — counts on the element literals, and a name any one of them holds a read on is alive on all of them. An array bound at the top level (const rows = [ … ]) is read the same wayreturn of the same function counts on all of them: two branches of one shape collapse to a single set of declarations, and the branch the checker dropped is alive on the reads the other one holds. When no branch reads it, the death is told once, on the branch that writes it first — the copies are two edits and one fact, and --fix comes back for the next copy on its ownSome consumption is invisible to static reference search. Rather than guess, norefs suppresses those findings entirely:
keyof-targeted types: when keyof T appears anywhere, code is enumerating or indexing T’s keys dynamically. All of T’s members are skipped.Object.keys/values/entries/assign, JSON.stringify, structuredClone, or Reflect.ownKeys, iterated with for...in, or probed with a dynamic key in v marks its whole type as dynamically consumed.manifest[section] names a member without writing it down, and the key’s type says how many are in reach. A union of string literals — 'dependencies' | 'devDependencies' — reaches exactly those members and marks them used, leaving the rest of the type answerable. A string the type cannot pin down reaches every member, and the whole type is skipped. An index that is a number names no member at all. A key that fills the member in — manifest[section] = […], delete manifest[section] — is a write and counts as one, so a member the code only ever assigns by computed key is reported rather than credited.dump(recipe) is as dynamic as Object.keys(recipe) when dump is the function that makes that call. The sink standing inside the helper sees only the type parameter — the concrete type is back at the call site, and that is where norefs looks. It follows the relaying parameter through as many hops as the forwarding goes, and skips the type at each call site it reaches. Only the parameter that carries the value relays; the ones beside it answer for their members as usual. A relay handed on as a value rather than called — rows.forEach(dump) — writes no argument down, so the position it lands in answers instead: whatever type that position expects at the relaying parameter is what will arrive. A relay also answers to every name it is given: const scan = dump and const pantry = { sift: dump } are the same relay, and the calls behind the second name are read the same way. Whether that name declares a type of its own changes nothing — a relay takes whatever it is handed, so its own parameter is wide by construction, and only the call sites say what arrives.write-only verdict asks whether this run holds that callee’s body. A function the project declares and implements is read where the reference search already looks, and the verdict stands. A body norefs does not hold — a package, an ambient declaration, an overload with no implementation — is not: visit(doc, { FragmentSpread: visitor }) hands enter to a library that calls it, and expect(result).toEqual({ greeting: 'Hello' }) writes greeting in order to compare it. Both members keep the answer they had before. A literal assigned into a stored slot, held in a local binding, or returned is read back through the type that declares the member, and that is a read this run can see.as/satisfies cast whose value is spread into a combined array or passed bare — its properties may be consumed without any per-property reference. The affected type literal is skipped.throw is the most complete departure of the lot. The value leaves the call stack for whoever catches it, catch (error) types that value unknown, and every property the catcher reads is a read no reference search finds. An error object a library builds for its callers to read is a whole idiom shaped like this, so the type the throw hands over is skipped — including the type behind a parameter some function throws.extends/implements override (interface Derived extends Base { items: DerivedItem[] }) or a type predicate (v is Derived) forces one type to stay assignable to another, so the required members of the base shape are kept even when nothing reads them. A union is the fourth position of the same kind: { value: T; issues?: undefined } | { issues: Issue[]; value?: undefined } is how an exclusive union is written, each arm naming the other’s member so a value is one shape or the other and never a mixture. A guard narrows to one arm and the read lands there alone, while deleting the placeholder beside it changes what the type accepts. So a name two arms of one union both declare is kept on both.extends clause, including through an alias like Extract<Schedule, { type: 'DAILY' }>; a predicate’s asserted type (r is Recipe & { id: string }); and a written type argument against a literal constraint (pickFirst<Row>(…) where T extends { id: string }). A filter can also name a property one level in — Extract<Event, { payload: { kind: 'RENAME' } }> — and the nested name is read on whatever type the property holds, not on the type around it. An inferred type argument needs no rule — the value goes into the call whole, and the escape check already stops there. A constraint written in terms of another type parameter — Defaults extends Omit<Required<Options>, …> — is a shape only instantiation settles: it plainly requires members, and which ones is written nowhere, so every member of the argument counts as read..d.ts a package ships describes code the project did not write, and is never anybody’s finding. The project’s own are source, and their exported types and members answer like any module’s — a manifest that publishes one (types: './index.d.ts') names an entry point, and a package imported from one is imported. Two claims are held back there. A declaration is never called over-exported, and no fix ever edits one — the export keywords are what make the file a module rather than a script of globals, so dropping the last of them moves every declaration beside it into the global scope, and the file still compiles.atom/index.js beside atom/index.d.ts is one module written twice. Every import of it resolves to the declaration — that is what a declaration is for — so the implementation’s own exports and shapes collect no reference however heavily the module is used. The declaration is where the question is asked and answered; the file beside it stays quiet.return new StoreImpl() from a function typed as interface Store, with no implements clause — every call goes through the interface and the class members collect zero references while being used at runtime. The whole class is skipped, along with its base classes and any class whose instances only leave through methods of such a class. Declaring implements restores tracking.keyof typeof E, Object.values(E), for...in, and reverse mapping or computed lookup (E[x]) all reach members without per-member references. The whole enum is skipped.return statements are several shapes of one return value, and each literal answers for its own members. A return of a variable, a call, or nothing at all puts a shape here that this check cannot read, and a read of the value could land on that shape instead. The whole function is left alone.rows.map(send), save(rows), [...rows] and rows.sort() all carry a card somewhere no reference search follows. The elements are read one property at a time only through a callback written on the spot (forEach, map, flatMap, some, every, findIndex, findLastIndex, filter, find), a for…of binding, an index, or .length — and the binding each of those yields has to keep the element local in turn. Every other way of touching the array leaves all of its elements quiet. An array holding anything but object literals is left alone as a whole.cfg.outer passed bare, serialized, enumerated, or indexed with a computed key carries the whole inner shape with it, and a member of that shape can then be read with nothing to show for it. The descent stops at that property, and everything under it stays quiet. A property nothing reads stops it as well — that property is the finding, and the members below it would tell one death twice. A property whose shape loses every member is the finding for the same reason: one death, told once, on the node a reader would delete. A nested literal with a declared shape ({ … } satisfies Portions) hands off to the type collectors, exactly as a declared const object does.Object.values(Timeouts), a spread, an index with a computed key, or the binding passed on whole all read every member in one go, and each silences that declaration. A 'name' in Timeouts probe is the exception: it names one key, so it marks that key used and leaves the rest reportable. A const object with a declared type — an annotation or a satisfies — is skipped here as well, because the type that declares the shape is what the type collectors already report. A binding that loses every member is one finding on the binding, not one per member, whenever something still reads it: that read would outlive the members, and only you know what it was for. A binding nothing reads at all is different — the members are reported one by one, and --fix takes the whole declaration.src/routes/**, Next.js’s app/**/page.tsx and route.ts, Astro’s src/pages, SvelteKit’s +page.svelte, vitepress’s *.data.ts loaders and *.paths.ts routes — no import names any of them, and no manifest does either, so a run over one of those projects reports the whole route tree. norefs reads what a build writes down, and a convention is written down nowhere; knip’s framework plugins are the reason to run both tools. Until then, --entry src/routes — the route root itself — is the one line that settles it. A Storybook story is the same shape with one difference: .storybook/main.ts writes the glob down, so a story it lists is an entry point and its named exports are never reported.--fix leaves it, because the probe that vouches for a fix never held that file either, and “Verified” would be a green line over a deletion nothing witnessed. --fix-unsafe is where you say otherwise. Put the tests in a tsconfig — a second one, listed with -p, is enough — and the member answers like any other."./*" has no private modules. Every file the pattern matches is an entry point, so its exports are public API, and nothing at file or export level is reported anywhere the pattern reaches. That is what the package’s own manifest says it publishes; the member checks still answer inside those files. It stops there: a pattern names no file in particular, so it never decides which code ships. A gulpfile.ts beside the sources matches "./*" as readily as they do, and the dependency section would have called its build tools things the install needs.apply(dump, recipe), where apply takes a Function or a (o: object) => void — says nothing about recipe. The position is what norefs reads, and this one promises only object; the relay and the value meet inside the callee’s body, where no type is written down. A parameter declared with the signature that arrives ((r: Recipe) => void) restores it.({ 'count': t.count } = src)) resolves to no member, so the read it holds is an unverified name match rather than a reference. The member stays reported as written, never as dead.export const make = () => ({ … }) publishes a shape no type reference points at. Annotate the declaration and the shape is covered.vite-env.d.ts, or a file whose only content is declare module '*.svg'. The .d.ts files a run holds are the ones an import reached by name.drizzle-kit/node_modules/drizzle-orm is a symlink to drizzle-orm/dist, so the reads land on declarations the run never holds, and the source members they belong to look unread. Point the run at the source — or expect those members to be reported.import './x' for its side effects counts as used when its importer is reachable, even if nothing else touches it. That is the safe reading.--entry. Run norefs entries to see what was found before reaching for the flag. A framework’s route convention is the familiar case; an ORM is the one that swamps a report. entities: [__dirname + "/entity/*{.js,.ts}"] registers a whole tree by a glob the code builds, and every entity class, subscriber and naming strategy under it is loaded by a name written nowhere. Run typeorm’s own test suite through norefs and that is all you see. Point --entry at the directory, or add it to ignore.ignoreDependencies. A binary named in a package.json script is read; anywhere else is not.Some findings point at inline types with no name to anchor them — a {x, y} parameter type on an anonymous callback, for instance. These are the most false-positive-prone: TypeScript’s reference search loses track of a value forwarded via shorthand into a differently-declared structural type, because the read then resolves to the other declaration. By default norefs reports only findings tied to a named interface, type alias, or function; pass --anon to include the anonymous ones too:
norefs --anon