Skip to content
This repository has been archived by the owner on Nov 28, 2023. It is now read-only.

Commit

Permalink
Add type helpers for queries/mutations (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
Schniz authored Sep 5, 2022
1 parent f732e3c commit 5b90c45
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 30 deletions.
6 changes: 6 additions & 0 deletions .changeset/kind-carrots-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@next-fetch/react-query": patch
"@next-fetch/swr": patch
---

Add type helpers to get input/output of hooks
37 changes: 33 additions & 4 deletions packages/@next-fetch/react-query/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,35 @@ import type { NextConfig } from "next";
import type { HookMetadata } from "@next-fetch/core-plugin/client";
import { createPlugin } from "@next-fetch/core-plugin";

export type QueryResult<Input, Output> = (v: Input) => UseQueryResult<Output>;
export type MutationResult<Input, Output> = () => UseMutationResult<
Output,
any,
Input
> & { meta: HookMetadata };

export function query<Output>(
callback: HandlerCallback<void, Output>,
options?: Partial<HookIntoResponse<Output>>
): () => UseQueryResult<Output>;
): QueryResult<void, Output>;
export function query<Input, Output>(
parser: Parser<Input>,
callback: HandlerCallback<Input, Output>,
options?: Partial<HookIntoResponse<Output>>
): (v: Input) => UseQueryResult<Output>;
): QueryResult<Input, Output>;
export function query(): unknown {
throw new Error("This code path should not be reached");
}

export function mutation<Output>(
callback: HandlerCallback<void, Output>,
options?: Partial<HookIntoResponse<Output>>
): () => UseMutationResult<Output, any, void> & { meta: HookMetadata };
): MutationResult<void, Output>;
export function mutation<Input, Output>(
parser: Parser<Input>,
callback: HandlerCallback<Input, Output>,
options?: Partial<HookIntoResponse<Output>>
): () => UseMutationResult<Output, any, Input> & { meta: HookMetadata };
): MutationResult<Input, Output>;
export function mutation(): unknown {
throw new Error("This code path should not be reached");
}
Expand All @@ -43,3 +50,25 @@ export function withReactQueryApiEndpoints(given: NextConfig = {}): NextConfig {
serverPackageName: "@next-fetch/react-query/server",
})(given);
}

/**
* Retrieves the type of the input of a given query/mutation hook
*/
export type inputOf<
T extends QueryResult<any, any> | MutationResult<any, any>
> = T extends MutationResult<infer Input, any>
? Input
: T extends QueryResult<infer Input, any>
? Input
: never;

/**
* Retrieves the type of the output of a given query/mutation hook
*/
export type outputOf<
T extends QueryResult<any, any> | MutationResult<any, any>
> = T extends MutationResult<any, infer Output>
? Output
: T extends QueryResult<any, infer Output>
? Output
: never;
48 changes: 32 additions & 16 deletions packages/@next-fetch/swr/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,42 @@ import type {
import { createPlugin } from "@next-fetch/core-plugin";
import type { NextConfig } from "next";

export type QueryResult<Input, Output> = ((v: Input) => SWRResponse<Output>) & {
meta: HookMetadata;
};

export function query<Output>(
callback: HandlerCallback<void, Output>,
options?: Partial<HookIntoResponse<Output>>
): (() => SWRResponse<Output>) & { meta: HookMetadata };
): QueryResult<void, Output>;
export function query<Input, Output>(
parser: Parser<Input>,
callback: HandlerCallback<Input, Output>,
options?: Partial<HookIntoResponse<Output>>
): ((v: Input) => SWRResponse<Output>) & { meta: HookMetadata };
export function query<Input, Output>(
parser: unknown,
callback: unknown,
options?: unknown
): ((v: Input) => SWRResponse<Output>) & { meta: HookMetadata } {
): QueryResult<Input, Output>;
export function query(): unknown {
throw new Error("This code path should not be reached");
}

export type MutationOptions<Output> = HookIntoResponse<Output>;
export type MutationResult<Input, Output> = (() => SWRMutationResponse<
Output,
any,
Input
> & { meta: HookMetadata }) & {
meta: HookMetadata;
};

export function mutation<Output>(
callback: HandlerCallback<void, Output>,
options?: Partial<MutationOptions<Output>>
): () => SWRMutationResponse<Output, any, void> & { meta: HookMetadata };
): MutationResult<void, Output>;
export function mutation<Input, Output>(
parser: Parser<Input>,
callback: HandlerCallback<Input, Output>,
options?: Partial<MutationOptions<Output>>
): () => SWRMutationResponse<Output, any, Input> & { meta: HookMetadata };
export function mutation<Input, Output>(
parser: unknown,
callback: unknown,
options?: unknown
): (() => SWRMutationResponse<Output, any, Input> & { meta: HookMetadata }) & {
meta: HookMetadata;
} {
): MutationResult<Input, Output>;
export function mutation(): unknown {
throw new Error("This code path should not be reached");
}

Expand All @@ -56,3 +57,18 @@ export function withSwrApiEndpoints(given: NextConfig = {}): NextConfig {
serverPackageName: "@next-fetch/swr/server",
})(given);
}

export type inputOf<
T extends MutationResult<any, any> | QueryResult<any, any>
> = T extends MutationResult<infer Input, any>
? Input
: T extends QueryResult<infer Input, any>
? Input
: never;
export type outputOf<
T extends MutationResult<any, any> | QueryResult<any, any>
> = T extends MutationResult<any, infer Output>
? Output
: T extends QueryResult<any, infer Output>
? Output
: never;
13 changes: 9 additions & 4 deletions packages/example-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@
"start": "next start",
"dev": "next dev --port=3002",
"prepare-env:test": "playwright install --with-deps",
"test": "playwright test",
"test": "concurrently 'pnpm test:tsc' 'pnpm test:e2e' 'pnpm test:unit'",
"test:tsc": "tsc --project tsconfig.tests.json",
"test:e2e": "playwright test",
"test:unit": "vitest run",
"vercel-build": "pnpm run -w build"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@tanstack/react-query": "^4.0.10",
"next": "^12.2.4",
"@next-fetch/react-query": "workspace:*",
"@next-fetch/swr": "workspace:*",
"@tanstack/react-query": "^4.0.10",
"next": "^12.2.4",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"swr": "^2.0.0-beta.6",
Expand All @@ -30,6 +33,8 @@
"@types/node": "^17.0.38",
"@types/react": "^18.0.10",
"@types/react-dom": "^18.0.5",
"typescript": "^4.7.2"
"concurrently": "^7.3.0",
"typescript": "^4.7.2",
"vitest": "^0.20.2"
}
}
3 changes: 3 additions & 0 deletions packages/example-app/pages/api/people.swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import z from "zod";
import { query, mutation } from "@next-fetch/swr";
import { userAgent } from "next/server";

/**
* Queries a string
*/
export const useAllPeople = query(
async () => {
return `Many people are here!`;
Expand Down
90 changes: 90 additions & 0 deletions packages/example-app/tests/types/type-inference.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { test, describe } from "vitest";
import type {
inputOf as rqInputOf,
outputOf as rqOutputOf,
} from "@next-fetch/react-query";
import type {
useListPeopleWith as ReactQueryUseListPeopleWith,
usePerson as ReactQueryUsePerson,
useAllPeople as ReactQueryUseAllPeople,
} from "../../pages/api/rq/people.rq";
import type {
inputOf as swrInputOf,
outputOf as swrOutputOf,
} from "@next-fetch/swr";
import type {
useListPeopleWith as SWRUseListPeopleWith,
usePerson as SWRUsePerson,
useAllPeople as SWRUseAllPeople,
} from "../../pages/api/people.swr";

type IsAny<T> = 0 extends 1 & T ? true : false;
type SameType<T, U> = true extends IsAny<T> | IsAny<U>
? false
: [T] extends [U]
? [U] extends [T]
? true
: false
: false;
function expectSameType<T, U>(_v: SameType<T, U>) {}

describe("helpers", () => {
test("expectSameType", () => {
expectSameType<string, boolean>(false);
expectSameType<string, string>(true);
expectSameType<unknown, string>(false);

// Check that `any` fails the conditional as well
expectSameType<any, string>(false);
expectSameType<string, any>(false);

// @ts-expect-error this should fail
expectSameType<string, string>(false);
});
});

describe("react query", () => {
test("mutation", () => {
type Input = rqInputOf<typeof ReactQueryUseListPeopleWith>;
type Output = rqOutputOf<typeof ReactQueryUseListPeopleWith>;
expectSameType<Input, { name: string }>(true);
expectSameType<Output, string[]>(true);
});

test("query with no args", () => {
type Input = rqInputOf<typeof ReactQueryUseAllPeople>;
type Output = rqOutputOf<typeof ReactQueryUseAllPeople>;
expectSameType<Input, void>(true);
expectSameType<Output, string>(true);
});

test("query with args", () => {
type Input = rqInputOf<typeof ReactQueryUsePerson>;
type Output = rqOutputOf<typeof ReactQueryUsePerson>;
expectSameType<Input, { name: string }>(true);
expectSameType<Output, string>(true);
});
});

describe("swr", () => {
test("mutation", () => {
type Input = swrInputOf<typeof SWRUseListPeopleWith>;
type Output = swrOutputOf<typeof SWRUseListPeopleWith>;
expectSameType<Input, { name: string }>(true);
expectSameType<Output, string[]>(true);
});

test("query with no args", () => {
type Input = swrInputOf<typeof SWRUseAllPeople>;
type Output = swrOutputOf<typeof SWRUseAllPeople>;
expectSameType<Input, void>(true);
expectSameType<Output, string>(true);
});

test("query with args", () => {
type Input = swrInputOf<typeof SWRUsePerson>;
type Output = swrOutputOf<typeof SWRUsePerson>;
expectSameType<Input, { name: string }>(true);
expectSameType<Output, string>(true);
});
});
9 changes: 9 additions & 0 deletions packages/example-app/tsconfig.tests.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"incremental": false
},
"include": ["./tests/**/*.ts"]
}
6 changes: 6 additions & 0 deletions packages/example-app/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
exclude: ["**/node_modules/**", "**/dist/**", "**/e2e/**"],
},
});
Loading

2 comments on commit 5b90c45

@vercel
Copy link

@vercel vercel bot commented on 5b90c45 Sep 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-swr-endpoints-example-app – ./packages/example-app

next-swr-endpoints-example-app-vercel-labs.vercel.app
next-swr-endpoints-example-app-git-main-vercel-labs.vercel.app
next-swr-endpoints-example-app.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 5b90c45 Sep 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-fetch – ./packages/docs

next-fetch-pi.vercel.app
next-fetch-git-main-vercel-labs.vercel.app
next-fetch-vercel-labs.vercel.app

Please sign in to comment.