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);