From 32d1f7568e1a4b72fbe2de4ada6985c65675a84b Mon Sep 17 00:00:00 2001 From: Brian Bugh Date: Fri, 10 Jan 2020 17:16:51 -0600 Subject: [PATCH] fix(ts): Add full test coverage for TypeScript hook types & fix missing overloads (#895) * fix: add apollo-composable type overloads + tests There were a lot of TypeScript edge cases, where calling functions with certain argument options would result in `any` being expressed as a type, or where the variables/options were not strictly requiring the desired inputs. This adds TypeScript function overloads for all hook edge cases so that all types are correct. This does not change any behavior. This also adds almost complete coverage for types, excepting the cases where `strict` is required, or where a failure is expected, which TypeScript does not currently support. See: https://github.com/microsoft/TypeScript/issues/29394 * chore: run type tests on prepublish * chore: code style --- packages/vue-apollo-composable/package.json | 4 +- .../src/useApolloClient.ts | 7 +- .../vue-apollo-composable/src/useMutation.ts | 65 +++++- .../vue-apollo-composable/src/useQuery.ts | 67 +++++- .../vue-apollo-composable/src/useResult.ts | 80 +++++++- .../src/useSubscription.ts | 59 +++++- .../src/util/ExtractSingleKey.ts | 9 + .../tests/fixtures/graphql-example-types.ts | 85 ++++++++ .../tests/types/assertions.ts | 9 + .../tests/types/tsconfig.json | 11 + .../tests/types/useApolloClient-types.test.ts | 32 +++ .../tests/types/useLoading-types.test.ts | 50 +++++ .../tests/types/useMutation-types.test.ts | 147 +++++++++++++ .../tests/types/useQuery-types.test.ts | 148 +++++++++++++ .../tests/types/useResult-types.test.ts | 146 +++++++++++++ .../tests/types/useSubscription-types.test.ts | 194 ++++++++++++++++++ .../tests/types/util/ExtractSingleKey.test.ts | 28 +++ 17 files changed, 1118 insertions(+), 23 deletions(-) create mode 100644 packages/vue-apollo-composable/src/util/ExtractSingleKey.ts create mode 100644 packages/vue-apollo-composable/tests/fixtures/graphql-example-types.ts create mode 100644 packages/vue-apollo-composable/tests/types/assertions.ts create mode 100644 packages/vue-apollo-composable/tests/types/tsconfig.json create mode 100644 packages/vue-apollo-composable/tests/types/useApolloClient-types.test.ts create mode 100644 packages/vue-apollo-composable/tests/types/useLoading-types.test.ts create mode 100644 packages/vue-apollo-composable/tests/types/useMutation-types.test.ts create mode 100644 packages/vue-apollo-composable/tests/types/useQuery-types.test.ts create mode 100644 packages/vue-apollo-composable/tests/types/useResult-types.test.ts create mode 100644 packages/vue-apollo-composable/tests/types/useSubscription-types.test.ts create mode 100644 packages/vue-apollo-composable/tests/types/util/ExtractSingleKey.test.ts diff --git a/packages/vue-apollo-composable/package.json b/packages/vue-apollo-composable/package.json index b36c92b..e59454b 100644 --- a/packages/vue-apollo-composable/package.json +++ b/packages/vue-apollo-composable/package.json @@ -29,7 +29,9 @@ "scripts": { "dev": "yarn build --watch", "build": "tsc --outDir dist -d", - "prepublishOnly": "yarn build" + "prepublishOnly": "yarn test && yarn build", + "test": "yarn test:types", + "test:types": "tsc -p tests/types/" }, "dependencies": { "throttle-debounce": "^2.1.0" diff --git a/packages/vue-apollo-composable/src/useApolloClient.ts b/packages/vue-apollo-composable/src/useApolloClient.ts index 824a070..2a848da 100644 --- a/packages/vue-apollo-composable/src/useApolloClient.ts +++ b/packages/vue-apollo-composable/src/useApolloClient.ts @@ -4,7 +4,12 @@ import ApolloClient from 'apollo-client' export const DefaultApolloClient = Symbol('default-apollo-client') export const ApolloClients = Symbol('apollo-clients') -export function useApolloClient (clientId: string = null) { +export interface UseApolloClientReturn { + resolveClient: (clientId?: string) => ApolloClient + readonly client: ApolloClient +} + +export function useApolloClient (clientId?: string): UseApolloClientReturn { const providedApolloClients: { [key: string]: ApolloClient } = inject(ApolloClients, null) const providedApolloClient: ApolloClient = inject(DefaultApolloClient, null) diff --git a/packages/vue-apollo-composable/src/useMutation.ts b/packages/vue-apollo-composable/src/useMutation.ts index 1a109b1..e82033c 100644 --- a/packages/vue-apollo-composable/src/useMutation.ts +++ b/packages/vue-apollo-composable/src/useMutation.ts @@ -7,6 +7,9 @@ import { ReactiveFunction } from './util/ReactiveFunction' import { useEventHook } from './util/useEventHook' import { trackMutation } from './util/loadingTracking' +/** + * `useMutation` options for mutations that don't require `variables`. + */ export interface UseMutationOptions< TResult = any, TVariables = OperationVariables @@ -14,13 +17,67 @@ export interface UseMutationOptions< clientId?: string } -export function useMutation< +/** + * `useMutation` options for mutations that don't use variables. + */ +export type UseMutationOptionsNoVariables< TResult = any, TVariables = OperationVariables +> = Omit, 'variables'> + +/** + * `useMutation` options for mutations require variables. + */ +export interface UseMutationOptionsWithVariables< + TResult = any, + TVariables = OperationVariables +> extends UseMutationOptions { + variables: TVariables +} + +export interface UseMutationReturn { + mutate: (variables?: TVariables, overrideOptions?: Pick, 'update' | 'optimisticResponse' | 'context' | 'updateQueries' | 'refetchQueries' | 'awaitRefetchQueries' | 'errorPolicy' | 'fetchPolicy' | 'clientId'>) => Promise, Record>> + loading: Ref + error: Ref + called: Ref + onDone: (fn: (param?: FetchResult, Record>) => void) => { + off: () => void + }; + onError: (fn: (param?: Error) => void) => { + off: () => void + }; +}; + +/** + * Use a mutation that does not require variables or options. + * */ +export function useMutation( + document: DocumentNode | ReactiveFunction +): UseMutationReturn + +/** + * Use a mutation that does not require variables. + */ +export function useMutation( + document: DocumentNode | ReactiveFunction, + options: UseMutationOptionsNoVariables | ReactiveFunction> +): UseMutationReturn + +/** + * Use a mutation that requires variables. + */ +export function useMutation( + document: DocumentNode | ReactiveFunction, + options: UseMutationOptionsWithVariables | ReactiveFunction> +): UseMutationReturn + +export function useMutation< + TResult, + TVariables extends OperationVariables > ( document: DocumentNode | Ref | ReactiveFunction, - options: UseMutationOptions | Ref> | ReactiveFunction> = null, -) { + options?: UseMutationOptions | Ref> | ReactiveFunction>, +): UseMutationReturn { if (!options) options = {} const loading = ref(false) @@ -34,7 +91,7 @@ export function useMutation< // Apollo Client const { resolveClient } = useApolloClient() - async function mutate (variables: TVariables = null, overrideOptions: Omit = {}) { + async function mutate (variables?: TVariables, overrideOptions: Omit = {}) { let currentDocument: DocumentNode if (typeof document === 'function') { currentDocument = document() diff --git a/packages/vue-apollo-composable/src/useQuery.ts b/packages/vue-apollo-composable/src/useQuery.ts index cdcde69..c2e1178 100644 --- a/packages/vue-apollo-composable/src/useQuery.ts +++ b/packages/vue-apollo-composable/src/useQuery.ts @@ -35,15 +35,70 @@ interface SubscribeToMoreItem { unsubscribeFns: Function[] } +export interface UseQueryReturn { + result: Ref + loading: Ref + networkStatus: Ref + error: Ref + start: () => void + stop: () => void + restart: () => void + document: Ref + variables: Ref + options: UseQueryOptions | Ref> + query: Ref> + refetch: (variables?: TVariables) => Promise> + fetchMore: (options: FetchMoreQueryOptions & FetchMoreOptions) => Promise> + subscribeToMore: (options: SubscribeToMoreOptions | Ref> | ReactiveFunction>) => void + onResult: (fn: (param?: ApolloQueryResult) => void) => { + off: () => void + } + onError: (fn: (param?: Error) => void) => { + off: () => void + } +} + +/** + * Use a query that does not require variables or options. + * */ +export function useQuery( + document: DocumentNode | Ref | ReactiveFunction +): UseQueryReturn + +/** + * Use a query that requires options but not variables. + */ +export function useQuery( + document: DocumentNode | Ref | ReactiveFunction, + variables: TVariables, + options: UseQueryOptions | Ref> | ReactiveFunction> +): UseQueryReturn + +/** + * Use a query that requires variables. + */ +export function useQuery( + document: DocumentNode | Ref | ReactiveFunction, + variables: TVariables | Ref | ReactiveFunction +): UseQueryReturn + +/** + * Use a query that requires variables and options. + */ +export function useQuery( + document: DocumentNode | Ref | ReactiveFunction, + variables: TVariables | Ref | ReactiveFunction, + options: UseQueryOptions | Ref> | ReactiveFunction> +): UseQueryReturn + export function useQuery< - TResult = any, - TVariables = OperationVariables, - TCacheShape = any + TResult, + TVariables extends OperationVariables > ( document: DocumentNode | Ref | ReactiveFunction, - variables: TVariables | Ref | ReactiveFunction = null, - options: UseQueryOptions | Ref> | ReactiveFunction> = {}, -) { + variables?: TVariables | Ref | ReactiveFunction, + options?: UseQueryOptions | Ref> | ReactiveFunction>, +): UseQueryReturn { // Is on server? const vm = getCurrentInstance() const isServer = vm.$isServer diff --git a/packages/vue-apollo-composable/src/useResult.ts b/packages/vue-apollo-composable/src/useResult.ts index 158faac..506a680 100644 --- a/packages/vue-apollo-composable/src/useResult.ts +++ b/packages/vue-apollo-composable/src/useResult.ts @@ -1,15 +1,79 @@ import { Ref, computed } from '@vue/composition-api' +import { ExtractSingleKey } from './util/ExtractSingleKey' + +export type UseResultReturn = Readonly>> + + /** + * Resolve a `result`, returning either the first key of the `result` if there + * is only one, or the `result` itself. The `value` of the ref will be + * `undefined` until it is resolved. + * + * @example + * const { result } = useQuery(...) + * const user = useResult(result) + * // user is `void` until the query resolves + * + * @param {Ref} result A `result` returned from `useQuery` to resolve. + * @returns Readonly ref with `void` or the resolved `result`. + */ +export function useResult( + result: Ref +): UseResultReturn> + +/** + * Resolve a `result`, returning either the first key of the `result` if there + * is only one, or the `result` itself. The `value` of the ref will be + * `defaultValue` until it is resolved. + * + * @example + * const { result } = useQuery(...) + * const profile = useResult(result, {}) + * // profile is `{}` until the query resolves + * + * @param {Ref} result A `result` returned from `useQuery` to resolve. + * @param {TDefaultValue} defaultValue The default return value before `result` is resolved. + * @returns Readonly ref with the `defaultValue` or the resolved `result`. + */ +export function useResult( + result: Ref, + defaultValue: TDefaultValue +): UseResultReturn> + +/** + * Resolve a `result`, returning the `result` mapped with the `pick` function. + * The `value` of the ref will be `defaultValue` until it is resolved. + * + * @example + * const { result } = useQuery(...) + * const comments = useResult(result, undefined, (data) => data.comments) + * // user is `undefined`, then resolves to the result's `comments` + * + * @param {Ref} result A `result` returned from `useQuery` to resolve. + * @param {TDefaultValue} defaultValue The default return value before `result` is resolved. + * @param {(data:TResult)=>TReturnValue} pick The function that receives `result` and maps a return value from it. + * @returns Readonly ref with the `defaultValue` or the resolved and `pick`-mapped `result` + */ +export function useResult< + TResult, + TDefaultValue, + TReturnValue, + TResultKey extends keyof TResult = keyof TResult, +>( + result: Ref, + defaultValue: TDefaultValue | undefined, + pick: (data: TResult) => TReturnValue +): UseResultReturn export function useResult< - TReturnValue = any, - TDefaultValue = any, - TResult = any + TResult, + TDefaultValue, + TReturnValue, > ( result: Ref, - defaultValue: TDefaultValue = null, - pick: (data: TResult) => TReturnValue = null, -) { - return computed(() => { + defaultValue?: TDefaultValue, + pick?: (data: TResult) => TReturnValue, +): UseResultReturn { + return computed(() => { const value = result.value if (value) { if (pick) { @@ -22,7 +86,7 @@ export function useResult< const keys = Object.keys(value) if (keys.length === 1) { // Automatically take the only key in result data - return value[keys[0]] + return value[keys[0] as keyof TResult] } else { // Return entire result data return value diff --git a/packages/vue-apollo-composable/src/useSubscription.ts b/packages/vue-apollo-composable/src/useSubscription.ts index 3e3a9f1..14c9072 100644 --- a/packages/vue-apollo-composable/src/useSubscription.ts +++ b/packages/vue-apollo-composable/src/useSubscription.ts @@ -22,14 +22,67 @@ export interface UseSubscriptionOptions < debounce?: number } +export interface UseSubscriptionReturn { + result: Ref + loading: Ref + error: Ref + start: () => void + stop: () => void + restart: () => void + document: Ref + variables: Ref + options: UseSubscriptionOptions | Ref> + subscription: Ref, Record>>> + onResult: (fn: (param?: FetchResult, Record>) => void) => { + off: () => void + } + onError: (fn: (param?: Error) => void) => { + off: () => void + } +} + + +/** + * Use a subscription that does not require variables or options. + * */ +export function useSubscription( + document: DocumentNode | Ref | ReactiveFunction +): UseSubscriptionReturn + +/** + * Use a subscription that requires options but not variables. + */ +export function useSubscription( + document: DocumentNode | Ref | ReactiveFunction, + variables: TVariables, + options: UseSubscriptionOptions | Ref> | ReactiveFunction> +): UseSubscriptionReturn + +/** + * Use a subscription that requires variables. + */ +export function useSubscription( + document: DocumentNode | Ref | ReactiveFunction, + variables: TVariables | Ref | ReactiveFunction +): UseSubscriptionReturn + +/** + * Use a subscription that requires variables and options. + */ +export function useSubscription( + document: DocumentNode | Ref | ReactiveFunction, + variables: TVariables | Ref | ReactiveFunction, + options: UseSubscriptionOptions | Ref> | ReactiveFunction> +): UseSubscriptionReturn + export function useSubscription < - TResult = any, - TVariables = OperationVariables + TResult, + TVariables > ( document: DocumentNode | Ref | ReactiveFunction, variables: TVariables | Ref | ReactiveFunction = null, options: UseSubscriptionOptions | Ref> | ReactiveFunction> = null -) { +): UseSubscriptionReturn { // Is on server? const vm = getCurrentInstance() const isServer = vm.$isServer diff --git a/packages/vue-apollo-composable/src/util/ExtractSingleKey.ts b/packages/vue-apollo-composable/src/util/ExtractSingleKey.ts new file mode 100644 index 0000000..2f20fb8 --- /dev/null +++ b/packages/vue-apollo-composable/src/util/ExtractSingleKey.ts @@ -0,0 +1,9 @@ +/** + * Check if a type is a union, and return true if so, otherwise false. + */ +export type IsUnion = U extends any ? ([T] extends [U] ? false : true) : never + +/** + * Extracts an inner type if T has a single key K, otherwise it returns T. + */ +export type ExtractSingleKey = IsUnion extends true ? T : T[K] diff --git a/packages/vue-apollo-composable/tests/fixtures/graphql-example-types.ts b/packages/vue-apollo-composable/tests/fixtures/graphql-example-types.ts new file mode 100644 index 0000000..2f67cd6 --- /dev/null +++ b/packages/vue-apollo-composable/tests/fixtures/graphql-example-types.ts @@ -0,0 +1,85 @@ +import gql from "graphql-tag"; + +export type ID = string; + +export type Example = { + id: ID; + name?: string; + colors?: string[]; +}; + +export const ExampleFragmentDoc = gql` + fragment ExampleFragment on Example { + name + colors + } +`; + +export type ExampleFragment = { + name?: string; + colors?: string[]; +}; + +export const ExampleDocument = gql` + query getExample($id: ID!) { + example(id: $id) { + id + ...ExampleFragment + } + } + + ${ExampleFragmentDoc} +`; + +export type ExampleQuery = { + example?: { + __typename?: "Example"; + id?: string; + } & ExampleFragment; +}; + +export type ExampleQueryVariables = { + id: ID; +}; + +export type ExampleUpdatePayload = { + errors?: string[]; + example?: Example; +}; + +export type ExampleUpdateMutation = { + exampleUpdate?: ExampleUpdatePayload; +}; + +export type ExampleInput = { + name?: string; + colors?: string[]; +}; + +export type ExampleUpdateMutationVariables = { + id: ID; + example: ExampleInput; +}; + +export type ExampleUpdatedSubscription = { + exampleUpdated: ExampleFragment; +}; + +export type ExampleUpdatedSubscriptionVariables = { + id: ID; +}; + +export type SingleKeyExampleQuery = { + example?: { + __typename?: "Example"; + }; +}; + +export type MultiKeyExampleQuery = { + example?: { + __typename?: "Example"; + }; + otherExample?: { + __typename?: "OtherExample"; + }; +}; diff --git a/packages/vue-apollo-composable/tests/types/assertions.ts b/packages/vue-apollo-composable/tests/types/assertions.ts new file mode 100644 index 0000000..86eee25 --- /dev/null +++ b/packages/vue-apollo-composable/tests/types/assertions.ts @@ -0,0 +1,9 @@ +export type ExactType = T extends U ? (U extends T ? T : never) : never; + +/** + * Verify that a type matches an exact expected type. + * + * NOTE: Some cases don't work (like `any`, `unknown`) due to how typescript + * widens types. Manually verify the assert is reliable when using. + */ +export function assertExactType(expected: ExactType) {} diff --git a/packages/vue-apollo-composable/tests/types/tsconfig.json b/packages/vue-apollo-composable/tests/types/tsconfig.json new file mode 100644 index 0000000..ca1a3b2 --- /dev/null +++ b/packages/vue-apollo-composable/tests/types/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "noEmit": true, + // "strict": true // TODO: this should be enabled, but src is broken with strict + }, + "include": [ + "*.test.ts" + ] +} diff --git a/packages/vue-apollo-composable/tests/types/useApolloClient-types.test.ts b/packages/vue-apollo-composable/tests/types/useApolloClient-types.test.ts new file mode 100644 index 0000000..e69d53d --- /dev/null +++ b/packages/vue-apollo-composable/tests/types/useApolloClient-types.test.ts @@ -0,0 +1,32 @@ +import { useApolloClient, UseApolloClientReturn } from "../../src"; +import { assertExactType } from "./assertions"; + +// ============================================================================= +// With no type and no clientId +// - the store type should be `any` +// ============================================================================= +{ + const noClientId = useApolloClient(); + noClientId.client.extract(true).storeType.is.any; +} + +// ============================================================================= +// With no type and a clientId +// - the store type should be `any` +// ============================================================================= +{ + const withClientId = useApolloClient("88K2tP"); + withClientId.client.extract(true).storeType.is.any; +} + +// ============================================================================= +// With specific type and a client id +// - the store type should be the specified tyep +// ============================================================================= +{ + const withType = useApolloClient<"cacheShape">("38pX2d"); + const store = withType.client.extract(true); + + assertExactType>(withType); + assertExactType(store); +} diff --git a/packages/vue-apollo-composable/tests/types/useLoading-types.test.ts b/packages/vue-apollo-composable/tests/types/useLoading-types.test.ts new file mode 100644 index 0000000..3707ffa --- /dev/null +++ b/packages/vue-apollo-composable/tests/types/useLoading-types.test.ts @@ -0,0 +1,50 @@ +import { Ref } from "@vue/composition-api"; +import { + useGlobalMutationLoading, + useGlobalQueryLoading, + useGlobalSubscriptionLoading, + useMutationLoading, + useQueryLoading, + useSubscriptionLoading +} from "../../src/useLoading"; +import { assertExactType } from "./assertions"; + +{ + const useQueryLoadingReturn = useQueryLoading(); + assertExactType>>(useQueryLoadingReturn); +} + +{ + const useMutationLoadingReturn = useMutationLoading(); + assertExactType>>( + useMutationLoadingReturn + ); +} + +{ + const useSubscriptionLoadingReturn = useSubscriptionLoading(); + assertExactType>>( + useSubscriptionLoadingReturn + ); +} + +{ + const useGlobalQueryLoadingReturn = useGlobalQueryLoading(); + assertExactType>>( + useGlobalQueryLoadingReturn + ); +} + +{ + const useGlobalMutationLoadingReturn = useGlobalMutationLoading(); + assertExactType>>( + useGlobalMutationLoadingReturn + ); +} + +{ + const useGlobalSubscriptionLoadingReturn = useGlobalSubscriptionLoading(); + assertExactType>>( + useGlobalSubscriptionLoadingReturn + ); +} diff --git a/packages/vue-apollo-composable/tests/types/useMutation-types.test.ts b/packages/vue-apollo-composable/tests/types/useMutation-types.test.ts new file mode 100644 index 0000000..e014736 --- /dev/null +++ b/packages/vue-apollo-composable/tests/types/useMutation-types.test.ts @@ -0,0 +1,147 @@ +import { FetchResult } from "apollo-link"; +import { useMutation } from "../../src"; +import { + ExampleDocument, + ExampleUpdateMutation, + ExampleUpdateMutationVariables, + ExampleUpdatePayload +} from "../fixtures/graphql-example-types"; +import { assertExactType } from "./assertions"; + +// ============================================================================= +// With no types: +// - TResult should be `any` +// - TVariables should be `undefined` +// ============================================================================= +{ + const useMutationNoTypes = useMutation(ExampleDocument); + + useMutationNoTypes.onDone(param => { + assertExactType | undefined>(param); + param?.data.dataType.is.anything; + }); + + useMutationNoTypes.mutate(undefined, {}); +} + +// ============================================================================= +// With just the mutation: +// - TResult should be the mutation type +// - TVariables should be `undefined` +// ============================================================================= +{ + const useMutationOnlyMutationType = useMutation(ExampleDocument); + + useMutationOnlyMutationType.onDone(param => { + assertExactType | undefined>(param); + assertExactType( + param.data.exampleUpdate + ); + }); + + useMutationOnlyMutationType.mutate(undefined, {}); +} + +// ============================================================================= +// With just the mutation and with options: +// - TResult should be the mutation type +// - TVariables should be `any` +// ============================================================================= +{ + const useMutationOnlyMutationTypeWithOptions = useMutation( + ExampleDocument, + { + fetchPolicy: "cache-first" + } + ); + + useMutationOnlyMutationTypeWithOptions.onDone(param => { + assertExactType | undefined>(param); + assertExactType( + param.data.exampleUpdate + ); + }); + + useMutationOnlyMutationTypeWithOptions.mutate(undefined, {}); +} + +// ============================================================================= +// With all things typed +// - TResult should be the mutation type +// - TVariables should be the variables type +// ============================================================================= +{ + const useMutationAllTyped = useMutation( + ExampleDocument, + { variables: { id: "1", example: { name: "new" } } } + ); + + useMutationAllTyped.mutate({ id: "2", example: { name: "remix" } }, {}); + + useMutationAllTyped.onDone(param => { + assertExactType | undefined>(param); + assertExactType( + param.data.exampleUpdate + ); + }); +} + +// ============================================================================= +// With all things typed and with options +// - TResult should be the mutation type +// - TVariables should be the variables type +// ============================================================================= +{ + const withOptionsVariables = { id: "1", example: { name: "new" } }; + const withOptions = useMutation( + ExampleDocument, + { + awaitRefetchQueries: true, + clientId: "37Hn7m", + context: "any", + errorPolicy: "all", + fetchPolicy: "cache-first", + optimisticResponse: (vars: ExampleUpdateMutationVariables) => ({ + exampleUpdate: { example: { id: "" } } + }), + refetchQueries: ["firstQuery", "secondQuery"], + update: (proxy, mutationResult: FetchResult) => { + mutationResult.data?.exampleUpdate; + }, + updateQueries: { + query: (result, options) => { + options.mutationResult.data?.exampleUpdate; + return {}; + } + }, + variables: withOptionsVariables + } + ); + + withOptions.onDone(param => { + assertExactType | undefined>(param); + assertExactType( + param.data.exampleUpdate + ); + }); +} + +// ====== Expected failures, uncomment to test ====== + +// // @ts-expect-error +// // With everything typed: +// // - TResult should be the mutation type +// // - TVariables should be *required* +// const expectedFailureNoRequiredVars +// = useMutation(ExampleDocument) + +// // @ts-expect-error +// // With everything typed: +// // - TResult should be the mutation type +// // - TVariables should be *strongly typed* +// const expectedFailureWrongVars +// = useMutation(ExampleDocument, { +// variables: { +// invalidKey: 'wrong' +// } +// }) diff --git a/packages/vue-apollo-composable/tests/types/useQuery-types.test.ts b/packages/vue-apollo-composable/tests/types/useQuery-types.test.ts new file mode 100644 index 0000000..f923773 --- /dev/null +++ b/packages/vue-apollo-composable/tests/types/useQuery-types.test.ts @@ -0,0 +1,148 @@ +import { OperationVariables } from "apollo-client"; +import { useQuery } from "../../src"; +import { + ExampleDocument, + ExampleQuery, + ExampleQueryVariables +} from "../fixtures/graphql-example-types"; +import { assertExactType } from "./assertions"; + +// ============================================================================= +// With no types: +// - TResult should be `any` +// - TVariables should be `undefined` +// ============================================================================= +{ + const useQueryNoTypes = useQuery(ExampleDocument); + + const useQueryNoTypesResult = useQueryNoTypes.result.value; + useQueryNoTypesResult.type.is.any; + + const useQueryNoTypesVariables = useQueryNoTypes.variables.value; + assertExactType(useQueryNoTypesVariables); +} + +// ============================================================================= +// With only query type: +// - TResult should be the query type +// - TVariables should be `undefined` +// ============================================================================= +{ + const useQueryOnlyQueryType = useQuery(ExampleDocument); + + const useQueryOnlyQueryTypeResult = useQueryOnlyQueryType.result.value; + assertExactType(useQueryOnlyQueryTypeResult); + + const useQueryOnlyQueryTypeVariables = useQueryOnlyQueryType.variables.value; + assertExactType(useQueryOnlyQueryTypeVariables); +} + +// ============================================================================= +// With only query type but passing in variables: +// - TResult should be the query type +// - TVariables should be OperationVariables +// ============================================================================= +{ + const useQueryWithVars = useQuery(ExampleDocument, { id: "asdf" }); + + const useQueryWithVarsResult = useQueryWithVars.result.value; + assertExactType(useQueryWithVarsResult); + + const useQueryWithVarsVariables = useQueryWithVars.variables.value; + assertExactType(useQueryWithVarsVariables); +} + +// ============================================================================= +// With all types +// - TResult should be the query type +// - TVariables should be the variables type +// ============================================================================= +{ + const useQueryAllTyped = useQuery(ExampleDocument, { + id: "k3x47b" + }); + + const useQueryAllTypedResult = useQueryAllTyped.result.value; + assertExactType(useQueryAllTypedResult); + + const useQueryAllTypedVariables = useQueryAllTyped.variables.value; + assertExactType( + useQueryAllTypedVariables + ); +} + +// ============================================================================= +// With query types, and no variables +// - TResult should be the query type +// - TVariables should be `undefined` +// ============================================================================= +{ + const useQueryOnlyQueryTypeNoVarsWithOptions = useQuery( + ExampleDocument, + undefined, + { + clientId: "89E3Yh" + } + ); + + const useQueryOnlyQueryTypeNoVarsWithOptionsResult = + useQueryOnlyQueryTypeNoVarsWithOptions.result.value; + assertExactType( + useQueryOnlyQueryTypeNoVarsWithOptionsResult + ); + + const useQueryOnlyQueryTypeNoVarsWithOptionsVariables = + useQueryOnlyQueryTypeNoVarsWithOptions.variables.value; + assertExactType( + useQueryOnlyQueryTypeNoVarsWithOptionsVariables + ); +} + +// ============================================================================= +// With query types, variables, and options +// - TResult should be the query type +// - TVariables should be the variables type +// ============================================================================= +{ + const useQueryWithOptions = useQuery( + ExampleDocument, + { id: "4E79Lq" }, + { + clientId: "any", + context: "any", + debounce: 500, + enabled: true, + errorPolicy: "all", + fetchPolicy: "cache-and-network", + fetchResults: true, + metadata: "any", + notifyOnNetworkStatusChange: true, + pollInterval: 500, + prefetch: true, + returnPartialData: true, + throttle: 1000 + } + ); + + const useQueryWithOptionsResult = useQueryWithOptions.result.value; + assertExactType(useQueryWithOptionsResult); + + const useQueryWithOptionsVariables = useQueryWithOptions.variables.value; + assertExactType( + useQueryWithOptionsVariables + ); +} + +// ====== Expected failures, uncomment to test ====== + +// // @ts-expect-error - should require variables to be OperationType +// const useQueryNoTypesBadVariables = useQuery(ExampleDocument, 'failme') + +// // @ts-expect-error - should require variables to be OperationType +// const useQueryBadQueryVariables = useQuery(ExampleDocument, 'failme') + +// // @ts-expect-error - should require variables to be OperationType +// const useQueryBadVariables = useQuery(ExampleDocument, 'failme') + +// // @ts-expect-error - this should expect two arguments +// const useQueryAllTypedMissingVariables = useQuery(ExampleDocument) diff --git a/packages/vue-apollo-composable/tests/types/useResult-types.test.ts b/packages/vue-apollo-composable/tests/types/useResult-types.test.ts new file mode 100644 index 0000000..4cfeecb --- /dev/null +++ b/packages/vue-apollo-composable/tests/types/useResult-types.test.ts @@ -0,0 +1,146 @@ +import { useQuery, useResult, UseResultReturn } from "../../src"; +import { + ExampleDocument, + ExampleQueryVariables, + MultiKeyExampleQuery, + SingleKeyExampleQuery +} from "../fixtures/graphql-example-types"; +import { assertExactType } from "./assertions"; + +const singleKeyQuery = useQuery(ExampleDocument, { + id: "j37rV7" +}); +const { result: singleKeyResult } = singleKeyQuery; + +const multiKeyQuery = useQuery(ExampleDocument, { + id: "j37rV7" +}); +const { result: multiKeyResult } = multiKeyQuery; + +// ============================================================================= +// With just a document, no types, and a single key +// - the result should extract the single key type +// - the default return value should be `void` +// ============================================================================= +{ + const useResult_JustDocument_SingleKey = useResult(singleKeyResult); + + assertExactType< + typeof useResult_JustDocument_SingleKey, + UseResultReturn + >(useResult_JustDocument_SingleKey); + + if (useResult_JustDocument_SingleKey.value) { + useResult_JustDocument_SingleKey.value.__typename; + } +} + +// ============================================================================= +// With just a document, no types, and multiple keys +// - the result should be the full result type +// - the default return value should be `void` +// ============================================================================= +{ + const useResult_JustDocument_MultiKey = useResult(multiKeyResult); + + assertExactType< + typeof useResult_JustDocument_MultiKey, + UseResultReturn + >(useResult_JustDocument_MultiKey); + + if (useResult_JustDocument_MultiKey.value) { + useResult_JustDocument_MultiKey.value.example?.__typename; + useResult_JustDocument_MultiKey.value.otherExample?.__typename; + } +} + +// ============================================================================= +// With just a document, no types, and a single key +// - the result should extract the single key type +// - the result should be either the default value or the expected extracted single key type +// ============================================================================= +{ + const useResult_WithDefaultValue_SingleKey = useResult(singleKeyResult, "secret" as const); + + assertExactType< + typeof useResult_WithDefaultValue_SingleKey, + UseResultReturn + >(useResult_WithDefaultValue_SingleKey); + + if (typeof useResult_WithDefaultValue_SingleKey.value === "string") { + const result = useResult_WithDefaultValue_SingleKey.value; + assertExactType(result); + useResult_WithDefaultValue_SingleKey.value; + } else { + useResult_WithDefaultValue_SingleKey.value?.__typename; + } +} + +// ============================================================================= +// With just a document, no types, and multiple keys +// - the result should be the full result type +// - the result should be either the default value or full expected key type +// ============================================================================= +{ + const useResult_WithDefaultValue_MultiKey = useResult(multiKeyResult, "secret" as const); + + assertExactType< + typeof useResult_WithDefaultValue_MultiKey, + UseResultReturn + >(useResult_WithDefaultValue_MultiKey); + + if (typeof useResult_WithDefaultValue_MultiKey.value === "string") { + const result = useResult_WithDefaultValue_MultiKey.value; + assertExactType(result); + useResult_WithDefaultValue_MultiKey.value; + } else { + useResult_WithDefaultValue_MultiKey.value.example?.__typename; + useResult_WithDefaultValue_MultiKey.value.otherExample?.__typename; + } +} + +// ============================================================================= +// With a document, default value, no types, and a pick function +// - the result should be either the default value or the pick function result +// ============================================================================= +{ + const useResult_WithPickFunction = useResult( + multiKeyResult, + [] as const, + data => data.otherExample?.__typename + ); + + assertExactType< + typeof useResult_WithPickFunction, + UseResultReturn<"OtherExample" | [] | undefined> + >(useResult_WithPickFunction); + + if (typeof useResult_WithPickFunction.value === "string") { + useResult_WithPickFunction.value.toLowerCase(); + } else if (useResult_WithPickFunction.value) { + useResult_WithPickFunction.value.some(() => {}); + } +} + +// ============================================================================= +// With a document, undefined default value, no types, and a pick function +// - the result should be either undefined or the pick function result +// ============================================================================= +// TODO: This test cannot work without strict: true in tsconfig, but many things +// are currently broken in strict. +// { +// const useResult_WithPickFunction_UndefinedDefault = useResult( +// multiKeyResult, +// undefined, +// data => data.otherExample?.__typename +// ); + +// assertExactType< +// typeof useResult_WithPickFunction_UndefinedDefault, +// UseResultReturn<"OtherExample" | undefined> +// >(useResult_WithPickFunction_UndefinedDefault); + +// if (typeof useResult_WithPickFunction_UndefinedDefault.value === "string") { +// useResult_WithPickFunction_UndefinedDefault.value.toLowerCase(); +// } +// } diff --git a/packages/vue-apollo-composable/tests/types/useSubscription-types.test.ts b/packages/vue-apollo-composable/tests/types/useSubscription-types.test.ts new file mode 100644 index 0000000..a29a909 --- /dev/null +++ b/packages/vue-apollo-composable/tests/types/useSubscription-types.test.ts @@ -0,0 +1,194 @@ +import { OperationVariables } from "apollo-client"; +import { useSubscription } from "../../src"; +import { + ExampleDocument, + ExampleUpdatedSubscription, + ExampleUpdatedSubscriptionVariables +} from "../fixtures/graphql-example-types"; +import { assertExactType } from "./assertions"; + +// ============================================================================= +// With no types: +// - TResult should be `any` +// - TVariables should be `undefined` +// ============================================================================= +{ + const useSubscription_NoTypes = useSubscription(ExampleDocument); + + // Result type should match the passed in subscription type + const useSubscription_NoTypesResult = useSubscription_NoTypes.result.value; + useSubscription_NoTypesResult.type.is.any; + + // Variables type should be `undefined` + const useSubscription_NoTypesVariables = useSubscription_NoTypes.variables.value; + assertExactType( + useSubscription_NoTypesVariables + ); + + // Result data type should be any + useSubscription_NoTypes.onResult(result => result?.data.type.is.any); +} + +// ============================================================================= +// With only subscription type: +// - TResult should be the subscription type +// - TVariables should be `undefined` +// ============================================================================= +{ + const useSubscription_OnlySubscriptionType = useSubscription( + ExampleDocument + ); + + // Result type should match the passed in subscription type + const useSubscription_OnlySubscriptionTypeResult = + useSubscription_OnlySubscriptionType.result.value; + assertExactType( + useSubscription_OnlySubscriptionTypeResult + ); + + // Variables type should be `undefined` + const useSubscription_OnlySubscriptionTypeVariables = + useSubscription_OnlySubscriptionType.variables.value; + assertExactType( + useSubscription_OnlySubscriptionTypeVariables + ); + + // Result data type should be the passed in result + useSubscription_OnlySubscriptionType.onResult(result => result?.data?.exampleUpdated.name); +} + +// ============================================================================= +// With only Subscription type but passing in variables: +// - TResult should be the Subscription type +// - TVariables should be OperationVariables +// ============================================================================= +{ + const useSubscription_WithVars = useSubscription(ExampleDocument, { + id: "asdf" + }); + + // Result type should match the passed in subscription type + const useSubscription_WithVarsResult = useSubscription_WithVars.result.value; + assertExactType( + useSubscription_WithVarsResult + ); + + // Variables type should match the passed in variables type + const useSubscription_WithVarsVariables = useSubscription_WithVars.variables.value; + assertExactType( + useSubscription_WithVarsVariables + ); + + // Result data type should be the passed in result + useSubscription_WithVars.onResult(result => result?.data?.exampleUpdated.name); +} + +// ============================================================================= +// With all types +// - TResult should be the subscription type +// - TVariables should be the variables type +// ============================================================================= +{ + const useSubscription_AllTyped = useSubscription< + ExampleUpdatedSubscription, + ExampleUpdatedSubscriptionVariables + >(ExampleDocument, { id: "k3x47b" }); + + // Result type should match the passed in subscription type + const useSubscription_AllTypedResult = useSubscription_AllTyped.result.value; + assertExactType( + useSubscription_AllTypedResult + ); + + // Variables type should match the passed in variables type + const useSubscription_AllTypedVariables = useSubscription_AllTyped.variables.value; + assertExactType( + useSubscription_AllTypedVariables + ); + + // Result data type should be the passed in result + useSubscription_AllTyped.onResult(result => result?.data?.exampleUpdated.name); +} + +// ============================================================================= +// With subscription types, and no variables +// - TResult should be the subscription type +// - TVariables should be `undefined` +// ============================================================================= +{ + const useSubscription_OnlySubscriptionType_NoVarsWithOptions = useSubscription< + ExampleUpdatedSubscription + >(ExampleDocument, undefined, { + clientId: "89E3Yh" + }); + + // Result type should match the passed in subscription type + const useSubscription_OnlySubscriptionType_NoVarsWithOptionsResult = + useSubscription_OnlySubscriptionType_NoVarsWithOptions.result.value; + assertExactType< + typeof useSubscription_OnlySubscriptionType_NoVarsWithOptionsResult, + ExampleUpdatedSubscription + >(useSubscription_OnlySubscriptionType_NoVarsWithOptionsResult); + + // Variables type should be `undefined` + const useSubscription_OnlySubscriptionType_NoVarsWithOptionsVariables = + useSubscription_OnlySubscriptionType_NoVarsWithOptions.variables.value; + assertExactType< + typeof useSubscription_OnlySubscriptionType_NoVarsWithOptionsVariables, + undefined + >(useSubscription_OnlySubscriptionType_NoVarsWithOptionsVariables); + + // Result data type should be the passed in result + useSubscription_OnlySubscriptionType_NoVarsWithOptions.onResult( + result => result?.data?.exampleUpdated.name + ); +} + +// ============================================================================= +// With subscription types, variables, and options +// - TResult should be the subscription type +// - TVariables should be the variables type +// ============================================================================= +{ + const useSubscription_WithOptions = useSubscription< + ExampleUpdatedSubscription, + ExampleUpdatedSubscriptionVariables + >( + ExampleDocument, + { id: "4E79Lq" }, + { + clientId: "8nf38r", + debounce: 1500, + enabled: true, + fetchPolicy: "cache-first", + throttle: 1500 + } + ); + + const useSubscription_WithOptionsResult = useSubscription_WithOptions.result.value; + assertExactType( + useSubscription_WithOptionsResult + ); + + const useSubscription_WithOptionsVariables = useSubscription_WithOptions.variables.value; + assertExactType( + useSubscription_WithOptionsVariables + ); + + // Result data type should be the passed in result + useSubscription_WithOptions.onResult(result => result?.data?.exampleUpdated.name); +} + +// // ====== Expected failures, uncomment to test ====== + +// // @ts-expect-error - should require variables to be OperationType +// const useSubscription_NoTypesBadVariables = useSubscription(ExampleDocument, 'failme') + +// // @ts-expect-error - should require variables to be OperationType +// const useSubscriptionBadSubscriptionVariables = useSubscription(ExampleDocument, 'failme') + +// // @ts-expect-error - should require variables to be OperationType +// const useSubscriptionBadVariables = useSubscription(ExampleDocument, 'failme') + +// // @ts-expect-error - this should expect two arguments +// const useSubscription_AllTypedMissingVariables = useSubscription(ExampleDocument) diff --git a/packages/vue-apollo-composable/tests/types/util/ExtractSingleKey.test.ts b/packages/vue-apollo-composable/tests/types/util/ExtractSingleKey.test.ts new file mode 100644 index 0000000..7e56ada --- /dev/null +++ b/packages/vue-apollo-composable/tests/types/util/ExtractSingleKey.test.ts @@ -0,0 +1,28 @@ +import { ExtractSingleKey, IsUnion } from "../../../src/util/ExtractSingleKey"; +import { MultiKeyExampleQuery, SingleKeyExampleQuery } from "../../fixtures/graphql-example-types"; +import { assertExactType } from "../assertions"; + +// IsUnion + +// When the type is a union, it should return true +const trueUnion: IsUnion<"id" | "name"> = true; +const numberTrueUnion: IsUnion<15 | 18> = true; + +// When the type is not a union, it should return false +const falseUnion: IsUnion<"id"> = false; +const numberFalseUnion: IsUnion<15> = false; +const arrayUnion: IsUnion<[string, number]> = false; + +// When the type is never, it should return never +let what: IsUnion; +assertExactType(what); + +// ExtractSingleKey + +// When the passed in type has a single key, it should return the type of that key +let singleKeyQuery: ExtractSingleKey; +assertExactType(singleKeyQuery); + +// When the passed in type has multiple keys, it should return the type +let multiKeyQuery: ExtractSingleKey; +assertExactType(multiKeyQuery);