Skip to content
Open
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
9 changes: 9 additions & 0 deletions .changeset/widen-typescript-peer-dep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"openapi-typescript": patch
"openapi-typescript-helpers": patch
---

Add TypeScript 6 support.

- `openapi-typescript`: widen the `typescript` peer dependency to `^5.x || ^6.x`.
- `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.
27 changes: 23 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,46 @@ jobs:
strategy:
matrix:
node-version: [22, 24]
typescript-version: ["5", "6"]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- uses: pnpm/action-setup@v5
with:
run_install: true
- name: Override catalog to TypeScript 5 (backward-compat check)
if: matrix.typescript-version == '5'
run: |
sed -i 's/^ typescript: \^6\..*/ typescript: ^5.9.3/' pnpm-workspace.yaml
grep '^ typescript:' pnpm-workspace.yaml
- run: pnpm install --no-frozen-lockfile
- run: pnpm test
test-e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: latest
node-version: 22 # Lock to the current stable LTS to fix newer Node stream leaks
- uses: pnpm/action-setup@v5
with:
run_install: true
- run: pnpm exec playwright install --with-deps
- run: pnpm run test-e2e

- name: Run E2E Tests
run: pnpm run test-e2e
timeout-minutes: 10 # Gracefully drops the job if it manages to hang, saving runner limits
env:
DEBUG: pw:webserver,pw:browser # Dumps real-time initialization data straight to stdout
NODE_ENV: test

- name: Upload Playwright Trace Report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: packages/openapi-fetch/playwright-report/
retention-days: 1
test-macos:
runs-on: macos-latest
steps:
Expand Down
18 changes: 14 additions & 4 deletions packages/openapi-fetch/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import { defineConfig, devices } from "@playwright/test";

const PORT = Number.parseInt(process.env.PORT || 4173 || "", 10);
const HOST = "127.0.0.1";
const PORT = Number.parseInt(process.env.PORT || "4173", 10);

export default defineConfig({
testMatch: "test/**/*.e2e.ts",
webServer: {
command: "pnpm run e2e-vite-build && pnpm run e2e-vite-start",
// We use the root-safe package workspace filter to accurately point to the exact local commands
command: `pnpm --filter openapi-fetch run e2e-vite-build && pnpm --filter openapi-fetch run e2e-vite-start --host ${HOST} --port ${PORT}`,
port: PORT,
reuseExistingServer: !process.env.CI,
timeout: 120_000, // Provides plenty of overhead room for TS6 type compilation
},
use: {
baseURL: `http://localhost:${PORT}`,
baseURL: `http://${HOST}:${PORT}`,
trace: "retain-on-failure",
},
projects: [
{
name: "chrome",
use: { ...devices["Desktop Chrome"] },
use: {
...devices["Desktop Chrome"],
launchOptions: {
args: ["--no-sandbox", "--disable-setuid-sandbox"],
},
},
},
{
name: "firefox",
Expand Down
16 changes: 8 additions & 8 deletions packages/openapi-fetch/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ export function createObservedClient<T extends {}, M extends MediaType = MediaTy
* Convert a Headers object to a plain object for easier comparison
*/
export function headersToObj(headers: Headers | Record<string, string>): Record<string, string> {
const iter =
headers instanceof Headers
? headers
// @ts-expect-error FIXME: this is a missing "lib" in tsconfig.json but dunno what
.entries()
: Object.entries(headers);
const result: Record<string, string> = {};
for (const [k, v] of iter) {
result[k] = v;
if (headers instanceof Headers) {
headers.forEach((value, key) => {
result[key] = value;
});
} else {
for (const [key, value] of Object.entries(headers)) {
result[key] = value;
}
}
return result;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "../..",
"strictNullChecks": false
},
"include": ["."],
Expand Down
1 change: 0 additions & 1 deletion packages/openapi-fetch/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"declaration": true,
"downlevelIteration": false,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"module": "NodeNext",
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-typescript-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@
"lint:ts": "tsc --noEmit"
},
"devDependencies": {
"typescript": "5.9.3"
"typescript": "catalog:"
}
}
20 changes: 12 additions & 8 deletions packages/openapi-typescript-helpers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,11 @@ export type Readable<T> =
? Readable<U>
: T extends (infer E)[]
? Readable<E>[]
: T extends object
? { [K in keyof T as NonNullable<T[K]> extends $Write<any> ? never : K]: Readable<T[K]> }
: T;
: T extends (...args: never[]) => unknown
? T
: T extends object
? { [K in keyof T as NonNullable<T[K]> extends $Write<any> ? never : K]: Readable<T[K]> }
: T;

/**
* Resolve type for writing (requests): strips $Read properties, unwraps $Write
Expand All @@ -240,8 +242,10 @@ export type Writable<T> =
? Writable<U>
: T extends (infer E)[]
? Writable<E>[]
: T extends object
? { [K in keyof T as NonNullable<T[K]> extends $Read<any> ? never : K]: Writable<T[K]> } & {
[K in keyof T as NonNullable<T[K]> extends $Read<any> ? K : never]?: never;
}
: T;
: T extends (...args: never[]) => unknown
? T
: T extends object
? { [K in keyof T as NonNullable<T[K]> extends $Read<any> ? never : K]: Writable<T[K]> } & {
[K in keyof T as NonNullable<T[K]> extends $Read<any> ? K : never]?: never;
}
: T;
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Type-level tests for Readable<T> / Writable<T> built-in object passthrough.
*
* These are pure compile-time assertions checked by `tsc --noEmit`. If any
* assertion is wrong, the file fails to type-check.
*/

import type { $Read, $Write, Readable, Writable } from "../src/index.js";

// Bidirectional structural assignability. Looser than the parametric `(<T>() => …)`
// trick but enough for "the resulting shape preserves X" — mapped-type-produced
// objects are structurally identical to their literal twins but fail strict
// parametric equality.
type Equals<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
type Expect<T extends true> = T;

// --- Date passthrough ---

type _ReadableDate = Expect<Equals<Readable<Date>, Date>>;
type _WritableDate = Expect<Equals<Writable<Date>, Date>>;

// $Read<Date> unwraps to Date, not to a structurally-mapped Date prototype
type _ReadableReadDate = Expect<Equals<Readable<$Read<Date>>, Date>>;
type _WritableWriteDate = Expect<Equals<Writable<$Write<Date>>, Date>>;

// Date inside an object field stays Date
type _ReadableObjectWithDate = Expect<Equals<Readable<{ created: Date }>, { created: Date }>>;
type _WritableObjectWithDate = Expect<Equals<Writable<{ created: Date }>, { created: Date }>>;

// --- RegExp passthrough ---

type _ReadableRegExp = Expect<Equals<Readable<RegExp>, RegExp>>;
type _WritableRegExp = Expect<Equals<Writable<RegExp>, RegExp>>;

type _ReadableObjectWithRegExp = Expect<Equals<Readable<{ pattern: RegExp }>, { pattern: RegExp }>>;

// --- Function passthrough ---

type Fn = (x: number) => string;

type _ReadableFn = Expect<Equals<Readable<Fn>, Fn>>;
type _WritableFn = Expect<Equals<Writable<Fn>, Fn>>;

type _ReadableObjectWithFn = Expect<Equals<Readable<{ handler: Fn }>, { handler: Fn }>>;

// --- Negative control: plain object still gets recursive treatment ---

// Plain nested object's $Write marker is still stripped from Readable
type _ReadableStripsWrite = Expect<
Equals<
Readable<{ id: number; password: $Write<string> }>,
{ id: number }
>
>;

// Plain nested object's $Read marker is still stripped from Writable
type _WritableStripsRead = Expect<
Equals<
Writable<{ id: $Read<number>; name: string }>,
{ name: string } & { id?: never }
>
>;
2 changes: 1 addition & 1 deletion packages/openapi-typescript-helpers/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
"compilerOptions": {
"skipLibCheck": false
},
"include": ["src"]
"include": ["src", "test"]
}
2 changes: 1 addition & 1 deletion packages/openapi-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"version": "pnpm run build"
},
"peerDependencies": {
"typescript": "^5.x"
"typescript": "^5.x || ^6.x"
},
"dependencies": {
"@redocly/openapi-core": "^1.34.6",
Expand Down
Loading
Loading