Enable strictNullChecks and noImplicitAny, and lint the test tree - #12
Merged
Conversation
tsconfig.json declared "strict": true and then switched two of its members back off, so the strictness the config appeared to promise was not the one in effect. Both are now on, and eslint covers test/**/*.ts alongside src/**/*.ts, with parserOptions.project listing both tsconfigs so type-aware rules resolve there. Yield: 0 errors in src/, 5 in test/ -- all the same shape, a test reading a deliberately undeclared key to assert that the parse boundary preserves unknown keys. Each is resolved by making the read explicit (`as unknown as Record<string, unknown>`) rather than by weakening a type or silencing the check. The compiler rejected the single-step cast on two of them and named the remedy; that form is used uniformly so identical intent reads identically. Mutation-tested: switching the boundary from z.looseObject to z.object turns all five red, so they still assert what they claim. Enabling noImplicitAny alone reports two errors that both flags together do not: without strictNullChecks a `null` literal infers as `any`. The half-configuration is worse than either end, which is why these land as one change rather than staged. One consequence worth calling out, since it changes a published type. The inferred output of the Idempotency response body was `string`, while the runtime schema accepts and preserves `null` and the hand-written interface declares `string | null`. Under the old setting the union collapsed, so the schema's inferred type contradicted both the runtime and the interface beside it; it now reads `string | null` and the three agree. Every emitted .js is unchanged, and that is the only type-level difference in the entire build output. Documentation that described the old configuration is corrected in the same commit, including the rationale for the requiredKey wrapper, which existed only to work around the inference collapse. The wrapper is retained rather than removed here: it has two call sites and removing it changes the errors they report, which deserves its own change and tests rather than arriving as a side effect of a compiler flag.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
tsconfig.jsondeclared"strict": trueand then switched two of its members back off, so the strictness the config appeared to promise was not the one in effect. Both are now on, and ESLint coverstest/**/*.tsalongsidesrc/**/*.ts.Every emitted
.jsin the build output is unchanged. There is exactly one type-level difference in the entirelib/, described below.Measured yield
Against the merged tree, before any fix:
src/errorstest/errorsstrictNullChecks: truenoImplicitAny: trueThe non-monotonicity is the interesting part and it drove the packaging.
noImplicitAnyalone reports two errors that both flags together do not: withoutstrictNullChecks, anullliteral infers asany, so{body: null}tripsTS7018. With both on,nullinfers asnulland the error is correctly absent. The half-configuration is worse than either end, which is why this lands as one change rather than the three staged PRs originally planned.The 5 real errors, and how they were resolved
All five are the same shape — a test reading a deliberately undeclared key to assert that the parse boundary preserves unknown keys rather than dropping them:
Resolved by making the read explicit —
as unknown as Record<string, unknown>— not by adding an index signature, casting toany, or disabling the rule. Each of those would have reproduced the exact permissiveness the flags exist to remove, while looking like the task was done.The compiler rejected the single-step
as Record<string, unknown>on two of the five (TS2352, "neither type sufficiently overlaps") and named theunknown-first remedy. That form is applied uniformly to all five so identical intent reads identically.Mutation-tested, because a rewritten test is exactly where a vacuous one gets introduced: switching the parse boundary from
z.looseObjecttoz.object(silently strips unknown keys) turns all five red — 18 failures, 15 files. Reverted, back to 903 passing.One published type changes, and it is a correction
The
Idempotencyresponse body isrequiredKey(z.string().nullable(), …). Under the old setting thestring | nullunion collapsed, so the schema's inferred output type claimedstring— while the runtime accepts and preservesnull, and the hand-writtenInterfacebeside it correctly declaresstring | null.So the schema's inferred type contradicted both the runtime and the interface. It now reads
string | nulland all three agree. Verified at runtime with a positive control:Consumers using the hand-written
Interfacewere always correct and are unaffected. Anyone deriving from the schema was being told a value the runtime produces cannot occur.Documentation corrected in the same commit
A change owns what it makes stale. Six places described the old configuration as current, including the rationale for the
requiredKeywrapper — which existed solely to work around the inference collapse.That collapse is now gone, verified before/after:
z.object({u: z.union([…])})accepted{}under the old setting (0 errors) and correctly reportsTS2741under the new one.The wrapper is retained, not removed. It has two call sites, and removing it changes the errors they report. That deserves its own change with its own tests rather than arriving as a side effect of a compiler-flag change. Its documentation now states it is obsolete for its original purpose and should not be used for new code.
Verification
Positive controls, since a green gate from an untested probe is not evidence:
const v: string | null = null; return v.length→TS18047, as requiredlib/comparison itself was controlled by injecting a declaration and confirming detectionNo attribution trailers: 0 in commit messages (grep positive-controlled, returns 1 on a synthetic), single identity for both author and committer.
Note for the
memberOffollow-upWritten under the stricter compiler, as intended.
requiredKeyshould not be used for it — see its updated documentation.