diff --git a/.changeset/slimy-cows-jump.md b/.changeset/slimy-cows-jump.md new file mode 100644 index 000000000..966e889ab --- /dev/null +++ b/.changeset/slimy-cows-jump.md @@ -0,0 +1,5 @@ +--- +"swr-openapi": minor +--- + +Disallow extra properties in swr-openapi init types diff --git a/packages/swr-openapi/src/__test__/types.test-d.ts b/packages/swr-openapi/src/__test__/types.test-d.ts index b3a16420b..1d2ea0e0b 100644 --- a/packages/swr-openapi/src/__test__/types.test-d.ts +++ b/packages/swr-openapi/src/__test__/types.test-d.ts @@ -85,6 +85,44 @@ describe("types", () => { useQuery("/pet/findByStatus", null); }); }); + + describe("rejects extra properties", () => { + it("in query params", () => { + useQuery("/pet/findByStatus", { + params: { + query: { + status: "available", + // @ts-expect-error extra property should be rejected + invalid_property: "nope", + }, + }, + }); + }); + + it("in path params", () => { + useQuery("/pet/{petId}", { + params: { + path: { + petId: 5, + // @ts-expect-error extra property should be rejected + invalid_path_param: "nope", + }, + }, + }); + }); + + it("in header params", () => { + useQuery("/pet/findByStatus", { + params: { + header: { + "X-Example": "test", + // @ts-expect-error extra property should be rejected + "Invalid-Header": "nope", + }, + }, + }); + }); + }); }); describe("useImmutable", () => { @@ -122,6 +160,44 @@ describe("types", () => { useImmutable("/pet/findByStatus", null); }); }); + + describe("rejects extra properties", () => { + it("in query params", () => { + useImmutable("/pet/findByStatus", { + params: { + query: { + status: "available", + // @ts-expect-error extra property should be rejected + invalid_property: "nope", + }, + }, + }); + }); + + it("in path params", () => { + useImmutable("/pet/{petId}", { + params: { + path: { + petId: 5, + // @ts-expect-error extra property should be rejected + invalid_path_param: "nope", + }, + }, + }); + }); + + it("in header params", () => { + useImmutable("/pet/findByStatus", { + params: { + header: { + "X-Example": "test", + // @ts-expect-error extra property should be rejected + "Invalid-Header": "nope", + }, + }, + }); + }); + }); }); describe("useInfinite", () => { @@ -154,40 +230,47 @@ describe("types", () => { useInfinite("/pet/findByStatus", () => null); }); }); - }); - describe("useMutate -> mutate", () => { - it("accepts path alone", async () => { - await mutate(["/pet/{petId}"]); - }); + describe("rejects extra properties", () => { + it("in query params", () => { + useInfinite("/pet/findByStatus", () => ({ + params: { + query: { + status: "available", + // @ts-expect-error extra property should be rejected + invalid_property: "nope", + }, + }, + })); + }); - it("accepts path and init", async () => { - await mutate([ - "/pet/{petId}", - { + it("in path params", () => { + useInfinite("/pet/{petId}", () => ({ params: { path: { petId: 5, + // @ts-expect-error extra property should be rejected + invalid_path_param: "nope", }, }, - }, - ]); - }); - - it("accepts partial init", async () => { - await mutate(["/pet/{petId}", { params: {} }]); - }); + })); + }); - it("does not accept `null` init", async () => { - await mutate([ - "/pet/{petId}", - // @ts-expect-error null not accepted - null, - ]); + it("in header params", () => { + useInfinite("/pet/findByStatus", () => ({ + params: { + header: { + "X-Example": "test", + // @ts-expect-error extra property should be rejected + "Invalid-Header": "nope", + }, + }, + })); + }); }); }); - describe("when init is not required", () => { + describe("useMutate -> mutate", () => { it("accepts path alone", async () => { await mutate(["/pet/{petId}"]); }); @@ -216,6 +299,84 @@ describe("types", () => { null, ]); }); + + describe("when init is not required", () => { + it("accepts path alone", async () => { + await mutate(["/pet/{petId}"]); + }); + + it("accepts path and init", async () => { + await mutate([ + "/pet/{petId}", + { + params: { + path: { + petId: 5, + }, + }, + }, + ]); + }); + + it("accepts partial init", async () => { + await mutate(["/pet/{petId}", { params: {} }]); + }); + + it("does not accept `null` init", async () => { + await mutate([ + "/pet/{petId}", + // @ts-expect-error null not accepted + null, + ]); + }); + }); + + describe("rejects extra properties", () => { + it("in path", () => { + mutate([ + "/pet/{petId}", + { + params: { + path: { + petId: 5, + // @ts-expect-error extra property should be rejected + invalid_path_param: "no", + }, + }, + }, + ]); + }); + + it("in query params", () => { + mutate([ + "/pet/findByStatus", + { + params: { + query: { + status: "available", + // @ts-expect-error extra property should be rejected + invalid_property: "nope", + }, + }, + }, + ]); + }); + + it("in header params", () => { + mutate([ + "/pet/findByStatus", + { + params: { + header: { + "X-Example": "test", + // @ts-expect-error extra property should be rejected + "Invalid-Header": "nope", + }, + }, + }, + ]); + }); + }); }); }); diff --git a/packages/swr-openapi/src/infinite.ts b/packages/swr-openapi/src/infinite.ts index c17584c30..22932298c 100644 --- a/packages/swr-openapi/src/infinite.ts +++ b/packages/swr-openapi/src/infinite.ts @@ -7,6 +7,7 @@ import useSWRInfinite, { } from "swr/infinite"; import type { TypesForGetRequest } from "./types.js"; import { useCallback, useDebugValue } from "react"; +import type { Exact } from "type-fest"; /** * Produces a typed wrapper for [`useSWRInfinite`](https://swr.vercel.app/docs/pagination#useswrinfinite). @@ -42,7 +43,7 @@ export function createInfiniteHook< return function useInfinite< Path extends PathsWithMethod, R extends TypesForGetRequest, - Init extends R["Init"], + Init extends Exact, Data extends R["Data"], Error extends R["Error"] | FetcherError, Config extends SWRInfiniteConfiguration, diff --git a/packages/swr-openapi/src/mutate.ts b/packages/swr-openapi/src/mutate.ts index 4304026be..d69b11099 100644 --- a/packages/swr-openapi/src/mutate.ts +++ b/packages/swr-openapi/src/mutate.ts @@ -2,7 +2,7 @@ import type { Client } from "openapi-fetch"; import type { MediaType, PathsWithMethod } from "openapi-typescript-helpers"; import { useCallback, useDebugValue } from "react"; import { type MutatorCallback, type MutatorOptions, useSWRConfig } from "swr"; -import type { PartialDeep } from "type-fest"; +import type { Exact, PartialDeep } from "type-fest"; import type { TypesForGetRequest } from "./types.js"; // Types are loose here to support ecosystem utilities like `_.isMatch` @@ -48,7 +48,7 @@ export function createMutateHook function mutate< Path extends PathsWithMethod, R extends TypesForGetRequest, - Init extends R["Init"], + Init extends Exact, >( [path, init]: [Path, PartialDeep?], data?: R["Data"] | Promise | MutatorCallback, diff --git a/packages/swr-openapi/src/query-base.ts b/packages/swr-openapi/src/query-base.ts index bf6ff3d88..cbdea6c0e 100644 --- a/packages/swr-openapi/src/query-base.ts +++ b/packages/swr-openapi/src/query-base.ts @@ -3,6 +3,7 @@ import type { MediaType, PathsWithMethod, RequiredKeysOf } from "openapi-typescr import type { Fetcher, SWRHook } from "swr"; import type { TypesForGetRequest } from "./types.js"; import { useCallback, useDebugValue, useMemo } from "react"; +import type { Exact } from "type-fest"; /** * @private @@ -17,7 +18,7 @@ export function configureBaseQueryHook(useHook: SWRHook) { return function useQuery< Path extends PathsWithMethod, R extends TypesForGetRequest, - Init extends R["Init"], + Init extends Exact, Data extends R["Data"], Error extends R["Error"] | FetcherError, Config extends R["SWRConfig"],