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.
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,19 @@ 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
Expand Down
12 changes: 5 additions & 7 deletions packages/openapi-fetch/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,12 @@ 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);
// Headers is iterable at runtime in both TS 5 and TS 6, but the lib type only
// declares Iterable in TS 6's lib.dom. Double-cast bridges the gap.
const entries =
headers instanceof Headers ? Array.from(headers as unknown as Iterable<[string, string]>) : Object.entries(headers);
const result: Record<string, string> = {};
for (const [k, v] of iter) {
for (const [k, v] of entries) {
result[k] = v;
}
return result;
Expand Down
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