Skip to content

Commit 2dfeffb

Browse files
a-tokyoclaude
andcommitted
feat(tsconfig): enable noUncheckedIndexedAccess in the base — 0.1.2
`strict: true` does not turn this on — it is opt-in, and it is the flag that makes `arr[i]` / `record[key]` honestly `T | undefined`. Without it TypeScript types an unchecked index as non-nullable, so code like const [row] = await db.insert(project).values(input).returning({ id: project.id }); return row.id; type-checks and then throws at runtime when the result is empty. Adopting this in a real consumer (fluxo-arc) immediately surfaced four latent bugs of exactly that shape in database writes, plus a crash in a string helper. All framework presets extend base, so react / react-native / next / nest inherit it. Also adds test/tsconfig.test.js — the tsconfig presets shipped with zero coverage. It pins the type-safety floor and asserts every preset extends base and never weakens it. Writing it caught that `nest` relaxes `isolatedModules` / `verbatimModuleSyntax`; that is a legitimate module-mechanics need (emitDecoratorMetadata + CommonJS decorator emit), not a safety weakening, so the test separates the two categories. Consumer impact: this is behaviour-affecting for anyone who EXTENDS the tsconfig presets — expect to add real guards on first adoption. Released as a patch because no consumer extends them yet (fluxo-web and fluxo-arc both consume only the ESLint and Prettier configs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d28ce54 commit 2dfeffb

4 files changed

Lines changed: 103 additions & 2 deletions

File tree

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Sonar issues before they reach CI. Five stack presets — **plain JS/TS**, **Rea
2929
Distributed via GitHub URL, pinned to a tag (no build step runs on install — raw ESM):
3030

3131
```bash
32-
npm i -D "github:zoldytech/javascript#0.1.1"
32+
npm i -D "github:zoldytech/javascript#0.1.2"
3333
```
3434

3535
## Usage
@@ -118,6 +118,19 @@ Extend the matching base in `tsconfig.json`:
118118
Available: `tsconfig/base.json`, `/react.json`, `/react-native.json`, `/next.json`, `/nest.json`
119119
(framework ones extend base).
120120

121+
The base goes beyond `strict`. Most notably it sets **`noUncheckedIndexedAccess`**, which `strict`
122+
does _not_ enable: it makes `arr[i]` and `record[key]` honestly `T | undefined`. Without it,
123+
TypeScript types this as non-nullable and the bug ships:
124+
125+
```ts
126+
const [row] = await db.insert(project).values(input).returning({ id: project.id });
127+
return row.id; // `row` is `T | undefined` at runtime; without the flag TS says it is always `T`
128+
```
129+
130+
Expect to add real guards when you first adopt it — that is the flag doing its job. Also on:
131+
`noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noFallthroughCasesInSwitch`,
132+
`verbatimModuleSyntax`, `isolatedModules`, `forceConsistentCasingInFileNames`.
133+
121134
## Git hooks (recommended, not shipped)
122135

123136
This package ships no hooks. Wire the recommended pipeline in your own repo:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zoldytech/javascript",
33
"type": "module",
4-
"version": "0.1.1",
4+
"version": "0.1.2",
55
"private": true,
66
"description": "Zoldytech's JavaScript/TypeScript standards: SonarQube-compatible ESLint presets (Next.js, NestJS, React+Vite, plain JS/TS) plus shared Prettier and tsconfig configs.",
77
"license": "MIT",

test/tsconfig.test.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// The tsconfig presets are a published contract, so they get the same guard the ESLint presets do.
2+
//
3+
// `strict: true` alone does NOT enable `noUncheckedIndexedAccess` — it is opt-in, and it is the one
4+
// flag that makes `arr[i]` / `record[key]` honestly `T | undefined`. Without it TypeScript silently
5+
// types `const [row] = await db.insert(...).returning()` as non-nullable, so `row.id` type-checks
6+
// and throws at runtime on an empty result. Assert it (and the rest of the floor) so a future edit
7+
// cannot quietly weaken the bar.
8+
9+
import assert from 'node:assert/strict';
10+
import { readFileSync } from 'node:fs';
11+
import path from 'node:path';
12+
import { test } from 'node:test';
13+
import { fileURLToPath } from 'node:url';
14+
15+
const here = path.dirname(fileURLToPath(import.meta.url));
16+
const readConfig = (name) =>
17+
JSON.parse(readFileSync(path.join(here, '..', 'tsconfig', name), 'utf8'));
18+
19+
const PRESETS = ['react.json', 'react-native.json', 'next.json', 'nest.json'];
20+
21+
/**
22+
* The type-SAFETY floor: what makes the compiler catch bugs. No preset may weaken these — doing so
23+
* is a breaking change to consumers.
24+
*/
25+
const SAFETY_FLOOR = {
26+
strict: true,
27+
noUncheckedIndexedAccess: true,
28+
noImplicitReturns: true,
29+
noFallthroughCasesInSwitch: true,
30+
noUnusedLocals: true,
31+
noUnusedParameters: true,
32+
forceConsistentCasingInFileNames: true,
33+
};
34+
35+
/**
36+
* Module MECHANICS: the base sets these, but a framework may legitimately override them (NestJS
37+
* turns `isolatedModules`/`verbatimModuleSyntax` off because `emitDecoratorMetadata` needs the
38+
* full-program, CommonJS decorator emit). They are not a safety bar, so they are asserted on the
39+
* base only — never on the presets.
40+
*/
41+
const BASE_MECHANICS = {
42+
isolatedModules: true,
43+
verbatimModuleSyntax: true,
44+
};
45+
46+
test('base: sets the full type-safety floor', () => {
47+
const { compilerOptions } = readConfig('base.json');
48+
for (const [flag, expected] of Object.entries(SAFETY_FLOOR)) {
49+
assert.equal(
50+
compilerOptions[flag],
51+
expected,
52+
`tsconfig/base.json must set ${flag}: ${expected}`
53+
);
54+
}
55+
});
56+
57+
test('base: sets the module mechanics defaults', () => {
58+
const { compilerOptions } = readConfig('base.json');
59+
for (const [flag, expected] of Object.entries(BASE_MECHANICS)) {
60+
assert.equal(
61+
compilerOptions[flag],
62+
expected,
63+
`tsconfig/base.json must set ${flag}: ${expected}`
64+
);
65+
}
66+
});
67+
68+
// The framework presets inherit the floor rather than restating it, so `extends` IS the contract:
69+
// if one stops extending base, it silently loses noUncheckedIndexedAccess and the rest.
70+
for (const preset of PRESETS) {
71+
test(`${preset}: extends base`, () => {
72+
assert.equal(readConfig(preset).extends, './base.json', `${preset} must extend ./base.json`);
73+
});
74+
75+
test(`${preset}: does not weaken the type-safety floor`, () => {
76+
const { compilerOptions = {} } = readConfig(preset);
77+
for (const [flag, expected] of Object.entries(SAFETY_FLOOR)) {
78+
if (flag in compilerOptions) {
79+
assert.equal(
80+
compilerOptions[flag],
81+
expected,
82+
`${preset} must not weaken ${flag} inherited from base`
83+
);
84+
}
85+
}
86+
});
87+
}

tsconfig/base.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"noImplicitAny": true,
99
"noImplicitReturns": true,
1010
"noFallthroughCasesInSwitch": true,
11+
"noUncheckedIndexedAccess": true,
1112
"noUnusedLocals": true,
1213
"noUnusedParameters": true,
1314
"forceConsistentCasingInFileNames": true,

0 commit comments

Comments
 (0)