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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, expectTypeOf, test } from "vitest";
import { createObservedClient } from "../helpers.js";
import type { paths } from "./schemas/read-write.js";
import type { paths, Readable, Writable } from "./schemas/read-write.js";

describe("readOnly/writeOnly", () => {
describe("deeply nested $Read unwrapping through $Read<Object>", () => {
Expand Down Expand Up @@ -110,4 +110,37 @@ describe("readOnly/writeOnly", () => {
expect(name).toBe("Alice");
});
});

describe("branded primitive types", () => {
test("Readable preserves branded string in object property", () => {
type BrandedString = string & { __brand: "BrandedString" };
type Schema = { id: BrandedString; name: string };
// Without fix: Readable maps branded primitives through the object branch,
// expanding ALL string prototype methods and producing a type that is NOT
// assignable back to BrandedString. The assignment below would be a type error.
const result = {} as Readable<Schema>;
const _id: BrandedString = result.id;
});

test("Writable preserves branded string in object property", () => {
type BrandedString = string & { __brand: "BrandedString" };
type Schema = { id: BrandedString; name: string };
const result = {} as Writable<Schema>;
const _id: BrandedString = result.id;
});

test("Readable preserves branded number in object property", () => {
type UserId = number & { __brand: "UserId" };
type Schema = { id: UserId; name: string };
const result = {} as Readable<Schema>;
const _id: UserId = result.id;
});

test("Writable preserves branded number in object property", () => {
type UserId = number & { __brand: "UserId" };
type Schema = { id: UserId; name: string };
const result = {} as Writable<Schema>;
const _id: UserId = result.id;
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ export type $Read<T> = {
export type $Write<T> = {
readonly $write: T;
};
export type Readable<T> = T extends $Write<any> ? never : T extends $Read<infer U> ? Readable<U> : T extends (infer E)[] ? Readable<E>[] : T extends object ? {
export type Readable<T> = T extends $Write<any> ? never : T extends $Read<infer U> ? Readable<U> : T extends (infer E)[] ? Readable<E>[] : T extends string | number | boolean | bigint | symbol ? T : T extends object ? {
[K in keyof T as NonNullable<T[K]> extends $Write<any> ? never : K]: Readable<T[K]>;
} : T;
export type Writable<T> = T extends $Read<any> ? never : T extends $Write<infer U> ? Writable<U> : T extends (infer E)[] ? Writable<E>[] : T extends object ? {
export type Writable<T> = T extends $Read<any> ? never : T extends $Write<infer U> ? Writable<U> : T extends (infer E)[] ? Writable<E>[] : T extends string | number | boolean | bigint | symbol ? 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;
Expand Down
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 string | number | boolean | bigint | symbol
? 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 string | number | boolean | bigint | symbol
? 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;
4 changes: 2 additions & 2 deletions packages/openapi-typescript/src/transform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ const transformers: Record<SchemaTransforms, (node: any, options: GlobalContext)
const READ_WRITE_HELPER_TYPES = `
export type $Read<T> = { readonly $read: T };
export type $Write<T> = { readonly $write: T };
export type Readable<T> = T extends $Write<any> ? never : T extends $Read<infer U> ? 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;
export type Writable<T> = T extends $Read<any> ? never : T extends $Write<infer U> ? 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;
export type Readable<T> = T extends $Write<any> ? never : T extends $Read<infer U> ? Readable<U> : T extends (infer E)[] ? Readable<E>[] : T extends string | number | boolean | bigint | symbol ? T : T extends object ? { [K in keyof T as NonNullable<T[K]> extends $Write<any> ? never : K]: Readable<T[K]> } : T;
export type Writable<T> = T extends $Read<any> ? never : T extends $Write<infer U> ? Writable<U> : T extends (infer E)[] ? Writable<E>[] : T extends string | number | boolean | bigint | symbol ? 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;
`;

export default function transformSchema(schema: OpenAPI3, ctx: GlobalContext) {
Expand Down
Loading