tsc TS18046: 'x' is of type 'unknown' in CI
TS18046 fires when you operate on a value typed unknown without narrowing it. Under strict mode, caught error variables are unknown, so reading err.message directly is an error.
What this error means
tsc fails with "error TS18046: 'err' is of type 'unknown'." inside a catch block or anywhere an unknown value is used before being narrowed.
src/run.ts:6:25 - error TS18046: 'err' is of type 'unknown'.
6 console.error('failed', err.message);
~~~Diagnose it: which tsconfig and which compiler?
A TypeScript error that appears only in CI usually means the runner is compiling with a different config or a different compiler version than your editor. Your editor uses the workspace TypeScript and the nearest tsconfig.json; CI uses whatever the lockfile resolved and whatever config the build script names.
# what CI will actually use
npx tsc --version
npx tsc --showConfig | head -40
# which files are in the program (a missing include is a common cause)
npx tsc --listFiles | wc -l
# type-check only, no emit, same as most CI gates
npx tsc --noEmitCommon causes
A catch variable is unknown under strict mode
useUnknownInCatchVariables (on under strict) types caught errors as unknown, so accessing properties without a guard is an error.
An unknown value used without narrowing
Values from JSON.parse, generic boundaries, or APIs typed unknown must be narrowed before use.
How to fix it
Narrow the value before using it
- Check the value with
instanceofor a type guard. - Access properties only inside the narrowed branch.
- Re-run tsc to confirm the unknown is handled.
try {
run();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error('failed', message);
}Use a typed helper to extract a message
Centralize narrowing in a helper so catch blocks stay clean and consistent.
function toMessage(e: unknown): string {
return e instanceof Error ? e.message : String(e);
}Pin the compiler so unrelated updates cannot break the gate
TypeScript adds errors in minor releases. An unpinned compiler turns a routine dependency update into a red build on code nobody touched, which is the most common false alarm in a TypeScript CI pipeline.
// package.json
{
"devDependencies": {
"typescript": "5.6.3" // exact, not ^5.6.3
}
}How to prevent it
- Narrow caught errors with
instanceof Errorbefore use. - Treat
unknownfrom parsing and APIs as needing a guard. - Keep
strict(and useUnknownInCatchVariables) consistent across configs.