Skip to content

chore(deps): TypeScript major version migration (pathfinder) - #1037

Closed
ByronDWall wants to merge 2 commits into
mainfrom
chore/deps-typescript-major-migration
Closed

chore(deps): TypeScript major version migration (pathfinder)#1037
ByronDWall wants to merge 2 commits into
mainfrom
chore/deps-typescript-major-migration

Conversation

@ByronDWall

Copy link
Copy Markdown
Contributor

MIGRATION RECIPE (for fan-out to other repos)

This section is written to be copied verbatim into the other 8 repos
(audit-log, identity, merchant-center-application-kit,
merchant-center-frontend, merchant-center-prices,
merchant-center-services, nimbus, ui-kit).

What changed

Before After
typescript 5.9.3 7.0.2

TypeScript's real current latest major on npm is 7 (7.0.2, released
2026-07-08 — "Project Corsa", a full port of the compiler/language service to
Go). TypeScript 6 (6.0.3) existed as an intermediate major and was the last
release built on the classic JS compiler; read its release notes too, because
every 6.0 breaking change also applies to 7.0 (7.0 is a superset).

References read:

tsconfig.json changes

Added one compiler option, no removals were needed for this repo's existing
config (it already used moduleResolution: "Bundler", module: "ESNext",
target: "ESNext", so none of the hard-removed options — target: es5,
moduleResolution: classic, module: amd/umd/systemjs/none, baseUrl,
downlevelIteration — were in play):

{
  "compilerOptions": {
    // TypeScript 6.0+ defaults `types` to `[]` instead of auto-discovering
    // every package under node_modules/@types. Spec files that use Jest's
    // ambient globals (describe/it/expect) without importing them from
    // '@jest/globals' will fail to compile unless you list them explicitly.
    "types": ["jest", "node"]
  }
}

Watch for in other repos: any repo relying on ambient @types/* global
augmentation (jest, mocha, node, cypress, testing-library matchers, etc.)
without an explicit types array needs this same fix. Grep for
describe(/it(/expect( used without a corresponding import, and for any
other ambient global usage (e.g. process, Buffer) that depended on
@types/node being auto-included.

If your repo does use any of the hard-removed/changed options, here's the
mapping (from the TS 6.0/7.0 release notes):

  • target: "es5" → bump to es2015+ (es5 is a hard error now)
  • downlevelIteration → remove (no longer has any effect below es5, which no
    longer exists)
  • moduleResolution: "node" / "node10" → migrate to "nodenext" or
    "bundler"
  • moduleResolution: "classic" → hard error, must move to "bundler" or
    "node16"/"nodenext"
  • module: "amd" | "umd" | "systemjs" | "none" → hard error, migrate to
    "esnext"/"commonjs"/bundler-appropriate value
  • baseUrl (used as a module-resolution root) → hard error; switch to
    explicit paths with relative globs, or a bundler/tsconfig-paths-style
    resolution
  • esModuleInterop: false / allowSyntheticDefaultImports: false /
    alwaysStrict: false → can no longer be set to false; delete the line
    (safer interop / strict mode is now unconditional)
  • outFile → removed entirely
  • Legacy module Foo { ... } namespace syntax → error; rename to
    namespace Foo { ... }
  • rootDir now defaults to . (the tsconfig's directory) instead of being
    inferred from your source layout — if your repo's tsconfig.json lives
    outside src/, add an explicit "rootDir": "./src" (or equivalent) to
    avoid unexpected emit-path changes (this only matters if you emit; a
    noEmit/--noEmit typecheck-only repo like this one is unaffected)

Error categories found and fix patterns

Running tsc --noEmit right after the bump (before any tsconfig changes)
produced ~8,442 TS2304/2593 errors + 4 TS2694 + 1 TS2345 across the
repo. All but one category collapsed to a single tsconfig fix; only one file
needed an actual source-code change.

1. TS2304/TS2593 "Cannot find name 'describe'/'it'/'expect'" (~8,442
occurrences, ~530 files)

Cause: types defaults to [] in TS 6.0+ (see tsconfig section above).

Fix: add "types": ["jest", "node"] to tsconfig.json. One-line fix,
resolved the entire category at once — no per-file changes needed.

2. TS2345 type mismatch on a compatibility-builder call (1 occurrence)

// before — TS 7 infers the untyped generic default (a union of the REST
// and GraphQL variant types) instead of narrowing, so the argument no
// longer matches the GraphQL-only validator:
const customLineItemDraftGraphql = CustomLineItemDraft.random()
  .custom(CustomFieldBooleanType.random())
  .buildGraphql();
validateGraphqlFields(customLineItemDraftGraphql); // TS2345

// after — supply the same explicit generic the rest of the codebase
// already uses for this pattern:
const customLineItemDraftGraphql = CustomLineItemDraft.random()
  .custom(CustomFieldBooleanType.random())
  .buildGraphql<TCustomLineItemDraftGraphql>();
validateGraphqlFields(customLineItemDraftGraphql); // OK

This is a genuine generic-default inference difference between TS 5.9 and
TS 7, not a TS 6/7-documented breaking change — it slipped through because
one spec file omitted the explicit type argument that ~40 other, nearly
identical spec files in this repo already pass. Watch for this pattern in
other repos
: any call site that relies on an unspecified generic type
argument defaulting through two levels of generics (an untyped factory call
feeding an untyped build*() call) is worth grepping for and double-checking
under TS 7, even if tsc doesn't flag it — in our case it did, but the
failure mode if it silently type-checked wrong would be a false green.

3. Runtime tool breakage: @preconstruct/cli's declaration bundler
(build-time, not a tsc error)

pnpm build failed with:

TypeError: Cannot read properties of undefined (reading 'fileExists')
    at retrieveConfigFilenameOrThrow (.../@preconstruct/cli/.../preconstruct-cli-cli.cjs.js)
    ...
  plugin: 'typescript-declarations'

Root cause (the single most important gotcha for fan-out repos):
TypeScript 7.0 is a full Go port ("Project Corsa"). The typescript npm
package's public API surface is no longer the classic JS "Strada"
Program/LanguageService API — require('typescript') on 7.0.2 resolves to
lib/version.cjs, which exports only version metadata. There is no
ts.sys, ts.createProgram, ts.findConfigFile, ts.TypeFlags, etc. Any
tool that calls into that classic API directly (declaration bundlers, custom
build scripts, ts-jest, rollup-plugin-typescript2-style plugins,
typescript-eslint for type-aware rules, ts-morph, etc.) will crash or
silently misbehave the moment typescript resolves to 7.x for that tool,
even though tsc itself works fine.

Fix pattern — pin a classic-API-compatible TypeScript (6.0.3, the last
JS-based release) as an explicit dependency scoped to just the
package/tool that needs it, while keeping 7.0.2 as the repo's primary
devDependency for tsc/editors/CI typecheck:

  • For a workspace package whose own bundler resolves typescript from
    that package's own directory (verify via
    require.resolve('typescript', { paths: [packageDir] }) — preconstruct
    does this via resolveFrom(packageDir, 'typescript')), just add:

    // <package>/package.json
    "devDependencies": {
      "typescript": "6.0.3"
    }

    pnpm's per-package node_modules isolation means that package now resolves
    its own 6.0.3 instead of falling back up to the root's 7.0.2.

  • For a transitive tool with an optional peer dependency on typescript
    (e.g. typescript-eslint v5, which declares typescript: '*' as an
    optional peer) — a plain pnpm.overrides/pnpm-workspace.yaml overrides
    entry with 'pkg>typescript': '6.0.3' syntax does not work, because
    optional peers are resolved from whatever instance is nearest in the graph,
    not from overrides. Use packageExtensions instead, which rewrites the
    package's own manifest to declare it as a hard dependency:

    # pnpm-workspace.yaml
    packageExtensions:
      '@typescript-eslint/eslint-plugin@5.62.0':
        dependencies:
          typescript: '6.0.3'
      '@typescript-eslint/parser@5.62.0':
        dependencies:
          typescript: '6.0.3'
      '@typescript-eslint/type-utils@5.62.0':
        dependencies:
          typescript: '6.0.3'
      '@typescript-eslint/typescript-estree@5.62.0':
        dependencies:
          typescript: '6.0.3'
      '@typescript-eslint/utils@5.62.0':
        dependencies:
          typescript: '6.0.3'

    After this, require.resolve('typescript', { paths: [thatPackageDir] })
    returns 6.0.3's lib/typescript.js (the classic entrypoint) instead of
    7.0.2's lib/version.cjs. Confirm with a quick node -e resolve check
    before assuming the fix landed — pnpm silently keeps the old lockfile
    resolution ("Already up to date") until you delete node_modules and
    reinstall, or otherwise force a re-resolution.

    This exact symptom hit ESLint type-aware rules here:
    TypeError: Cannot read properties of undefined (reading 'Any') from
    @typescript-eslint/type-utils reading ts.TypeFlags.Any — same root
    cause, same fix.

If you're on a newer typescript-eslint (v8/v9+) in your repo, check
first whether it has already added TS 7 support before reaching for this
workaround — v5 (used here, via
@commercetools-frontend/eslint-config-mc-app@24.12.0) predates TS 7 by a
wide margin and has no chance of supporting it natively.

Time taken

Roughly 1.5-2 hours of agent wall-clock time end-to-end (version research,
bump, iterative fixing, three tool-compatibility investigations, and
verification runs), most of it in the @preconstruct/cli/typescript-eslint
Strada-API investigation rather than actual tsc type errors.

Gotchas for fan-out agents

  1. Verify the real latest major yourself — don't assume "current major +
    1". npm view typescript versions --json at the time of this migration
    showed 7.0.2 as latest, with 6.0.3 as a real, separate intermediate
    major worth reading release notes for even though we bumped straight to 7.
  2. tsc --noEmit passing clean is not sufficient to call the migration
    done.
    Build tooling, ESLint, and any script that does
    require('typescript') directly can break even when the compiler itself
    is happy, because TS 7 removed the classic JS API. Always run the actual
    build and the actual CI lint/test command (not just pnpm test) before
    declaring success — this repo's pnpm build and pnpm lint both failed
    silently-differently from tsc and needed separate fixes.
  3. Check what moduleResolution/module/target this repo already
    uses before assuming you'll hit the hard-removed options.
    This repo was
    already on Bundler/ESNext/ESNext, so none of the hard breaks applied
    here — other repos on older configs (node, commonjs, es2020, etc.)
    will likely hit several of the removed-option errors listed above.
  4. ignoreDeprecations won't save you for the hard-removed options — it
    only covers the deprecated-but-still-working set (moduleResolution: node, esModuleInterop: false, etc.), not the ones that are now hard
    compiler errors (target: es5, moduleResolution: classic, module: amd/umd/systemjs/none, baseUrl as a resolution root, outFile).
  5. Any repo using ts-jest, ts-node, ts-morph, or a webpack/rollup/vite
    TypeScript plugin should expect the Strada-API-removal issue
    and budget
    time for it — it was the majority of the effort here despite affecting
    only tooling, not source code.

Summary of changes

Pathfinder migration of this repo's TypeScript major version, ahead of
fanning the same recipe out to 8 other commercetools frontend repos.

  • typescript: 5.9.37.0.2 (root package.json)
  • tsconfig.json: added "types": ["jest", "node"] (TS 6.0+ default for
    types changed from auto-discovery to [])
  • generators/package.json, standalone/package.json: added a scoped
    typescript@6.0.3 devDependency so @preconstruct/cli's declaration
    bundler (which needs the classic TS Program API, removed from TS 7's
    public npm package surface) keeps working
  • pnpm-workspace.yaml: added packageExtensions pinning
    typescript-eslint v5's optional typescript peer to 6.0.3 for the same
    Strada-API reason (type-aware lint rules read ts.TypeFlags)
  • One source fix: added an explicit generic type argument to a single
    buildGraphql() call in
    standalone/src/models/cart/cart/custom-line-item/custom-line-item-draft/builders.spec.ts
    where TS 7's generic-default inference surfaced a real (if narrow) type
    mismatch that TS 5.9 didn't catch

Verification

  • pnpm install — clean
  • pnpm typecheck (tsc --noEmit) — clean, 0 errors
  • pnpm build (preconstruct build) — clean
  • pnpm jest --projects jest.{eslint,test}.config.js (this repo's actual CI
    gate, combining lint + tests) — 2820/2820 lint suites + 535/535 test
    suites (1691/1691 tests) passing

All four checks were also run from a fully clean node_modules (not just
incrementally) to rule out stale-cache false positives.

Not merging — opening for review per the pathfinder migration process.

- Bump typescript 5.9.3 -> 7.0.2 (latest major, Project Corsa native compiler)
- tsconfig.json: add explicit `types: ["jest", "node"]` since TS 6.0+
  defaults `types` to [] instead of auto-discovering @types/* packages;
  our .spec.ts files rely on Jest's ambient globals.
- generators/package.json, standalone/package.json: pin a direct
  typescript@6.0.3 devDependency so @preconstruct/cli's declaration-bundling
  step (which calls the classic ts.sys/ts.createProgram Program API) keeps
  working. TS 7's npm package only exports version metadata now (the classic
  JS "Strada" API was removed as part of the Go-native rewrite).
- pnpm-workspace.yaml: add packageExtensions forcing typescript-eslint v5's
  optional typescript peer to resolve 6.0.3 for the same Strada-API reason
  (its type-aware lint rules read ts.TypeFlags, which no longer exists on
  TS 7's public surface).
Under TypeScript 7, calling buildGraphql() with no type argument on a
value returned by the untyped-generic compatibility builder
(CustomLineItemDraft.random(), whose type parameter defaults to
TCustomLineItemDraftGraphql | TCustomLineItemDraftRest) now resolves the
method's own default type parameter to that same union instead of
narrowing it, so the Rest variant's object shape leaked into a context
that expects the GraphQL variant only. TS 5.9 inferred/defaulted this
without complaint; TS 7 does not. Fixed by supplying the same explicit
generic argument other spec files in this repo already use for this
compatibility-builder pattern.
@ByronDWall
ByronDWall requested a review from a team as a code owner August 10, 2026 18:54
@changeset-bot

changeset-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: b433c2a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@ByronDWall

Copy link
Copy Markdown
Contributor Author

Closing — decided not to pursue the TypeScript v7 migration right now given the tooling-compat workaround required (preconstruct/typescript-eslint still depend on the removed Strada API). Recipe stays here for reference if we revisit later.

@ByronDWall ByronDWall closed this Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant