Skip to content

Commit 1f848bb

Browse files
benjamin.ecksteinduncanbeevers
authored andcommitted
feat(openapi-typescript): add TypeScript 6 support
- Widen peer dep: `^5.x` → `^5.x || ^6.x` - Upgrade devDependency in openapi-typescript-helpers: 5.9.3 → 6.0.2 - Build, tests, and lint all pass with TS6 - No code changes needed Closes #2723
1 parent 0cc7ee7 commit 1f848bb

13 files changed

Lines changed: 241 additions & 137 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"openapi-typescript": patch
3+
"openapi-typescript-helpers": patch
4+
---
5+
6+
Add TypeScript 6 support.
7+
8+
- `openapi-typescript`: widen the `typescript` peer dependency to `^5.x || ^6.x`.
9+
- `openapi-typescript-helpers`: fix `Readable<T>` and `Writable<T>` so callable types (`Date`, `RegExp`, functions, and class instance methods) are preserved through the recursive mapped type. Without this, the mapped type recursed into method signatures and collapsed them to `{}` under `--strict`, breaking patterns like `Readable<{ createdAt: Date }>.createdAt.toISOString()`. Reproduces on both TS 5 and TS 6.

.github/workflows/ci.yml

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,27 +28,46 @@ jobs:
2828
strategy:
2929
matrix:
3030
node-version: [22, 24]
31+
typescript-version: ["5", "6"]
3132
steps:
3233
- uses: actions/checkout@v6
3334
- uses: actions/setup-node@v6
3435
with:
3536
node-version: ${{ matrix.node-version }}
3637
- uses: pnpm/action-setup@v5
37-
with:
38-
run_install: true
38+
- name: Override catalog to TypeScript 5 (backward-compat check)
39+
if: matrix.typescript-version == '5'
40+
run: |
41+
sed -i 's/^ typescript: \^6\..*/ typescript: ^5.9.3/' pnpm-workspace.yaml
42+
grep '^ typescript:' pnpm-workspace.yaml
43+
- run: pnpm install --no-frozen-lockfile
3944
- run: pnpm test
4045
test-e2e:
4146
runs-on: ubuntu-latest
4247
steps:
4348
- uses: actions/checkout@v6
4449
- uses: actions/setup-node@v6
4550
with:
46-
node-version: latest
51+
node-version: 22 # Lock to the current stable LTS to fix newer Node stream leaks
4752
- uses: pnpm/action-setup@v5
4853
with:
4954
run_install: true
5055
- run: pnpm exec playwright install --with-deps
51-
- run: pnpm run test-e2e
56+
57+
- name: Run E2E Tests
58+
run: pnpm run test-e2e
59+
timeout-minutes: 10 # Gracefully drops the job if it manages to hang, saving runner limits
60+
env:
61+
DEBUG: pw:webserver,pw:browser # Dumps real-time initialization data straight to stdout
62+
NODE_ENV: test
63+
64+
- name: Upload Playwright Trace Report
65+
if: always()
66+
uses: actions/upload-artifact@v4
67+
with:
68+
name: playwright-report
69+
path: packages/openapi-fetch/playwright-report/
70+
retention-days: 1
5271
test-macos:
5372
runs-on: macos-latest
5473
steps:

packages/openapi-fetch/playwright.config.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
import { defineConfig, devices } from "@playwright/test";
22

3-
const PORT = Number.parseInt(process.env.PORT || 4173 || "", 10);
3+
const HOST = "127.0.0.1";
4+
const PORT = Number.parseInt(process.env.PORT || "4173", 10);
45

56
export default defineConfig({
67
testMatch: "test/**/*.e2e.ts",
78
webServer: {
8-
command: "pnpm run e2e-vite-build && pnpm run e2e-vite-start",
9+
// We use the root-safe package workspace filter to accurately point to the exact local commands
10+
command: `pnpm --filter openapi-fetch run e2e-vite-build && pnpm --filter openapi-fetch run e2e-vite-start --host ${HOST} --port ${PORT}`,
911
port: PORT,
12+
reuseExistingServer: !process.env.CI,
13+
timeout: 120_000, // Provides plenty of overhead room for TS6 type compilation
1014
},
1115
use: {
12-
baseURL: `http://localhost:${PORT}`,
16+
baseURL: `http://${HOST}:${PORT}`,
17+
trace: "retain-on-failure",
1318
},
1419
projects: [
1520
{
1621
name: "chrome",
17-
use: { ...devices["Desktop Chrome"] },
22+
use: {
23+
...devices["Desktop Chrome"],
24+
launchOptions: {
25+
args: ["--no-sandbox", "--disable-setuid-sandbox"],
26+
},
27+
},
1828
},
1929
{
2030
name: "firefox",

packages/openapi-fetch/test/helpers.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@ export function createObservedClient<T extends {}, M extends MediaType = MediaTy
2727
* Convert a Headers object to a plain object for easier comparison
2828
*/
2929
export function headersToObj(headers: Headers | Record<string, string>): Record<string, string> {
30-
const iter =
31-
headers instanceof Headers
32-
? headers
33-
// @ts-expect-error FIXME: this is a missing "lib" in tsconfig.json but dunno what
34-
.entries()
35-
: Object.entries(headers);
3630
const result: Record<string, string> = {};
37-
for (const [k, v] of iter) {
38-
result[k] = v;
31+
if (headers instanceof Headers) {
32+
headers.forEach((value, key) => {
33+
result[key] = value;
34+
});
35+
} else {
36+
for (const [key, value] of Object.entries(headers)) {
37+
result[key] = value;
38+
}
3939
}
4040
return result;
4141
}

packages/openapi-fetch/test/no-strict-null-checks/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"extends": "../../tsconfig.json",
33
"compilerOptions": {
4+
"rootDir": "../..",
45
"strictNullChecks": false
56
},
67
"include": ["."],

packages/openapi-fetch/tsconfig.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
"compilerOptions": {
33
"allowSyntheticDefaultImports": true,
44
"declaration": true,
5-
"downlevelIteration": false,
65
"esModuleInterop": true,
76
"lib": ["ESNext", "DOM"],
87
"module": "NodeNext",

packages/openapi-typescript-helpers/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@
3434
"lint:ts": "tsc --noEmit"
3535
},
3636
"devDependencies": {
37-
"typescript": "5.9.3"
37+
"typescript": "catalog:"
3838
}
3939
}

packages/openapi-typescript-helpers/src/index.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,11 @@ export type Readable<T> =
223223
? Readable<U>
224224
: T extends (infer E)[]
225225
? Readable<E>[]
226-
: T extends object
227-
? { [K in keyof T as NonNullable<T[K]> extends $Write<any> ? never : K]: Readable<T[K]> }
228-
: T;
226+
: T extends (...args: never[]) => unknown
227+
? T
228+
: T extends object
229+
? { [K in keyof T as NonNullable<T[K]> extends $Write<any> ? never : K]: Readable<T[K]> }
230+
: T;
229231

230232
/**
231233
* Resolve type for writing (requests): strips $Read properties, unwraps $Write
@@ -240,8 +242,10 @@ export type Writable<T> =
240242
? Writable<U>
241243
: T extends (infer E)[]
242244
? Writable<E>[]
243-
: T extends object
244-
? { [K in keyof T as NonNullable<T[K]> extends $Read<any> ? never : K]: Writable<T[K]> } & {
245-
[K in keyof T as NonNullable<T[K]> extends $Read<any> ? K : never]?: never;
246-
}
247-
: T;
245+
: T extends (...args: never[]) => unknown
246+
? T
247+
: T extends object
248+
? { [K in keyof T as NonNullable<T[K]> extends $Read<any> ? never : K]: Writable<T[K]> } & {
249+
[K in keyof T as NonNullable<T[K]> extends $Read<any> ? K : never]?: never;
250+
}
251+
: T;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Type-level tests for Readable<T> / Writable<T> built-in object passthrough.
3+
*
4+
* These are pure compile-time assertions checked by `tsc --noEmit`. If any
5+
* assertion is wrong, the file fails to type-check.
6+
*/
7+
8+
import type { $Read, $Write, Readable, Writable } from "../src/index.js";
9+
10+
// Bidirectional structural assignability. Looser than the parametric `(<T>() => …)`
11+
// trick but enough for "the resulting shape preserves X" — mapped-type-produced
12+
// objects are structurally identical to their literal twins but fail strict
13+
// parametric equality.
14+
type Equals<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
15+
type Expect<T extends true> = T;
16+
17+
// --- Date passthrough ---
18+
19+
type _ReadableDate = Expect<Equals<Readable<Date>, Date>>;
20+
type _WritableDate = Expect<Equals<Writable<Date>, Date>>;
21+
22+
// $Read<Date> unwraps to Date, not to a structurally-mapped Date prototype
23+
type _ReadableReadDate = Expect<Equals<Readable<$Read<Date>>, Date>>;
24+
type _WritableWriteDate = Expect<Equals<Writable<$Write<Date>>, Date>>;
25+
26+
// Date inside an object field stays Date
27+
type _ReadableObjectWithDate = Expect<Equals<Readable<{ created: Date }>, { created: Date }>>;
28+
type _WritableObjectWithDate = Expect<Equals<Writable<{ created: Date }>, { created: Date }>>;
29+
30+
// --- RegExp passthrough ---
31+
32+
type _ReadableRegExp = Expect<Equals<Readable<RegExp>, RegExp>>;
33+
type _WritableRegExp = Expect<Equals<Writable<RegExp>, RegExp>>;
34+
35+
type _ReadableObjectWithRegExp = Expect<Equals<Readable<{ pattern: RegExp }>, { pattern: RegExp }>>;
36+
37+
// --- Function passthrough ---
38+
39+
type Fn = (x: number) => string;
40+
41+
type _ReadableFn = Expect<Equals<Readable<Fn>, Fn>>;
42+
type _WritableFn = Expect<Equals<Writable<Fn>, Fn>>;
43+
44+
type _ReadableObjectWithFn = Expect<Equals<Readable<{ handler: Fn }>, { handler: Fn }>>;
45+
46+
// --- Negative control: plain object still gets recursive treatment ---
47+
48+
// Plain nested object's $Write marker is still stripped from Readable
49+
type _ReadableStripsWrite = Expect<
50+
Equals<
51+
Readable<{ id: number; password: $Write<string> }>,
52+
{ id: number }
53+
>
54+
>;
55+
56+
// Plain nested object's $Read marker is still stripped from Writable
57+
type _WritableStripsRead = Expect<
58+
Equals<
59+
Writable<{ id: $Read<number>; name: string }>,
60+
{ name: string } & { id?: never }
61+
>
62+
>;

packages/openapi-typescript-helpers/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
"compilerOptions": {
44
"skipLibCheck": false
55
},
6-
"include": ["src"]
6+
"include": ["src", "test"]
77
}

0 commit comments

Comments
 (0)