Skip to content

Commit 11e4fcf

Browse files
committed
Fix Record<string, never> appearing in discriminator union
1 parent 898cf3c commit 11e4fcf

14 files changed

+515
-492
lines changed

.changeset/old-owls-whisper.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": patch
3+
---
4+
5+
Fix Record<string, never> appearing in union

.eslintrc.cjs

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ module.exports = {
44
parserOptions: {
55
project: ["./tsconfig.json"],
66
},
7-
extends: ["eslint:recommended", "plugin:@typescript-eslint/strict"],
8-
plugins: ["@typescript-eslint", "prettier"],
7+
extends: ["eslint:recommended", "plugin:@typescript-eslint/strict", "plugin:vitest/recommended"],
8+
plugins: ["@typescript-eslint", "no-only-tests", "prettier", "vitest"],
99
rules: {
1010
"@typescript-eslint/consistent-indexed-object-style": "off", // sometimes naming keys is more user-friendly
1111
"@typescript-eslint/no-dynamic-delete": "off", // delete is OK
@@ -19,6 +19,8 @@ module.exports = {
1919
rules: {
2020
"@typescript-eslint/ban-ts-comment": "off", // allow @ts-ignore only in tests
2121
"@typescript-eslint/no-empty-function": "off", // don’t enforce this in tests
22+
"no-only-tests/no-only-tests": "error",
23+
"vitest/valid-title": "off", // doesn’t work?
2224
},
2325
},
2426
],

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
"del-cli": "^5.0.0",
2828
"eslint": "^8.44.0",
2929
"eslint-config-prettier": "^8.8.0",
30+
"eslint-plugin-no-only-tests": "^3.1.0",
3031
"eslint-plugin-prettier": "^4.2.1",
32+
"eslint-plugin-vitest": "^0.2.6",
3133
"npm-run-all": "^4.1.5",
3234
"prettier": "^2.8.8",
3335
"typescript": "^5.1.6"

packages/openapi-fetch/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848
"build:ts-min": "esbuild --bundle src/index.ts --format=esm --minify --outfile=dist/index.min.js && cp dist/index.d.ts dist/index.min.d.ts",
4949
"build:cjs": "esbuild --bundle src/index.ts --format=cjs --outfile=dist/index.cjs",
5050
"lint": "pnpm run lint:js",
51-
"lint:js": "prettier --check \"{src,test}/**/*\"",
51+
"lint:js": "eslint \"{src,test}/**/*.{js,ts}\"",
52+
"lint:prettier": "prettier --check \"{src,test}/**/*\"",
5253
"test": "pnpm run test:ts && npm run test:js",
5354
"test:js": "vitest run",
5455
"test:ts": "tsc --noEmit",

packages/openapi-fetch/src/index.test.ts

+10-7
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ describe("client", () => {
227227
});
228228

229229
describe("body", () => {
230+
// these are pure type tests; no runtime assertions needed
231+
/* eslint-disable vitest/expect-expect */
230232
it("requires necessary requestBodies", async () => {
231233
const client = createClient<paths>({ baseUrl: "https://myapi.com/v1" });
232234
mockFetch({ status: 200, body: JSON.stringify({ message: "OK" }) });
@@ -285,6 +287,7 @@ describe("client", () => {
285287
});
286288
});
287289
});
290+
/* eslint-enable vitest/expect-expect */
288291
});
289292

290293
describe("options", () => {
@@ -408,7 +411,7 @@ describe("client", () => {
408411
expect(response.status).toBe(204);
409412

410413
// assert error is empty
411-
expect(error).toBe(undefined);
414+
expect(error).toBeUndefined();
412415
});
413416

414417
it("treats `default` as an error", async () => {
@@ -478,7 +481,7 @@ describe("client", () => {
478481
expect(response.status).toBe(200);
479482

480483
// assert error is empty
481-
expect(error).toBe(undefined);
484+
expect(error).toBeUndefined();
482485
});
483486

484487
it("sends correct options, returns error", async () => {
@@ -500,7 +503,7 @@ describe("client", () => {
500503
expect(response.status).toBe(404);
501504

502505
// assert data is empty
503-
expect(data).toBe(undefined);
506+
expect(data).toBeUndefined();
504507
});
505508

506509
// note: this was a previous bug in the type inference
@@ -543,7 +546,7 @@ describe("client", () => {
543546
expect(response.status).toBe(201);
544547

545548
// assert error is empty
546-
expect(error).toBe(undefined);
549+
expect(error).toBeUndefined();
547550
});
548551

549552
it("supports sepecifying utf-8 encoding", async () => {
@@ -563,7 +566,7 @@ describe("client", () => {
563566
expect(response.status).toBe(201);
564567

565568
// assert error is empty
566-
expect(error).toBe(undefined);
569+
expect(error).toBeUndefined();
567570
});
568571
});
569572

@@ -588,7 +591,7 @@ describe("client", () => {
588591
expect(data).toEqual({});
589592

590593
// assert error is empty
591-
expect(error).toBe(undefined);
594+
expect(error).toBeUndefined();
592595
});
593596

594597
it("returns empty object on Content-Length: 0", async () => {
@@ -604,7 +607,7 @@ describe("client", () => {
604607
expect(data).toEqual({});
605608

606609
// assert error is empty
607-
expect(error).toBe(undefined);
610+
expect(error).toBeUndefined();
608611
});
609612
});
610613

packages/openapi-typescript/examples/digital-ocean-api.ts

+12-17
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@
77
/** WithRequired type helpers */
88
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
99

10-
/** OneOf type helpers */
11-
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
12-
type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
13-
type OneOf<T extends any[]> = T extends [infer Only] ? Only : T extends [infer A, infer B, ...infer Rest] ? OneOf<[XOR<A, B>, ...Rest]> : never;
14-
1510
export interface paths {
1611
"/v2/1-clicks": {
1712
get: external["resources/1-clicks/oneClicks_list.yml"]
@@ -8231,17 +8226,17 @@ export interface external {
82318226
"ratelimit-reset": external["shared/headers.yml"]["ratelimit-reset"];
82328227
};
82338228
content: {
8234-
"application/json": OneOf<[{
8229+
"application/json": {
82358230
droplet: external["resources/droplets/models/droplet.yml"];
82368231
links: {
82378232
actions?: external["shared/models/action_link.yml"][];
82388233
};
8239-
}, {
8234+
} | {
82408235
droplets: external["resources/droplets/models/droplet.yml"][];
82418236
links: {
82428237
actions?: external["shared/models/action_link.yml"][];
82438238
};
8244-
}]>;
8239+
};
82458240
};
82468241
}
82478242
"resources/droplets/responses/droplet_neighbors_ids.yml": {
@@ -9048,13 +9043,13 @@ export interface external {
90489043
droplet_id: number;
90499044
};
90509045
};
9051-
"resources/floating_ips/models/floating_ip_create.yml": OneOf<[{
9046+
"resources/floating_ips/models/floating_ip_create.yml": {
90529047
/**
90539048
* @description The ID of the Droplet that the floating IP will be assigned to.
90549049
* @example 2457247
90559050
*/
90569051
droplet_id: number;
9057-
}, {
9052+
} | {
90589053
/**
90599054
* @description The slug identifier for the region the floating IP will be reserved to.
90609055
* @example nyc3
@@ -9066,7 +9061,7 @@ export interface external {
90669061
* @example 746c6152-2fa2-11ed-92d3-27aaa54e4988
90679062
*/
90689063
project_id?: string;
9069-
}]>
9064+
}
90709065
"resources/floating_ips/models/floating_ip.yml": {
90719066
/**
90729067
* Format: ipv4
@@ -11760,15 +11755,15 @@ export interface external {
1176011755
disable_lets_encrypt_dns_records?: boolean;
1176111756
firewall?: external["resources/load_balancers/models/lb_firewall.yml"];
1176211757
}
11763-
"resources/load_balancers/models/load_balancer_create.yml": OneOf<[WithRequired<{
11758+
"resources/load_balancers/models/load_balancer_create.yml": (WithRequired<{
1176411759
$ref?: external["resources/load_balancers/models/attributes.yml"]["load_balancer_droplet_ids"];
1176511760
} & {
1176611761
region?: external["shared/attributes/region_slug.yml"];
11767-
} & external["resources/load_balancers/models/load_balancer_base.yml"], "droplet_ids" | "region">, WithRequired<{
11762+
} & external["resources/load_balancers/models/load_balancer_base.yml"], "droplet_ids" | "region">) | (WithRequired<{
1176811763
$ref?: external["resources/load_balancers/models/attributes.yml"]["load_balancer_droplet_tag"];
1176911764
} & {
1177011765
region?: external["shared/attributes/region_slug.yml"];
11771-
} & external["resources/load_balancers/models/load_balancer_base.yml"], "tag" | "region">]>
11766+
} & external["resources/load_balancers/models/load_balancer_base.yml"], "tag" | "region">)
1177211767
"resources/load_balancers/models/load_balancer.yml": external["resources/load_balancers/models/load_balancer_base.yml"] & {
1177311768
region?: external["resources/regions/models/region.yml"];
1177411769
} & {
@@ -13782,13 +13777,13 @@ export interface external {
1378213777
droplet_id: number;
1378313778
};
1378413779
};
13785-
"resources/reserved_ips/models/reserved_ip_create.yml": OneOf<[{
13780+
"resources/reserved_ips/models/reserved_ip_create.yml": {
1378613781
/**
1378713782
* @description The ID of the Droplet that the reserved IP will be assigned to.
1378813783
* @example 2457247
1378913784
*/
1379013785
droplet_id: number;
13791-
}, {
13786+
} | {
1379213787
/**
1379313788
* @description The slug identifier for the region the reserved IP will be reserved to.
1379413789
* @example nyc3
@@ -13800,7 +13795,7 @@ export interface external {
1380013795
* @example 746c6152-2fa2-11ed-92d3-27aaa54e4988
1380113796
*/
1380213797
project_id?: string;
13803-
}]>
13798+
}
1380413799
"resources/reserved_ips/models/reserved_ip.yml": {
1380513800
/**
1380613801
* Format: ipv4

0 commit comments

Comments
 (0)