feat: support TypeScript 6.0 - #1878
Conversation
The generic `keyof T` branch of TypeResolver masked a *symbol*'s flags with `ts.TypeFlags.TypeParameter`. That only ever matched because TypeFlags and SymbolFlags happened to share the value 262144 for TypeParameter. TypeScript 6.0 renumbers `TypeFlags.TypeParameter` to 524288, so the check never matches and generating metadata for a model containing `keyof T` fails with "Could not determine the keys on T". Mask with `ts.SymbolFlags.TypeParameter`, which is correct on every supported TypeScript version.
When a type name has several declarations, getModelTypeDeclarations drops the ones coming from TypeScript's own lib files so that a user model shadowing a lib name (e.g. 'Account') wins. TypeScript 6.0 ships more lib declarations, so built-in types such as `Error` now have multiple lib declarations and no user declaration. The filter removed all of them and metadata generation failed with "Could not find declarations for type 'Error'" for models like `interface CustomError extends Error`. Only apply the filter when it leaves at least one declaration behind.
MetadataGenerator compiles the user's project with `compilerOptions || {}`.
Up to TypeScript 5.x an empty option bag meant `strictNullChecks: false`, and
the generated specs were built on that assumption.
TypeScript 6.0 enables strict null checking by default. With an empty option
bag optional properties resolve as `T | undefined` and conditional types take
the other branch, so specs silently change shape: `boolean` properties are
emitted as `object`, mapped and conditional aliases resolve differently.
Default explicitly to `{ strictNullChecks: false }` so a project that does not
configure compilerOptions gets the same spec on TypeScript 5 and 6. Projects
that do pass compilerOptions keep full control.
@typescript-eslint 6.16.0 only supports TypeScript <5.4. Under TypeScript 6 its type resolution collapses and it reports roughly 1100 spurious "unsafe any" errors, so the repo cannot be linted on TS 6 at all. v8 supports >=4.8.4 <6.1.0 and covers both TypeScript versions in the CI matrix. The `recommended-requiring-type-checking` preset was renamed to `recommended-type-checked` in v6 and the old name was removed in v7. The v8 presets enable rules the repo did not run before: - source: drop assertions the compiler no longer needs, use a non-null assertion where typeToTypeNode's optional result is narrowed, turn a short-circuit warn into a statement, and annotate the two @hapi/boom calls that are injected into the template service as CallableFunction. - tests: disable the rules that fight chai's expression-style assertions (`expect(x).to.be.ok`) and mocha's promise-returning callbacks.
Widen the typescript dependency to `^5.7.2 || ^6.0.0` in every package so consumers can build tsoa projects with TypeScript 6, and add `^6.0.0` to the CI matrix next to `^5.0.0` so both majors are built, linted and tested. TypeScript 6.0 turns several deprecations into errors, so the repo's own configuration and fixtures needed updating: - drop `downlevelIteration`, a no-op at the es2021 target, and `baseUrl`. The imports that relied on `baseUrl` to resolve (`fixtures/*`, `unit/*`) keep working through `paths`, which no longer requires `baseUrl` and which tests/tsconfig.json already used for `@tsoa/cli/*` and `@tsoa/runtime/*`. - `tests/esm` moves from the deprecated `moduleResolution: "node"` to `"bundler"`, which matches how its mocha loader already resolves specifiers. - set `esModuleInterop` explicitly in tests/tsconfig.json. TS 6 flips its default to true, so the fixtures that imported callable CommonJS modules (express, koa, supertest, ...) as namespaces move to default imports. - `export module Namespace2` becomes `export namespace Namespace2` (TS1540) and a side-effect import of a controller that does not exist is removed (TS2882). TypeScript 7 is out of scope: it is the native compiler and does not expose the JS compiler API that @tsoa/cli relies on.
01a9c8f to
adc305b
Compare
CI note: one pre-existing test failure, not introduced by this PRI ran this branch through CI on my fork (the workflow triggers It is not caused by this PR. I pushed unmodified
So it reproduces on Locally the full suite passes (1946 passing) on both TypeScript 5.9.3 and 6.0.3, which points at the environment rather than the code. My best guess at the mechanism, for whatever it is worth:
I have deliberately not touched that test in this PR, since it is out of scope and fixing it would muddy the TypeScript 6 diff. Happy to open a separate issue for it, or to include a fix here if you would prefer that. Aside from this one pre-existing failure, both matrix legs are green — build, lint and the full suite pass on TypeScript 5.9.3 and 6.0.3. |
|
Correction to my previous comment: I guessed at
So multer moved the cut-off from "reject when the file reaches the limit" to "reject when it exceeds the limit". Anything genuinely over the limit is still rejected in both versions, so the upload cap itself is intact — the only case that flips is a file of exactly the limit, which is precisely what writeFileSync('./moreThan8mb', new Buffer(8 * 1024 * 1024)); // === limits.fileSizeThat makes the test dependent on multer's old off-by-one rather than on tsoa behaviour, and writeFileSync('./moreThan8mb', Buffer.alloc(8 * 1024 * 1024 + 1));( I have left this out of this PR to keep the TypeScript 6 diff focused, and it is unrelated to these changes — it reproduces on unmodified |
closes #1877
All Submissions:
No new test files are added. Please see the test plan below — the three fixes are only observable when the compiler is TypeScript 6, and the existing suite already covers them once the new CI matrix leg runs it there. Happy to add dedicated tests if you would prefer them anyway.
What this does
TypeScript 6.0 is the last release of the JavaScript-based compiler, and npm's
latestis now the native TypeScript 7, which does not expose the JS compiler API@tsoa/clineeds. Users upgrading to TypeScript 6 currently have to drop tsoa. This makes tsoa build and generate correctly on TypeScript 6 while keeping TypeScript 5 fully supported.Five commits, smallest first:
fix(cli): mask symbol flags withSymbolFlagswhen resolvingkeyof— the generickeyof Tbranch masked a symbol's flags withts.TypeFlags.TypeParameter. It only ever matched becauseTypeFlags.TypeParameterandSymbolFlags.TypeParameterwere both262144. TS 6 renumbers the former to524288, so generation fails withCould not determine the keys on T. This is a latent bug that happens to be invisible before TS 6.fix(cli): keep lib declarations for types declared only by TypeScript —getModelTypeDeclarationsdrops declarations undernode_modules/typescriptso a user model shadowing a lib name wins. TS 6 ships more lib declarations, soErrorhas several lib declarations and no user declaration, the filter removes all of them, andinterface CustomError extends Errorfails withCould not find declarations for type 'Error'. Now the filter is only applied when it leaves something behind.fix(cli): keep non-strict compiler defaults when none are configured —MetadataGeneratorbuilds the program withcompilerOptions || {}. Through TS 5 that meantstrictNullChecks: false; TS 6 enables it by default, so optional properties becomeT | undefined, conditional types take the other branch, and specs silently change shape (optionalbooleanemitted as"type": "object"). Defaults explicitly to{ strictNullChecks: false }so projects that configure nothing keep today's output. Projects that passcompilerOptionsare untouched.chore(lint): move to typescript-eslint v8 — 6.16.0 only supports TS<5.4; on TS 6 its type resolution collapses into ~1100 spurious errors, so the repo cannot be linted there at all. v8 supports>=4.8.4 <6.1.0, covering both matrix legs. Includes the preset rename (recommended-requiring-type-checking→recommended-type-checked, removed in v7) and the source/test adjustments its newly-enabled rules require.feat: support TypeScript 6.0 — widenstypescriptto^5.7.2 || ^6.0.0in every package, adds^6.0.0to the CI matrix, and migrates the repo's own tsconfigs and fixtures off options TS 6 turned into errors (downlevelIteration,baseUrl,moduleResolution: "node", theesModuleInteropdefault flip, TS1540export module, and one side-effect import of a controller that does not exist).TypeScript 7 is explicitly out of scope.
Potential Problems With The Approach
strictNullChecksdefault is the main judgement call. I chose to preserve current output for projects that configure nocompilerOptions, rather than letting TS 6's defaults through and updating the expected specs — the latter would make generated output differ between the TS 5 and TS 6 CI legs. The trade-off is that tsoa now states a default instead of inheriting the compiler's. Worth a maintainer opinion.validateCompilerOptionsread the project'stsconfig.json. Once it lands, the CLI path will usually pass real options, so this default mostly applies to programmaticMetadataGeneratorcallers — but a consumer whose tsconfig setsstrict: truewould then getT | undefinedoptional properties on TS 6. Worth deciding together which of the two behaviours you want; happy to rebase on whichever merges first.resolutionsnow forces TypeScript 6 onto lerna, which declarestypescript: ">=3 < 6". Everything builds, lints and tests fine (lerna only orchestrates scripts here), but it is a range violation created by the rootresolutionspin, so flagging it.tests/esmmoves tomoduleResolution: "bundler"rather thannode16/nodenext.bundlermatches how its mocha loader already resolves specifiers (--experimental-specifier-resolution=node, extensionless imports);nodenextwould require adding extensions throughout. Say the word if you would rather have the stricter setting.Test plan
The mechanism is commit 5 adding
^6.0.0to the CItypescript-versionmatrix: it runs the entire existing suite against TypeScript 6, where it did not run before. Each of the three fixes has existing tests that fail without it on that leg:keyoffix — without it,tests/prepare.tsaborts before any test runs:KeysMember<T>intests/fixtures/testModel.ts(keysOfAny,keysOfInterface) throwsCould not determine the keys on T. The whole suite is blocked.tests/prepare.tsaborts the same way onCustomError extends Errorintests/fixtures/controllers/getController.ts:Could not find declarations for type 'Error'. Again the whole suite is blocked.strictNullChecksfix — with route generation unblocked, 24 definition-generation assertions fail without it, acrossdefaults,mappeds,conditionals,advancedTypeAliases,nestedTypes,jsDocTypeNamesandjsdocMapintests/unit/swagger/definitionsGeneration/definitions.spec.ts— e.g.defaults.basic.boolValue3expected"type": "boolean", actual"type": "object".I verified both matrix legs locally, building each package and running lint plus both test suites end to end:
teststests/esmSince the fixes are version-conditional, a dedicated unit test would assert the same thing the existing suite already asserts, and would pass on the TS 5 leg either way. I went with the matrix leg as the regression guard, but I am glad to add explicit tests if you would rather have them pinned down independently.