Conversation
`packages/commons` and `packages/models` both declared a `tsd` dependency and carried `*.test-types.ts` files, but nothing ever executed them: tsd discovers `*.test-d.ts`, no package had a script invoking it, and `packages/commons` had no `test:compile` target at all. Drop tsd rather than wire it up. It pins its own TypeScript (`@tsd/typescript` 5.4.5 vs. the repo's 5.7.2), force-sets `skipLibCheck: false` over the root tsconfig, ignores `tsd.typingsFile`/`tsd.testFiles` in package.json so it needs CLI flags plus a built `dist/types/index.d.ts`, and would need its own nx target. `test:compile` already exists on three packages and `nx.json` already wires it into `test`, so adding that target to commons is all it takes. No assertion library replaces it; everything is expressed in plain TypeScript. The tsd helpers do not survive a naive port -- under plain tsc, `expectType<T>(e: T)` weakens from identity to assignability and `expectNotAssignable<T>(e: any)` is a no-op -- so: - Assignability is asserted with native `satisfies`, which keeps the excess-property checks and literal inference a real call site gets. A type-level `extends` check is not equivalent: excess properties are invisible to it, which would silently drop five of the assertions here (three in `RequestType.test-types.ts`, two in `Response.test-types.ts`). Passing the literal to a helper function instead also reintroduces widening -- `status: 400` becomes `number` -- which is exactly what made the old assertions vacuous. - Exact type identity uses a local `ExpectExact<Equals<...>>` pair. `typeof` respects control-flow narrowing, including nested guards, so this works for the narrowed assertions in `Response.test-types.ts`. The four `expectNotAssignable` calls in `Response.test-types.ts` were vacuous: with a parameter typed `any` there is no contextual type, so `status: 200` widened to `number` and failed the `200` literal no matter what the assertion was meant to check. They are now `@ts-expect-error` checks on the specific offending property. Also fixes two packaging defects this touches: - `tsd` sat in `@mittwald/api-models`' runtime `dependencies`, so every consumer installed it. - Both packages compiled their unit tests and type tests into `dist`, and the emitted `*.test-types.js` imported `"tsd"` -- a devDependency, i.e. a broken import in the published output. A new build-only `tsconfig.build.json` excludes them; `test:compile` keeps using the plain `tsconfig.json`, so they are still type-checked. Verified that nothing but test files left the build output (16 files in commons, 6 in models, none added). The stale `exclude: ["test-d/**/*", "*.test.ts"]` is gone from both tsconfigs: no `test-d` directories exist, and the bare glob only matched package-root files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mfal
force-pushed
the
claude/determined-pare-d3f6ef
branch
from
September 24, 2026 08:00
92f00ef to
aed4607
Compare
mfal
marked this pull request as ready for review
September 24, 2026 08:14
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.
Problem
packages/commonsandpackages/modelsboth declared atsddependency and carried*.test-types.tsfiles, but nothing ever executed them:*.test-d.tsby default, and no package had a script invoking it —grep -rn tsd packages/*/package.jsononly ever hitdependencies.packages/commonshad notest:compiletarget (unlikegenerator,modelsandmittwald), so its type tests were not even type-checked as part of the test pipeline.Affected files:
Decision: drop tsd, add no replacement library
I got tsd running first, to decide on evidence rather than assumption. It works, but only as
tsd --typings dist/types/index.d.ts --files 'src/**/*.test-d.ts', because it ignorestsd.typingsFile/tsd.testFilesinpackage.json(those come from CLI flags only — seenode_modules/tsd/dist/lib/index.js). Beyond the plumbing:@tsd/typescript5.4.5 vs. the repo's 5.7.2), so type testswould be validated by a different compiler than the one producing the shipped
.d.ts.lib/config.jsforce-setsskipLibCheck: false, overriding the roottsconfig.json— anydependency with a sloppy
.d.tsbreaks the type tests for unrelated reasons.dist/types/index.d.ts, so atest:typestarget would needdependsOn: ["build"]plus a newtargetDefaultsentry.Against that,
test:compilealready exists on three packages andnx.jsonalready wires it intotest— so adding that target tocommonsis sufficient. No assertion library replaces tsd;everything is plain TypeScript.
How the assertions are now expressed
The tsd helpers do not survive a naive port:
tscexpectAssignable<T>(e: T)expectType<T>(e: T)expectNotAssignable<T>(e: any)Assignability → native
satisfiesvoid ({ extra: true } satisfies RequestType)keeps the excess-property checks and literalinference a real call site passing an object literal gets.
A type-level
extendscheck is not equivalent. Excess properties are invisible to it, so fiveassertions would silently stop testing anything — three in
RequestType.test-types.ts(
data: { foo: "", extra: "" },{ data: {}, headers: … },{ pathParameters: {}, headers: … })and two in
Response.test-types.ts(the nestedextra: "!"and the top-level one). Verified byrunning every one of the 22 negative assertions through
expectTypeOf(...).not.toExtend<T>():exactly those five fail to hold. (Weak-type detection does survive a type-level check — it is
part of the assignability relation — so cases like
{ extra: true }vs.RequestTypeare caughteither way. Only the excess-property check is lost.)
Handing the literal to a helper function instead reintroduces a different trap — widening:
That is precisely the bug that made the old
expectNotAssignablecalls vacuous.satisfiescannothit it, because the contextual type keeps literals literal.
Exact identity → local
ExpectExact<Equals<…>>typeofrespects control-flow narrowing, including nested guards, so this works for the narrowedassertions in
Response.test-types.ts.The
expectNotAssignablecalls were vacuousAll four in
Response.test-types.tspassed for the wrong reason: with a parameter typedanythereis no contextual type, so
status: 200widened tonumberand failed the200literal regardlessof what the assertion was meant to check. They are now real
@ts-expect-errorchecks on thespecific offending property.
Changes
packages/commons/package.json:test→"", addedtest:compile(tsc --noEmit) andtest:unit(the formertestbody) — now matchingmodels/generator.tsdremoved from both packages; no dependency added in its place.tsdremoved from@mittwald/api-models' runtimedependencies, where it was making everyconsumer install it.
tsconfig.build.jsonin both, excluding**/*.test.tsand**/*.test-types.ts.Necessary:
distwas shipping the type tests, and the emitted*.test-types.jsimported"tsd",a devDependency — a broken import in the published output.
test:compilekeeps using the plaintsconfig.json, so those files are still type-checked.exclude: ["test-d/**/*", "*.test.ts"]from both tsconfigs — notest-ddirectories exist, and the bare glob only matched package-root files.
Verification
Both pass.
commons:testnow runstest:compile+test:unitthrough thedependsOninnx.json, andyarn install --immutableis satisfied.Mutation-tested, since "green" proves nothing for type tests that were previously inert — and
one candidate helper turned out to be silently inert during development (a defaulted type parameter
is not re-checked against its constraint at the use site). Each assertion form was verified to fail
when violated:
200 | 201 | 400→200 | 201TS2344: Type 'false' does not satisfy the constraint 'true'string→string | number(supertype)TS2344— exactness, not just assignabilitystring→anyTS2344@ts-expect-errorTS2353: 'extra' does not exist in type 'RequestWithOptionalHeaders'Also confirmed the build-config change drops only test files — built once with each config and
diffed the output file lists: 16 files gone in
commons, 6 inmodels, none added, allexportsmap targets still resolve.
Out of scope
packages/generatorstill compiles its unit tests intodist(same defect, untouched here).packages/mittwald'stest:client-generation-cleanis still justgit diff --exit-code, sonx run-many -t testfails on a dirty worktree independently of any of this.🤖 Generated with Claude Code