Skip to content

Commit 1b9c635

Browse files
committed
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: microsoft/TypeScript#29394 * chore: run type tests on prepublish * chore: code style
1 parent 029a937 commit 1b9c635

17 files changed

+1118
-23
lines changed

packages/vue-apollo-composable/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
"scripts": {
3030
"dev": "yarn build --watch",
3131
"build": "tsc --outDir dist -d",
32-
"prepublishOnly": "yarn build"
32+
"prepublishOnly": "yarn test && yarn build",
33+
"test": "yarn test:types",
34+
"test:types": "tsc -p tests/types/"
3335
},
3436
"dependencies": {
3537
"throttle-debounce": "^2.1.0"

packages/vue-apollo-composable/src/useApolloClient.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import ApolloClient from 'apollo-client'
44
export const DefaultApolloClient = Symbol('default-apollo-client')
55
export const ApolloClients = Symbol('apollo-clients')
66

7-
export function useApolloClient<TCacheShape = any> (clientId: string = null) {
7+
export interface UseApolloClientReturn<TCacheShape> {
8+
resolveClient: (clientId?: string) => ApolloClient<TCacheShape>
9+
readonly client: ApolloClient<TCacheShape>
10+
}
11+
12+
export function useApolloClient<TCacheShape = any> (clientId?: string): UseApolloClientReturn<TCacheShape> {
813
const providedApolloClients: { [key: string]: ApolloClient<TCacheShape> } = inject(ApolloClients, null)
914
const providedApolloClient: ApolloClient<TCacheShape> = inject(DefaultApolloClient, null)
1015

packages/vue-apollo-composable/src/useMutation.ts

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,77 @@ import { ReactiveFunction } from './util/ReactiveFunction'
77
import { useEventHook } from './util/useEventHook'
88
import { trackMutation } from './util/loadingTracking'
99

10+
/**
11+
* `useMutation` options for mutations that don't require `variables`.
12+
*/
1013
export interface UseMutationOptions<
1114
TResult = any,
1215
TVariables = OperationVariables
1316
> extends Omit<MutationOptions<TResult, TVariables>, 'mutation'> {
1417
clientId?: string
1518
}
1619

17-
export function useMutation<
20+
/**
21+
* `useMutation` options for mutations that don't use variables.
22+
*/
23+
export type UseMutationOptionsNoVariables<
24+
TResult = any,
25+
TVariables = OperationVariables
26+
> = Omit<UseMutationOptions<TResult, TVariables>, 'variables'>
27+
28+
/**
29+
* `useMutation` options for mutations require variables.
30+
*/
31+
export interface UseMutationOptionsWithVariables<
1832
TResult = any,
1933
TVariables = OperationVariables
34+
> extends UseMutationOptions<TResult, TVariables> {
35+
variables: TVariables
36+
}
37+
38+
export interface UseMutationReturn<TResult, TVariables> {
39+
mutate: (variables?: TVariables, overrideOptions?: Pick<UseMutationOptions<any, OperationVariables>, 'update' | 'optimisticResponse' | 'context' | 'updateQueries' | 'refetchQueries' | 'awaitRefetchQueries' | 'errorPolicy' | 'fetchPolicy' | 'clientId'>) => Promise<FetchResult<any, Record<string, any>, Record<string, any>>>
40+
loading: Ref<boolean>
41+
error: Ref<Error>
42+
called: Ref<boolean>
43+
onDone: (fn: (param?: FetchResult<TResult, Record<string, any>, Record<string, any>>) => void) => {
44+
off: () => void
45+
};
46+
onError: (fn: (param?: Error) => void) => {
47+
off: () => void
48+
};
49+
};
50+
51+
/**
52+
* Use a mutation that does not require variables or options.
53+
* */
54+
export function useMutation<TResult = any>(
55+
document: DocumentNode | ReactiveFunction<DocumentNode>
56+
): UseMutationReturn<TResult, undefined>
57+
58+
/**
59+
* Use a mutation that does not require variables.
60+
*/
61+
export function useMutation<TResult = any>(
62+
document: DocumentNode | ReactiveFunction<DocumentNode>,
63+
options: UseMutationOptionsNoVariables<TResult, undefined> | ReactiveFunction<UseMutationOptionsNoVariables<TResult, undefined>>
64+
): UseMutationReturn<TResult, undefined>
65+
66+
/**
67+
* Use a mutation that requires variables.
68+
*/
69+
export function useMutation<TResult = any, TVariables extends OperationVariables = OperationVariables>(
70+
document: DocumentNode | ReactiveFunction<DocumentNode>,
71+
options: UseMutationOptionsWithVariables<TResult, TVariables> | ReactiveFunction<UseMutationOptionsWithVariables<TResult, TVariables>>
72+
): UseMutationReturn<TResult, TVariables>
73+
74+
export function useMutation<
75+
TResult,
76+
TVariables extends OperationVariables
2077
> (
2178
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
22-
options: UseMutationOptions<TResult, TVariables> | Ref<UseMutationOptions<TResult, TVariables>> | ReactiveFunction<UseMutationOptions<TResult, TVariables>> = null,
23-
) {
79+
options?: UseMutationOptions<TResult, TVariables> | Ref<UseMutationOptions<TResult, TVariables>> | ReactiveFunction<UseMutationOptions<TResult, TVariables>>,
80+
): UseMutationReturn<TResult, TVariables> {
2481
if (!options) options = {}
2582

2683
const loading = ref<boolean>(false)
@@ -34,7 +91,7 @@ export function useMutation<
3491
// Apollo Client
3592
const { resolveClient } = useApolloClient()
3693

37-
async function mutate (variables: TVariables = null, overrideOptions: Omit<UseMutationOptions, 'variables'> = {}) {
94+
async function mutate (variables?: TVariables, overrideOptions: Omit<UseMutationOptions, 'variables'> = {}) {
3895
let currentDocument: DocumentNode
3996
if (typeof document === 'function') {
4097
currentDocument = document()

packages/vue-apollo-composable/src/useQuery.ts

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,70 @@ interface SubscribeToMoreItem {
3535
unsubscribeFns: Function[]
3636
}
3737

38+
export interface UseQueryReturn<TResult, TVariables> {
39+
result: Ref<TResult>
40+
loading: Ref<boolean>
41+
networkStatus: Ref<number>
42+
error: Ref<Error>
43+
start: () => void
44+
stop: () => void
45+
restart: () => void
46+
document: Ref<DocumentNode>
47+
variables: Ref<TVariables>
48+
options: UseQueryOptions<TResult, TVariables> | Ref<UseQueryOptions<TResult, TVariables>>
49+
query: Ref<ObservableQuery<TResult, TVariables>>
50+
refetch: (variables?: TVariables) => Promise<ApolloQueryResult<TResult>>
51+
fetchMore: <K extends keyof TVariables>(options: FetchMoreQueryOptions<TVariables, K> & FetchMoreOptions<TResult, TVariables>) => Promise<ApolloQueryResult<TResult>>
52+
subscribeToMore: <TSubscriptionVariables = OperationVariables, TSubscriptionData = TResult>(options: SubscribeToMoreOptions<TResult, TSubscriptionVariables, TSubscriptionData> | Ref<SubscribeToMoreOptions<TResult, TSubscriptionVariables, TSubscriptionData>> | ReactiveFunction<SubscribeToMoreOptions<TResult, TSubscriptionVariables, TSubscriptionData>>) => void
53+
onResult: (fn: (param?: ApolloQueryResult<TResult>) => void) => {
54+
off: () => void
55+
}
56+
onError: (fn: (param?: Error) => void) => {
57+
off: () => void
58+
}
59+
}
60+
61+
/**
62+
* Use a query that does not require variables or options.
63+
* */
64+
export function useQuery<TResult = any>(
65+
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>
66+
): UseQueryReturn<TResult, undefined>
67+
68+
/**
69+
* Use a query that requires options but not variables.
70+
*/
71+
export function useQuery<TResult = any, TVariables extends undefined = undefined>(
72+
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
73+
variables: TVariables,
74+
options: UseQueryOptions<TResult, TVariables> | Ref<UseQueryOptions<TResult, TVariables>> | ReactiveFunction<UseQueryOptions<TResult, TVariables>>
75+
): UseQueryReturn<TResult, TVariables>
76+
77+
/**
78+
* Use a query that requires variables.
79+
*/
80+
export function useQuery<TResult = any, TVariables extends OperationVariables = OperationVariables>(
81+
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
82+
variables: TVariables | Ref<TVariables> | ReactiveFunction<TVariables>
83+
): UseQueryReturn<TResult, TVariables>
84+
85+
/**
86+
* Use a query that requires variables and options.
87+
*/
88+
export function useQuery<TResult = any, TVariables extends OperationVariables = OperationVariables>(
89+
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
90+
variables: TVariables | Ref<TVariables> | ReactiveFunction<TVariables>,
91+
options: UseQueryOptions<TResult, TVariables> | Ref<UseQueryOptions<TResult, TVariables>> | ReactiveFunction<UseQueryOptions<TResult, TVariables>>
92+
): UseQueryReturn<TResult, TVariables>
93+
3894
export function useQuery<
39-
TResult = any,
40-
TVariables = OperationVariables,
41-
TCacheShape = any
95+
TResult,
96+
TVariables extends OperationVariables
4297
> (
4398
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
44-
variables: TVariables | Ref<TVariables> | ReactiveFunction<TVariables> = null,
45-
options: UseQueryOptions<TResult, TVariables> | Ref<UseQueryOptions<TResult, TVariables>> | ReactiveFunction<UseQueryOptions<TResult, TVariables>> = {},
46-
) {
99+
variables?: TVariables | Ref<TVariables> | ReactiveFunction<TVariables>,
100+
options?: UseQueryOptions<TResult, TVariables> | Ref<UseQueryOptions<TResult, TVariables>> | ReactiveFunction<UseQueryOptions<TResult, TVariables>>,
101+
): UseQueryReturn<TResult, TVariables> {
47102
// Is on server?
48103
const vm = getCurrentInstance()
49104
const isServer = vm.$isServer

packages/vue-apollo-composable/src/useResult.ts

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,79 @@
11
import { Ref, computed } from '@vue/composition-api'
2+
import { ExtractSingleKey } from './util/ExtractSingleKey'
3+
4+
export type UseResultReturn<T> = Readonly<Ref<Readonly<T>>>
5+
6+
/**
7+
* Resolve a `result`, returning either the first key of the `result` if there
8+
* is only one, or the `result` itself. The `value` of the ref will be
9+
* `undefined` until it is resolved.
10+
*
11+
* @example
12+
* const { result } = useQuery(...)
13+
* const user = useResult(result)
14+
* // user is `void` until the query resolves
15+
*
16+
* @param {Ref<TResult>} result A `result` returned from `useQuery` to resolve.
17+
* @returns Readonly ref with `void` or the resolved `result`.
18+
*/
19+
export function useResult<TResult, TResultKey extends keyof TResult = keyof TResult>(
20+
result: Ref<TResult>
21+
): UseResultReturn<void | ExtractSingleKey<TResult, TResultKey>>
22+
23+
/**
24+
* Resolve a `result`, returning either the first key of the `result` if there
25+
* is only one, or the `result` itself. The `value` of the ref will be
26+
* `defaultValue` until it is resolved.
27+
*
28+
* @example
29+
* const { result } = useQuery(...)
30+
* const profile = useResult(result, {})
31+
* // profile is `{}` until the query resolves
32+
*
33+
* @param {Ref<TResult>} result A `result` returned from `useQuery` to resolve.
34+
* @param {TDefaultValue} defaultValue The default return value before `result` is resolved.
35+
* @returns Readonly ref with the `defaultValue` or the resolved `result`.
36+
*/
37+
export function useResult<TResult, TDefaultValue, TResultKey extends keyof TResult = keyof TResult>(
38+
result: Ref<TResult>,
39+
defaultValue: TDefaultValue
40+
): UseResultReturn<TDefaultValue | ExtractSingleKey<TResult, TResultKey>>
41+
42+
/**
43+
* Resolve a `result`, returning the `result` mapped with the `pick` function.
44+
* The `value` of the ref will be `defaultValue` until it is resolved.
45+
*
46+
* @example
47+
* const { result } = useQuery(...)
48+
* const comments = useResult(result, undefined, (data) => data.comments)
49+
* // user is `undefined`, then resolves to the result's `comments`
50+
*
51+
* @param {Ref<TResult>} result A `result` returned from `useQuery` to resolve.
52+
* @param {TDefaultValue} defaultValue The default return value before `result` is resolved.
53+
* @param {(data:TResult)=>TReturnValue} pick The function that receives `result` and maps a return value from it.
54+
* @returns Readonly ref with the `defaultValue` or the resolved and `pick`-mapped `result`
55+
*/
56+
export function useResult<
57+
TResult,
58+
TDefaultValue,
59+
TReturnValue,
60+
TResultKey extends keyof TResult = keyof TResult,
61+
>(
62+
result: Ref<TResult>,
63+
defaultValue: TDefaultValue | undefined,
64+
pick: (data: TResult) => TReturnValue
65+
): UseResultReturn<TDefaultValue | TReturnValue>
266

367
export function useResult<
4-
TReturnValue = any,
5-
TDefaultValue = any,
6-
TResult = any
68+
TResult,
69+
TDefaultValue,
70+
TReturnValue,
771
> (
872
result: Ref<TResult>,
9-
defaultValue: TDefaultValue = null,
10-
pick: (data: TResult) => TReturnValue = null,
11-
) {
12-
return computed<TDefaultValue | TReturnValue>(() => {
73+
defaultValue?: TDefaultValue,
74+
pick?: (data: TResult) => TReturnValue,
75+
): UseResultReturn<TResult | TResult[keyof TResult] | TDefaultValue | TReturnValue | undefined> {
76+
return computed(() => {
1377
const value = result.value
1478
if (value) {
1579
if (pick) {
@@ -22,7 +86,7 @@ export function useResult<
2286
const keys = Object.keys(value)
2387
if (keys.length === 1) {
2488
// Automatically take the only key in result data
25-
return value[keys[0]]
89+
return value[keys[0] as keyof TResult]
2690
} else {
2791
// Return entire result data
2892
return value

packages/vue-apollo-composable/src/useSubscription.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,67 @@ export interface UseSubscriptionOptions <
2222
debounce?: number
2323
}
2424

25+
export interface UseSubscriptionReturn<TResult, TVariables> {
26+
result: Ref<TResult>
27+
loading: Ref<boolean>
28+
error: Ref<Error>
29+
start: () => void
30+
stop: () => void
31+
restart: () => void
32+
document: Ref<DocumentNode>
33+
variables: Ref<TVariables>
34+
options: UseSubscriptionOptions<TResult, TVariables> | Ref<UseSubscriptionOptions<TResult, TVariables>>
35+
subscription: Ref<Observable<FetchResult<TResult, Record<string, any>, Record<string, any>>>>
36+
onResult: (fn: (param?: FetchResult<TResult, Record<string, any>, Record<string, any>>) => void) => {
37+
off: () => void
38+
}
39+
onError: (fn: (param?: Error) => void) => {
40+
off: () => void
41+
}
42+
}
43+
44+
45+
/**
46+
* Use a subscription that does not require variables or options.
47+
* */
48+
export function useSubscription<TResult = any>(
49+
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>
50+
): UseSubscriptionReturn<TResult, undefined>
51+
52+
/**
53+
* Use a subscription that requires options but not variables.
54+
*/
55+
export function useSubscription<TResult = any, TVariables extends undefined = undefined>(
56+
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
57+
variables: TVariables,
58+
options: UseSubscriptionOptions<TResult, TVariables> | Ref<UseSubscriptionOptions<TResult, TVariables>> | ReactiveFunction<UseSubscriptionOptions<TResult, TVariables>>
59+
): UseSubscriptionReturn<TResult, TVariables>
60+
61+
/**
62+
* Use a subscription that requires variables.
63+
*/
64+
export function useSubscription<TResult = any, TVariables extends OperationVariables = OperationVariables>(
65+
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
66+
variables: TVariables | Ref<TVariables> | ReactiveFunction<TVariables>
67+
): UseSubscriptionReturn<TResult, TVariables>
68+
69+
/**
70+
* Use a subscription that requires variables and options.
71+
*/
72+
export function useSubscription<TResult = any, TVariables extends OperationVariables = OperationVariables>(
73+
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
74+
variables: TVariables | Ref<TVariables> | ReactiveFunction<TVariables>,
75+
options: UseSubscriptionOptions<TResult, TVariables> | Ref<UseSubscriptionOptions<TResult, TVariables>> | ReactiveFunction<UseSubscriptionOptions<TResult, TVariables>>
76+
): UseSubscriptionReturn<TResult, TVariables>
77+
2578
export function useSubscription <
26-
TResult = any,
27-
TVariables = OperationVariables
79+
TResult,
80+
TVariables
2881
> (
2982
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
3083
variables: TVariables | Ref<TVariables> | ReactiveFunction<TVariables> = null,
3184
options: UseSubscriptionOptions<TResult, TVariables> | Ref<UseSubscriptionOptions<TResult, TVariables>> | ReactiveFunction<UseSubscriptionOptions<TResult, TVariables>> = null
32-
) {
85+
): UseSubscriptionReturn<TResult, TVariables> {
3386
// Is on server?
3487
const vm = getCurrentInstance()
3588
const isServer = vm.$isServer
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Check if a type is a union, and return true if so, otherwise false.
3+
*/
4+
export type IsUnion<T, U = T> = U extends any ? ([T] extends [U] ? false : true) : never
5+
6+
/**
7+
* Extracts an inner type if T has a single key K, otherwise it returns T.
8+
*/
9+
export type ExtractSingleKey<T, K extends keyof T = keyof T> = IsUnion<K> extends true ? T : T[K]

0 commit comments

Comments
 (0)