Repository: vercel/next.js
Pull request: feat: detect @typescript/native-preview as alternative TypeScript compiler

## What?

This PR adds support for detecting `@typescript/native-preview` as an alternative TypeScript compiler. When installed, Next.js will no longer automatically install the `typescript` package.

## Why?

Fixes #88580

Users who want to use `@typescript/native-preview` (a Go-based native TypeScript compiler) should not be forced to also install the JavaScript-based `typescript` package. This is especially relevant for projects using `next.config.ts` where TypeScript support is provided by Node.js native TS or SWC.

## How?

1. Added `hasNativeTypeScriptPreview()` function to detect if `@typescript/native-preview` is installed
2. Modified the dependency check logic in `verifyTypeScriptSetup()` to skip auto-installing `typescript` when the native preview is present
3. Added an informative log message to notify users that some TypeScript features (like type checking during build) require the standard `typescript` package
4. Added e2e test to verify the behavior

## Test Plan

- Added new e2e test at `test/e2e/app-dir/typescript-native-preview/`
- The test verifies:
  - Detection message is shown
  - `typescript` is not auto-installed
  - The app still works (SWC/Turbopack handles compilation)

Changed files:
filename: packages/next/src/lib/verify-typescript-setup.ts
patch: @@ -2,7 +2,10 @@ import { bold, cyan, red, yellow } from './picocolors'
 import path, { join } from 'path'
 
 import { hasNecessaryDependencies } from './has-necessary-dependencies'
-import type { NecessaryDependencies } from './has-necessary-dependencies'
+import type {
+  MissingDependency,
+  NecessaryDependencies,
+} from './has-necessary-dependencies'
 import semver from 'next/dist/compiled/semver'
 import { CompileError } from './compile-error'
 import * as log from '../build/output/log'
@@ -14,13 +17,16 @@ import { writeConfigurationDefaults } from './typescript/writeConfigurationDefau
 import { installDependencies } from './install-dependencies'
 import { isCI } from '../server/ci-info'
 import { missingDepsError } from './typescript/missingDependencyError'
+import { resolveFrom } from './resolve-from'
 
-const requiredPackages = [
-  {
-    file: 'typescript/lib/typescript.js',
-    pkg: 'typescript',
-    exportsRestrict: true,
-  },
+const typescriptPackage: MissingDependency = {
+  file: 'typescript/lib/typescript.js',
+  pkg: 'typescript',
+  exportsRestrict: true,
+}
+
+const requiredPackages: MissingDependency[] = [
+  typescriptPackage,
   {
     file: '@types/react/index.d.ts',
     pkg: '@types/react',
@@ -33,6 +39,20 @@ const requiredPackages = [
   },
 ]
 
+/**
+ * Check if @typescript/native-preview is installed as an alternative TypeScript compiler.
+ * This is a Go-based native TypeScript compiler that can be used instead of the standard
+ * TypeScript package for faster compilation.
+ */
+function hasNativeTypeScriptPreview(dir: string): boolean {
+  try {
+    resolveFrom(dir, '@typescript/native-preview/package.json')
+    return true
+  } catch {
+    return false
+  }
+}
+
 export async function verifyTypeScriptSetup({
   dir,
   distDir,
@@ -76,12 +96,55 @@ export async function verifyTypeScriptSetup({
       return { version: null }
     }
 
+    // Check if @typescript/native-preview is installed as an alternative
+    const hasNativePreview = hasNativeTypeScriptPreview(dir)
+
     // Ensure TypeScript and necessary `@types/*` are installed:
     let deps: NecessaryDependencies = hasNecessaryDependencies(
       dir,
       requiredPackages
     )
 
+    // If @typescript/native-preview is installed and only the typescript package is missing,
+    // we can skip auto-installing typescript since the native preview provides TS compilation.
+    // However, we still need @types/react and @types/node for type checking.
+    if (hasNativePreview && deps.missing?.length > 0) {
+      const missingWithoutTypescript = deps.missing.filter(
+        (dep) => dep.pkg !== 'typescript'
+      )
+      const onlyTypescriptMissing =
+        deps.missing.length === 1 && deps.missing[0].pkg === 'typescript'
+
+      if (onlyTypescriptMissing) {
+        // @typescript/native-preview is installed and only typescript is missing
+        // Skip installation and return early - the project can use the native preview
+        log.info(
+          `Detected ${bold('@typescript/native-preview')} as TypeScript compiler. ` +
+            `Some Next.js TypeScript features (like type checking during build) require the standard ${bold('typescript')} package.`
+        )
+
+        // Still write type declarations since they don't require the typescript package
+        await writeAppTypeDeclarations({
+          baseDir: dir,
+          distDir,
+          distDirRoot,
+          imageImportsEnabled: !disableStaticImages,
+          hasPagesDir,
+          hasAppDir,
+        })
+
+        return { version: null }
+      }
+
+      // If there are other missing deps besides typescript, only install those
+      if (
+        missingWithoutTypescript.length > 0 &&
+        missingWithoutTypescript.length < deps.missing.length
+      ) {
+        deps.missing = missingWithoutTypescript
+      }
+    }
+
     if (deps.missing?.length > 0) {
       if (isCI) {
         // we don't attempt auto install in CI to avoid side-effects

filename: test/development/typescript-native-preview/index.test.ts
patch: @@ -0,0 +1,71 @@
+import { nextTestSetup } from 'e2e-utils'
+import { retry } from 'next-test-utils'
+
+describe('typescript-native-preview', () => {
+  const { next } = nextTestSetup({
+    files: {
+      'app/layout.tsx': `
+        import { ReactNode } from 'react'
+        export default function Root({ children }: { children: ReactNode }) {
+          return (
+            <html>
+              <body>{children}</body>
+            </html>
+          )
+        }
+      `,
+      'app/page.tsx': `
+        export default function Page() {
+          return <p>hello world</p>
+        }
+      `,
+      'tsconfig.json': JSON.stringify({
+        compilerOptions: {
+          target: 'ES2017',
+          lib: ['dom', 'dom.iterable', 'esnext'],
+          allowJs: true,
+          skipLibCheck: true,
+          strict: true,
+          noEmit: true,
+          esModuleInterop: true,
+          module: 'esnext',
+          moduleResolution: 'bundler',
+          resolveJsonModule: true,
+          isolatedModules: true,
+          jsx: 'preserve',
+          incremental: true,
+          plugins: [{ name: 'next' }],
+        },
+        include: ['next-env.d.ts', '**/*.ts', '**/*.tsx'],
+        exclude: ['node_modules'],
+      }),
+    },
+    dependencies: {
+      // Explicitly exclude typescript from default dependencies
+      // so that we can test the native-preview detection
+      typescript: undefined,
+      // Install @typescript/native-preview instead of typescript
+      '@typescript/native-preview': 'latest',
+      '@types/react': 'latest',
+      '@types/node': 'latest',
+    },
+  })
+
+  it('should detect @typescript/native-preview and not auto-install typescript', async () => {
+    // The app should still work since SWC/Turbopack handles TS compilation
+    const $ = await next.render$('/')
+    expect($('p').text()).toBe('hello world')
+
+    // Wait for the info message about native-preview to be logged
+    // The TypeScript verification happens asynchronously after page render
+    await retry(() => {
+      expect(next.cliOutput).toMatch(
+        /Detected.*@typescript\/native-preview|@typescript\/native-preview.*detected/i
+      )
+    })
+
+    // Should NOT show that typescript is being installed as a dependency
+    // (it should recognize that native-preview is a valid alternative)
+    expect(next.cliOutput).not.toMatch(/Installing.*typescript[^/]/i)
+  })
+})