Repository: vercel/next.js
Pull request: Remove `satisfies` keyword from type validation to preserve old TS compatibility

We already merged this to be released as a patch for `next-15-5` (#83071), but @eps1lon suggested moving this into the main branch as well so we don't block users from upgrading.

**Problem:**
The route type validation was using the `satisfies` keyword, which was introduced in TypeScript 4.9. This caused compatibility issues for projects using older TypeScript versions.

**Solution:**
Replaced the `satisfies` approach with a constraint-based type validation pattern:
- Uses a generic constraint helper type `__IsExpected<Specific extends ExpectedType>`  
- Validates types through constraint satisfaction instead of the `satisfies` keyword
- Maintains identical type checking behavior while supporting older TS versions

**Changes:**
- Updated type validation generation in `typegen.ts` to use constraint-based pattern
- Updated test expectations to match new error message format ("does not satisfy the constraint" vs "does not satisfy the expected type")

This ensures Next.js type validation works across a broader range of TypeScript versions without losing type safety.

Changed files:
filename: packages/next/src/server/lib/router-utils/typegen.ts
patch: @@ -446,12 +446,19 @@ export function generateValidatorFile(
             type === 'RouteHandlerConfig')
             ? `${type}<${JSON.stringify(route)}>`
             : type
+
+        // NOTE: we previously used `satisfies` here, but it's not supported by TypeScript 4.8 and below.
+        // If we ever raise the TS minimum version, we can switch back.
+
         return `// Validate ${filePath}
 {
+  type __IsExpected<Specific extends ${typeWithRoute}> = Specific
   const handler = {} as typeof import(${JSON.stringify(
     importPath.replace(/\.tsx?$/, '.js')
   )})
-  handler satisfies ${typeWithRoute}
+  type __Check = __IsExpected<typeof handler>
+  // @ts-ignore
+  type __Unused = __Check
 }`
       })
       .join('\n\n')

filename: test/e2e/app-dir/typed-routes-validator/typed-routes-validator.test.ts
patch: @@ -13,7 +13,7 @@ describe('typed-routes-validator', () => {
   it('should generate route validation correctly', async () => {
     const dts = await next.readFile('.next/types/validator.ts')
     // sanity check that dev generation is working
-    expect(dts).toContain('handler satisfies AppPageConfig')
+    expect(dts).toContain('const handler = {} as typeof import(')
   })
 
   if (isNextStart) {
@@ -51,7 +51,7 @@ describe('typed-routes-validator', () => {
 
       expect(exitCode).toBe(1)
       expect(cliOutput).toMatch(
-        /Type error: Type 'typeof import\(.*\)' does not satisfy the expected type 'AppPageConfig<"\/invalid">'/
+        /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'AppPageConfig</
       )
     })
 
@@ -111,7 +111,7 @@ describe('typed-routes-validator', () => {
 
       expect(exitCode).toBe(1)
       expect(cliOutput).toMatch(
-        /Type error: Type 'typeof import.*does not satisfy the expected type 'RouteHandlerConfig<"\/invalid">'/
+        /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'RouteHandlerConfig</
       )
     })
 
@@ -132,7 +132,9 @@ describe('typed-routes-validator', () => {
       await next.deleteFile('app/invalid-2/route.ts')
 
       expect(exitCode).toBe(1)
-      expect(cliOutput).toContain(`Types of property 'POST' are incompatible.`)
+      expect(cliOutput).toMatch(
+        /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'RouteHandlerConfig</
+      )
     })
 
     it('should pass type checking with valid layout exports', async () => {
@@ -174,7 +176,7 @@ describe('typed-routes-validator', () => {
 
       expect(exitCode).toBe(1)
       expect(cliOutput).toMatch(
-        /Type error: Type 'typeof import\(.*does not satisfy the expected type 'LayoutConfig<"\/invalid">'/
+        /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'LayoutConfig</
       )
     })
 
@@ -220,7 +222,7 @@ describe('typed-routes-validator', () => {
 
       expect(exitCode).toBe(1)
       expect(cliOutput).toMatch(
-        /Type error: Type 'typeof import\(.*does not satisfy the expected type 'ApiRouteConfig'/
+        /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'ApiRouteConfig'/
       )
     })
   }