Repository: vercel/next.js
Pull request: [strict-route-types] Switch to `satisfies` when validating page and route modules

Reverts https://github.com/vercel/next.js/pull/83239 flagged behind `experimental.strictRouteTypes`.

Makes the error messages a bit friendlier since we no longer need these `__*` helper types which made it harder to grok what we were actually doing.

`satisfies` is available since TypeScript 4.9. Our lowest supported version is 5.1. We could land this unflagged but this just makes it clearer that all the type changes are behind a single flag instead of being spread across flagged and unflagged.

Closes https://linear.app/vercel/issue/NXT-128



Changed files:
filename: packages/next/src/server/lib/router-utils/typegen.ts
patch: @@ -688,18 +688,12 @@ export function generateValidatorFileStrict(
             ? `${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')
   )})
-  type __Check = __IsExpected<typeof handler>
-  // @ts-ignore
-  type __Unused = __Check
+  handler satisfies ${typeWithRoute}
 }`
       })
       .join('\n\n')

filename: test/e2e/app-dir/typed-routes-validator/typed-routes-validator.test.ts
patch: @@ -1,6 +1,9 @@
 import { nextTestSetup } from 'e2e-utils'
 import { getDistDir } from 'next-test-utils'
 
+const strictRouteTypes =
+  process.env.__NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES === 'true'
+
 describe('typed-routes-validator', () => {
   const { next, isNextDev, isNextStart, skipped } = nextTestSetup({
     files: __dirname,
@@ -55,9 +58,15 @@ describe('typed-routes-validator', () => {
       await next.deleteFile('app/invalid/page.tsx')
 
       expect(exitCode).toBe(1)
-      expect(cliOutput).toMatch(
-        /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'AppPageConfig</
-      )
+      if (strictRouteTypes) {
+        expect(cliOutput).toMatch(
+          /Type error: Type 'typeof import\(.*' does not satisfy the expected type 'AppPageConfig</
+        )
+      } else {
+        expect(cliOutput).toMatch(
+          /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'AppPageConfig</
+        )
+      }
     })
 
     it('should pass type checking with valid page props', async () => {
@@ -92,9 +101,15 @@ describe('typed-routes-validator', () => {
       await next.deleteFile('app/invalid/page.tsx')
 
       expect(exitCode).toBe(1)
-      expect(cliOutput).toMatch(
-        /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'AppPageConfig</
-      )
+      if (strictRouteTypes) {
+        expect(cliOutput).toMatch(
+          /Type error: Type 'typeof import\(.*' does not satisfy the expected type 'AppPageConfig</
+        )
+      } else {
+        expect(cliOutput).toMatch(
+          /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'AppPageConfig</
+        )
+      }
     })
 
     it('should pass type checking with valid route handler exports', async () => {
@@ -150,9 +165,15 @@ describe('typed-routes-validator', () => {
       await next.deleteFile('app/invalid/route.ts')
 
       expect(exitCode).toBe(1)
-      expect(cliOutput).toMatch(
-        /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'RouteHandlerConfig</
-      )
+      if (strictRouteTypes) {
+        expect(cliOutput).toMatch(
+          /Type error: Type 'typeof import\(.*' does not satisfy the expected type 'RouteHandlerConfig</
+        )
+      } else {
+        expect(cliOutput).toMatch(
+          /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'RouteHandlerConfig</
+        )
+      }
     })
 
     it('should fail type checking with invalid route handler params', async () => {
@@ -171,9 +192,15 @@ describe('typed-routes-validator', () => {
       await next.deleteFile('app/invalid-2/route.ts')
 
       expect(exitCode).toBe(1)
-      expect(cliOutput).toMatch(
-        /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'RouteHandlerConfig</
-      )
+      if (strictRouteTypes) {
+        expect(cliOutput).toMatch(
+          /Type error: Type 'typeof import\(.*' does not satisfy the expected type 'RouteHandlerConfig</
+        )
+      } else {
+        expect(cliOutput).toMatch(
+          /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'RouteHandlerConfig</
+        )
+      }
     })
 
     it('should pass type checking with valid layout exports', async () => {
@@ -212,9 +239,15 @@ describe('typed-routes-validator', () => {
       await next.deleteFile('app/invalid/layout.tsx')
 
       expect(exitCode).toBe(1)
-      expect(cliOutput).toMatch(
-        /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'LayoutConfig</
-      )
+      if (strictRouteTypes) {
+        expect(cliOutput).toMatch(
+          /Type error: Type 'typeof import\(.*' does not satisfy the expected type 'LayoutConfig</
+        )
+      } else {
+        expect(cliOutput).toMatch(
+          /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'LayoutConfig</
+        )
+      }
     })
 
     it('should pass type checking with valid API route exports', async () => {
@@ -256,9 +289,15 @@ describe('typed-routes-validator', () => {
       await next.deleteFile('pages/api/invalid-api.ts')
 
       expect(exitCode).toBe(1)
-      expect(cliOutput).toMatch(
-        /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'ApiRouteConfig'/
-      )
+      if (strictRouteTypes) {
+        expect(cliOutput).toMatch(
+          /Type error: Type 'typeof import\(.*' does not satisfy the expected type 'ApiRouteConfig'/
+        )
+      } else {
+        expect(cliOutput).toMatch(
+          /Type error: Type 'typeof import\(.*' does not satisfy the constraint 'ApiRouteConfig'/
+        )
+      }
     })
   }
 })