Skip to content

Upgrade to TypeScript v5.0 #3651

@MajorLift

Description

@MajorLift

Motivation

As part of Shared Libraries Q2 2024 OKRs (O3/KR4), we will be upgrading all core packages to use TypeScript v5.0.

There are some significant blockers associated with this upgrade due to breaking changes in TypeScript's handling of modules. Once unblocked, we will be able to proceed to further version upgrades, gaining access to the latest features and improvements in TypeScript, and reaching parity with the extension.

References

Features

  • const Type Parameters:
    • Causes as const inference by default.
    • 74 as const instances in core.
type HasNames = { names: readonly string[] };
function getNamesExactly<const T extends HasNames>(arg: T): T["names"] {
//                       ^^^^^
    return arg.names;
}
// Inferred type without `const`: string[]
// Inferred type with `const`: readonly ["Alice", "Bob", "Eve"]
// Note: Didn't need to write 'as const' here
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });

Blockers

node16/nodenext

  • TypeScript v4.7 introduced the node16/nodenext options for the --module and --moduleResolution compiler options.

  • We are currently at v4.8 using the node option for moduleResolution, which is renamed to node10 starting from TypeScript v5.0 (the node alias is maintained for compatibility).

    • node10 does not support correct module resolution for ESM packages.
    • node10 does not support the exports field in package.json

bundler

  • v5.0 introduces the bundler option for better compatibility, but this isn't intended for usage with npm libraries that don't use a bundler to build and deploy.

On the other hand, if you’re writing a library that’s meant to be published on npm, using the bundler option can hide compatibility issues that may arise for your users who aren’t using a bundler. So in these cases, using the node16 or nodenext resolution options is likely to be a better path.

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#--moduleresolution-bundler

  • bundler requires --module to be es2020+ and looks for type declarations under dist/esm/.
    src/AccountsController.test.ts:6:28 - error TS7016: Could not find a declaration file for module '@metamask/snaps-utils'. '/home/runner/work/core/core/node_modules/@metamask/snaps-utils/dist/esm/index.js' implicitly has an 'any' type.
      Try `npm i --save-dev @types/metamask__snaps-utils` if it exists or add a new declaration (.d.ts) file containing `declare module '@metamask/snaps-utils';`

    6 import { SnapStatus } from '@metamask/snaps-utils';
                                 ~~~~~~~~~~~~~~~~~~~~~~~
  • TODO: Investigate feasibility of using bundler option with tsup

Explanation

We need to update --module and --moduleResolution to node16 at minimum and preferably nodenext before we upgrade to TypeScript v5.0, but this entails a number of breaking changes:

Add file extensions to relative import

For example, in an ECMAScript module in Node.js, any relative import needs to include a file extension.

// entry.mjs
import * as utils from "./utils";     // ❌ wrong - we need to include the file extension.
import * as utils from "./utils.mjs"; // ✅ works

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#--moduleresolution-bundler

Also investigate allowImportingTsExtensions

Convert to dynamic import

it’s worth noting that the only way to import ESM files from a CJS module is using dynamic import() calls. This can present challenges, but is the behavior in Node.js today.

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html
https://nodejs.org/api/esm.html#esm_interoperability_with_commonjs

Add "type: module" to package.json

That’s why TypeScript 4.7 introduces a new option called moduleDetection. moduleDetection can take on 3 values: "auto" (the default), "legacy" (the same behavior as 4.6 and prior), and "force".

Under the mode "auto", TypeScript will not only look for import and export statements, but it will also check whether

  • the "type" field in package.json is set to "module" when running under --module nodenext/--module node16, and
  • check whether the current file is a JSX file when running under --jsx react-jsx

In cases where you want every file to be treated as a module, the "force" setting ensures that every non-declaration file is treated as a module. This will be true regardless of how module, moduleResolution, and jsx are configured.

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#control-over-module-detection

Move type declarations in dependencies to dist/cjs/

    src/AccountsController.test.ts:6:28 - error TS7016: Could not find a declaration file for module '@metamask/snaps-utils'. '/home/runner/work/core/core/node_modules/@metamask/snaps-utils/dist/cjs/index.js' implicitly has an 'any' type.
      Try `npm i --save-dev @types/metamask__snaps-utils` if it exists or add a new declaration (.d.ts) file containing `declare module '@metamask/snaps-utils';`

    6 import { SnapStatus } from '@metamask/snaps-utils';

Metadata

Metadata

Assignees

Labels

team-wallet-frameworkDeprecated: Please use `team-core-platform` instead.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions