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
5 changes: 5 additions & 0 deletions .changeset/silver-null-bodies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript-helpers": patch
---

Preserve optional null properties when filtering request body readOnly markers.
48 changes: 48 additions & 0 deletions packages/openapi-fetch/test/types.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,55 @@
import type { ErrorResponse, GetResponseContent, OkStatus, SuccessResponse } from "openapi-typescript-helpers";
import { assertType, describe, test } from "vitest";
import createClient from "../src/index.js";

describe("types", () => {
describe("request body", () => {
interface Paths {
"/": {
post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": Components["schemas"]["T"];
};
};
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
};
};
};
}

interface Components {
schemas: {
T: {
x?: null;
};
};
}

test("allows optional null request body properties", () => {
const demo = (body: Components["schemas"]["T"]) => {
const client = createClient<Paths>();
return client.POST("/", { body });
};

assertType<(body: Components["schemas"]["T"]) => Promise<unknown>>(demo);
});
});

describe("GetResponseContent", () => {
describe("MixedResponses", () => {
interface MixedResponses {
Expand Down
9 changes: 6 additions & 3 deletions packages/openapi-typescript-helpers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ export type $Read<T> = { readonly $read: T };
/** Marker type for writeOnly properties (excluded from response bodies) */
export type $Write<T> = { readonly $write: T };

type HasReadMarker<T> = [NonNullable<T>] extends [never] ? false : NonNullable<T> extends $Read<any> ? true : false;
type HasWriteMarker<T> = [NonNullable<T>] extends [never] ? false : NonNullable<T> extends $Write<any> ? true : false;

/**
* Resolve type for reading (responses): strips $Write properties, unwraps $Read
* - $Read<T> → T (readable), continues recursion
Expand All @@ -224,7 +227,7 @@ export type Readable<T> =
: 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]> }
? { [K in keyof T as HasWriteMarker<T[K]> extends true ? never : K]: Readable<T[K]> }
: T;

/**
Expand All @@ -241,7 +244,7 @@ export type Writable<T> =
: 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;
? { [K in keyof T as HasReadMarker<T[K]> extends true ? never : K]: Writable<T[K]> } & {
[K in keyof T as HasReadMarker<T[K]> extends true ? K : never]?: never;
}
: T;
Loading