Visitar URL original
feat: basic `typescript` support using Node.js typescript API (only strip types and runtime transormation) by alexander-akait · Pull Request #20964 · webpack/webpack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/experimental-typescript-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"webpack": minor
---

Add experimental TypeScript support via `experiments.typescript: true` (auto-enabled by `experiments.futureDefaults`). Uses Node.js's built-in `module.stripTypeScriptTypes` (Node.js >= 22.6 with the stable `mode: "strip"` API, including Node.js 26) to transform `.ts`, `.cts`, `.mts`, `data:text/typescript`, and `data:application/typescript` modules — no type checking, only erasable TypeScript (types, generics, `import type`, casts). `.tsx`/JSX and non-erasable syntax (`enum`, `namespace`, parameter-property constructors, decorator metadata) are NOT supported; use a TSX-capable loader (e.g. `ts-loader`, `swc-loader`) for those.

Adds matching default rules, extension resolution (`.ts` is resolved before `.js`), `extensionAlias` for `.js`/`.cjs`/`.mjs` to also try `.ts`/`.cts`/`.mts`, `tsconfig` resolution, and the `"typescript"` conditional-exports key so monorepo packages can ship `.ts` sources via `package.json#exports` (matching Node.js's amaro convention).
2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
"RCDATA",
"RAWTEXT",
"noembed",
"AACA",
"absolutify",
"abortable",
"acircular",
"amaro",
"amdmodule",
"analyse",
"analysed",
Expand Down
15 changes: 15 additions & 0 deletions declarations/WebpackOptions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,11 @@ export interface Experiments {
* @experimental
*/
syncWebAssembly?: boolean;
/**
* Enable typescript support.
* @experimental
*/
typescript?: boolean;
[k: string]: any;
}
/**
Expand Down Expand Up @@ -3465,6 +3470,11 @@ export interface ExperimentsNormalized {
* @experimental
*/
syncWebAssembly?: boolean;
/**
* Enable typescript support.
* @experimental
*/
typescript?: boolean;
}
/**
* Parser options for javascript modules.
Expand Down Expand Up @@ -3607,6 +3617,11 @@ export interface JavascriptParserOptions {
* Enable/disable parsing of System.js special syntax like System.import, System.get, System.set and System.register.
*/
system?: boolean;
/**
* Enable typescript support.
* @experimental
*/
typescript?: boolean;
/**
* Enable warnings when using the require function in a not statically analyse-able way.
*/
Expand Down
1 change: 1 addition & 0 deletions examples/typescript-non-erasable/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require("../build-common");
1 change: 1 addition & 0 deletions examples/typescript-non-erasable/example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log(require("./index"));
35 changes: 35 additions & 0 deletions examples/typescript-non-erasable/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// This example uses syntax that Node.js's built-in `stripTypeScriptTypes`
// rejects (enums, parameter-property constructors, namespaces) — i.e. it
// goes beyond the "erasable" subset enforced by tsconfig's
// `erasableSyntaxOnly`. Projects that rely on these features still need a
// real TypeScript transpiler such as `ts-loader` or `swc-loader`.
Comment on lines +1 to +5

enum Role {
Admin = "admin",
Editor = "editor",
Viewer = "viewer"
}

class User {
constructor(
public readonly name: string,
public readonly role: Role
) {}

describe(): string {
return `${this.name} (${this.role})`;
}
}

function asArray<T>(...items: T[]): T[] {
return [...items];
}

const users = asArray(
new User("Alice", Role.Admin),
new User("Bob", Role.Viewer)
);

for (const user of users) {
console.log(user.describe());
}
51 changes: 51 additions & 0 deletions examples/typescript-non-erasable/template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# When to use this example

Webpack 5 includes a built-in TypeScript transform via
`experiments.typescript: true` (see the `examples/typescript` example).
That transform uses Node.js's `module.stripTypeScriptTypes` and therefore
only handles the **erasable** TypeScript subset — types, `import type`,
`as`-casts, generics, etc. It rejects syntax that emits runtime code:
`enum`, `namespace`, parameter-property constructors, `export =`, decorator
metadata, JSX/`.tsx`.

If your project uses any of that non-erasable syntax, keep using a real
Comment on lines +5 to +11
Comment on lines +5 to +11
TypeScript transpiler. This example shows the classic setup with
`ts-loader` plus `fork-ts-checker-webpack-plugin` for type checking.
Comment thread
alexander-akait marked this conversation as resolved.
Resolved

# example.js

```javascript
_{{example.js}}_
```

# index.ts

```typescript
_{{index.ts}}_
```

# webpack.config.js

```javascript
_{{webpack.config.js}}_
```

# dist/output.js

```javascript
_{{dist/output.js}}_
```

# Info

## Unoptimized

```
_{{stdout}}_
```

## Production mode

```
_{{production:stdout}}_
```
5 changes: 5 additions & 0 deletions examples/typescript-non-erasable/test.filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"use strict";

const supportsOptionalChaining = require("../../test/helpers/supportsOptionalChaining");

module.exports = () => supportsOptionalChaining();
5 changes: 5 additions & 0 deletions examples/typescript-non-erasable/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"rootDir": "./"
}
}
25 changes: 25 additions & 0 deletions examples/typescript-non-erasable/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use strict";

const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");

/** @type {(env: "development" | "production") => import("webpack").Configuration} */
const config = (env = "development") => ({
mode: env,
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
options: {
transpileOnly: true
}
}
]
},
resolve: {
extensions: [".ts", ".js", ".json"]
},
plugins: [new ForkTsCheckerWebpackPlugin({ async: env === "production" })]
});

module.exports = config;
8 changes: 8 additions & 0 deletions examples/typescript/greeter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface User {
name: string;
age: number;
}

export function greet(user: User): string {
return `Hello, ${user.name} (${user.age})`;
}
12 changes: 5 additions & 7 deletions examples/typescript/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
const myName: string = "Junya";
const age: number = 22;
import { greet, type User } from "./greeter.ts";

function getArray<T>(...args: T[]): T[] {
return [...args];
}
const alice: User = { name: "Alice", age: 31 };
const bob: User = { name: "Bob", age: 27 };

console.log(getArray("foo", "bar"));
console.log(getArray(1, 2, 3));
console.log(greet(alice));
console.log(greet(bob));
21 changes: 21 additions & 0 deletions examples/typescript/template.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
# Built-in TypeScript support

Webpack 5 ships an experimental TypeScript transform behind
`experiments.typescript: true`. It wires up Node.js's
`module.stripTypeScriptTypes` (Node.js >= 22.7) to strip type annotations
from `.ts`, `.cts`, `.mts` files at build time, registers the matching
module rules, adds `.ts` to `resolve.extensions`, sets `extensionAlias`
so `.js`/`.cjs`/`.mjs` imports also resolve `.ts`/`.cts`/`.mts` siblings,
and turns on tsconfig.json resolution.

Only the **erasable** TypeScript subset is supported here. For
non-erasable syntax (enums, namespaces, parameter-property constructors,
JSX/`.tsx`), use `ts-loader` or `swc-loader` — see the
Comment thread
alexander-akait marked this conversation as resolved.
Resolved
`typescript-non-erasable` example.
Comment on lines +11 to +14
Comment on lines +11 to +14

# example.js

```javascript
Expand All @@ -10,6 +25,12 @@ _{{example.js}}_
_{{index.ts}}_
```

# greeter.ts

```typescript
_{{greeter.ts}}_
```

# webpack.config.js

```javascript
Expand Down
4 changes: 1 addition & 3 deletions examples/typescript/test.filter.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use strict";

const supportsOptionalChaining = require("../../test/helpers/supportsOptionalChaining");

module.exports = () => supportsOptionalChaining();
module.exports = () => "stripTypeScriptTypes" in require("module");
Comment thread
alexander-akait marked this conversation as resolved.
Resolved
14 changes: 11 additions & 3 deletions examples/typescript/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
{
"compilerOptions": {
"rootDir": "./"
}
"compilerOptions": {
"noEmit": true,
"target": "esnext",
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"allowImportingTsExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true
},
"include": ["./**/*.ts"]
}
35 changes: 22 additions & 13 deletions examples/typescript/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,31 @@

const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");

// `experiments.typescript: true` enables webpack's built-in TypeScript
// support. Internally it wires up `module.stripTypeScriptTypes` (Node.js
// >= 22.7) to strip type annotations from `.ts`, `.cts`, `.mts` files at
// build time. It also registers the matching module rules, adds `.ts` to
// `resolve.extensions`, sets up `extensionAlias` so `.js`/`.cjs`/`.mjs`
// imports also try their `.ts`/`.cts`/`.mts` siblings, and enables
// tsconfig.json resolution.
//
// `stripTypeScriptTypes` is purely a transpile step — it strips type
// annotations and does NOT type-check. We pair it with
// `fork-ts-checker-webpack-plugin`, which runs `tsc --noEmit` in a worker
// so build errors and type errors both surface at compile time, the same
// trade-off as `ts-loader { transpileOnly: true }` + `ForkTsChecker`.
//
// Limitations: only the **erasable** TypeScript subset is supported —
// `enum`, `namespace`, parameter-property constructors, decorator
// metadata, and JSX/`.tsx` are NOT handled here. For those, use
Comment on lines +7 to +21
// `ts-loader` or `swc-loader` (see the `typescript-non-erasable`
// example).
Comment thread
alexander-akait marked this conversation as resolved.
Resolved
Comment on lines +19 to +23

/** @type {(env: "development" | "production") => import("webpack").Configuration} */
const config = (env = "development") => ({
mode: env,
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
options: {
transpileOnly: true
}
}
]
},
resolve: {
extensions: [".ts", ".js", ".json"]
experiments: {
typescript: true
},
plugins: [new ForkTsCheckerWebpackPlugin({ async: env === "production" })]
});
Expand Down
2 changes: 1 addition & 1 deletion lib/Module.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ const makeSerializable = require("./util/makeSerializable");
/**
* Defines the code gen value type used by this module.
* @template {string} K
* @typedef {K extends keyof AllCodeGenerationSchemas ? AllCodeGenerationSchemas[K] : EXPECTED_ANY} CodeGenValue
* @typedef {K extends (keyof AllCodeGenerationSchemas) ? AllCodeGenerationSchemas[K] : EXPECTED_ANY} CodeGenValue
*/

/**
Expand Down
6 changes: 6 additions & 0 deletions lib/WebpackOptionsApply.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,12 @@ class WebpackOptionsApply extends OptionsApply {
new HtmlModulesPlugin().apply(compiler);
}

if (options.experiments.typescript) {
const TypeScriptPlugin = require("./typescript/TypeScriptPlugin");

new TypeScriptPlugin().apply(compiler);
}

if (options.experiments.lazyCompilation) {
const LazyCompilationPlugin = require("./hmr/LazyCompilationPlugin");

Expand Down
Loading
Loading