Prerequisites
Reproduction url
https://gist.github.com/atheer-alhamami/e7793e475da8f531b2305724eb9646f7
Reproduction access
Description of the issue
Follow-up to #1669, which fixed namespace imports used as object property values. That fix handles a nested spread when the enclosing property key is an identifier, but not when it is a string literal. The two are semantically identical references, so per the Namespace Imports guide (const spread = { ...NS } → all exports used) both should count as used.
| Key |
Value |
| Knip version |
6.34.0 (also reproduced on 6.32.2) |
| Node.js |
v24 |
| OS |
macOS |
Reproduction
index.ts — the two objects differ only in the quoting of one key:
import * as identNs from './identKey';
import * as literalNs from './literalKey';
// Identifier key: the nested spread is found, identAlpha counts as used.
export const withIdentifierKey = { queue: { handlers: { ...identNs } } };
// String-literal key: the nested spread is never visited, so literalAlpha
// is reported as an unused export even though the reference is identical.
export const withLiteralKey = { 'my-queue': { handlers: { ...literalNs } } };
npx knip reports:
Unused exports (1)
literalAlpha literalNs function literalKey.ts:1:17
identAlpha is correctly treated as used. literalAlpha is not.
--trace-export literalAlpha shows the reference being resolved and then rejected:
literalKey.ts:literalAlpha
└── index.ts:importNS[literalNs.literalAlpha] ✗
Likely cause
packages/knip/src/typescript/visitors/exports.ts, in the findSpreads helper (~line 137). The recursive branch only descends into properties whose key is an Identifier:
} else if (
prop.type === 'Property' &&
prop.value?.type === 'ObjectExpression' &&
prop.key?.type === 'Identifier' // string-literal keys are never recursed
) {
findSpreads(prop.value, [...path, prop.key.name]);
}
With a quoted key the walk stops one level above the SpreadElement, so the spread is never registered. Accepting a Literal/StringLiteral key here (using its value for the path segment) would appear to cover it, though I have not tested a patch.
Why it matters in practice
This shows up wherever a registry object is keyed by strings that are not valid identifiers. In our case it is a Temporal worker registry keyed by task-queue names:
export const workerConfigs: Record<string, WorkerQueueConfig> = {
'outbound-calls': {
activities: { ...callActivities, ...telemetryRecordActivities },
// ...
},
'qa-generation': { /* ... */ },
};
Every hyphenated queue name is a string literal, so every activity spread beneath one reports as unused. Renaming the keys is not an option — they are the queue identifiers the workers register under — so the only workaround we found is declaring the module as an entry, which then suppresses genuinely dead exports in it too.
Prerequisites
Reproduction url
https://gist.github.com/atheer-alhamami/e7793e475da8f531b2305724eb9646f7
Reproduction access
Description of the issue
Follow-up to #1669, which fixed namespace imports used as object property values. That fix handles a nested spread when the enclosing property key is an identifier, but not when it is a string literal. The two are semantically identical references, so per the Namespace Imports guide (
const spread = { ...NS }→ all exports used) both should count as used.Reproduction
index.ts— the two objects differ only in the quoting of one key:npx knipreports:identAlphais correctly treated as used.literalAlphais not.--trace-export literalAlphashows the reference being resolved and then rejected:Likely cause
packages/knip/src/typescript/visitors/exports.ts, in thefindSpreadshelper (~line 137). The recursive branch only descends into properties whose key is anIdentifier:With a quoted key the walk stops one level above the
SpreadElement, so the spread is never registered. Accepting aLiteral/StringLiteralkey here (using itsvaluefor the path segment) would appear to cover it, though I have not tested a patch.Why it matters in practice
This shows up wherever a registry object is keyed by strings that are not valid identifiers. In our case it is a Temporal worker registry keyed by task-queue names:
Every hyphenated queue name is a string literal, so every activity spread beneath one reports as unused. Renaming the keys is not an option — they are the queue identifiers the workers register under — so the only workaround we found is declaring the module as an
entry, which then suppresses genuinely dead exports in it too.