Skip to content

Commit

Permalink
fix(ts): Add full test coverage for TypeScript hook types & fix missi…
Browse files Browse the repository at this point in the history
…ng 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
  • Loading branch information
RayDev1988 committed Jan 10, 2020
1 parent 7cc7aba commit 9b8919e
Show file tree
Hide file tree
Showing 17 changed files with 1,118 additions and 23 deletions.
4 changes: 3 additions & 1 deletion packages/vue-apollo-composable/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 6 additions & 1 deletion packages/vue-apollo-composable/src/useApolloClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TCacheShape = any> (clientId: string = null) {
export interface UseApolloClientReturn<TCacheShape> {
resolveClient: (clientId?: string) => ApolloClient<TCacheShape>
readonly client: ApolloClient<TCacheShape>
}

export function useApolloClient<TCacheShape = any> (clientId?: string): UseApolloClientReturn<TCacheShape> {
const providedApolloClients: { [key: string]: ApolloClient<TCacheShape> } = inject(ApolloClients, null)
const providedApolloClient: ApolloClient<TCacheShape> = inject(DefaultApolloClient, null)

Expand Down
65 changes: 61 additions & 4 deletions packages/vue-apollo-composable/src/useMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,77 @@ 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
> extends Omit<MutationOptions<TResult, TVariables>, 'mutation'> {
clientId?: string
}

export function useMutation<
/**
* `useMutation` options for mutations that don't use variables.
*/
export type UseMutationOptionsNoVariables<
TResult = any,
TVariables = OperationVariables
> = Omit<UseMutationOptions<TResult, TVariables>, 'variables'>

/**
* `useMutation` options for mutations require variables.
*/
export interface UseMutationOptionsWithVariables<
TResult = any,
TVariables = OperationVariables
> extends UseMutationOptions<TResult, TVariables> {
variables: TVariables
}

export interface UseMutationReturn<TResult, TVariables> {
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>>>
loading: Ref<boolean>
error: Ref<Error>
called: Ref<boolean>
onDone: (fn: (param?: FetchResult<TResult, Record<string, any>, Record<string, any>>) => void) => {
off: () => void
};
onError: (fn: (param?: Error) => void) => {
off: () => void
};
};

/**
* Use a mutation that does not require variables or options.
* */
export function useMutation<TResult = any>(
document: DocumentNode | ReactiveFunction<DocumentNode>
): UseMutationReturn<TResult, undefined>

/**
* Use a mutation that does not require variables.
*/
export function useMutation<TResult = any>(
document: DocumentNode | ReactiveFunction<DocumentNode>,
options: UseMutationOptionsNoVariables<TResult, undefined> | ReactiveFunction<UseMutationOptionsNoVariables<TResult, undefined>>
): UseMutationReturn<TResult, undefined>

/**
* Use a mutation that requires variables.
*/
export function useMutation<TResult = any, TVariables extends OperationVariables = OperationVariables>(
document: DocumentNode | ReactiveFunction<DocumentNode>,
options: UseMutationOptionsWithVariables<TResult, TVariables> | ReactiveFunction<UseMutationOptionsWithVariables<TResult, TVariables>>
): UseMutationReturn<TResult, TVariables>

export function useMutation<
TResult,
TVariables extends OperationVariables
> (
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
options: UseMutationOptions<TResult, TVariables> | Ref<UseMutationOptions<TResult, TVariables>> | ReactiveFunction<UseMutationOptions<TResult, TVariables>> = null,
) {
options?: UseMutationOptions<TResult, TVariables> | Ref<UseMutationOptions<TResult, TVariables>> | ReactiveFunction<UseMutationOptions<TResult, TVariables>>,
): UseMutationReturn<TResult, TVariables> {
if (!options) options = {}

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

async function mutate (variables: TVariables = null, overrideOptions: Omit<UseMutationOptions, 'variables'> = {}) {
async function mutate (variables?: TVariables, overrideOptions: Omit<UseMutationOptions, 'variables'> = {}) {
let currentDocument: DocumentNode
if (typeof document === 'function') {
currentDocument = document()
Expand Down
67 changes: 61 additions & 6 deletions packages/vue-apollo-composable/src/useQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,70 @@ interface SubscribeToMoreItem {
unsubscribeFns: Function[]
}

export interface UseQueryReturn<TResult, TVariables> {
result: Ref<TResult>
loading: Ref<boolean>
networkStatus: Ref<number>
error: Ref<Error>
start: () => void
stop: () => void
restart: () => void
document: Ref<DocumentNode>
variables: Ref<TVariables>
options: UseQueryOptions<TResult, TVariables> | Ref<UseQueryOptions<TResult, TVariables>>
query: Ref<ObservableQuery<TResult, TVariables>>
refetch: (variables?: TVariables) => Promise<ApolloQueryResult<TResult>>
fetchMore: <K extends keyof TVariables>(options: FetchMoreQueryOptions<TVariables, K> & FetchMoreOptions<TResult, TVariables>) => Promise<ApolloQueryResult<TResult>>
subscribeToMore: <TSubscriptionVariables = OperationVariables, TSubscriptionData = TResult>(options: SubscribeToMoreOptions<TResult, TSubscriptionVariables, TSubscriptionData> | Ref<SubscribeToMoreOptions<TResult, TSubscriptionVariables, TSubscriptionData>> | ReactiveFunction<SubscribeToMoreOptions<TResult, TSubscriptionVariables, TSubscriptionData>>) => void
onResult: (fn: (param?: ApolloQueryResult<TResult>) => void) => {
off: () => void
}
onError: (fn: (param?: Error) => void) => {
off: () => void
}
}

/**
* Use a query that does not require variables or options.
* */
export function useQuery<TResult = any>(
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>
): UseQueryReturn<TResult, undefined>

/**
* Use a query that requires options but not variables.
*/
export function useQuery<TResult = any, TVariables extends undefined = undefined>(
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
variables: TVariables,
options: UseQueryOptions<TResult, TVariables> | Ref<UseQueryOptions<TResult, TVariables>> | ReactiveFunction<UseQueryOptions<TResult, TVariables>>
): UseQueryReturn<TResult, TVariables>

/**
* Use a query that requires variables.
*/
export function useQuery<TResult = any, TVariables extends OperationVariables = OperationVariables>(
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
variables: TVariables | Ref<TVariables> | ReactiveFunction<TVariables>
): UseQueryReturn<TResult, TVariables>

/**
* Use a query that requires variables and options.
*/
export function useQuery<TResult = any, TVariables extends OperationVariables = OperationVariables>(
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
variables: TVariables | Ref<TVariables> | ReactiveFunction<TVariables>,
options: UseQueryOptions<TResult, TVariables> | Ref<UseQueryOptions<TResult, TVariables>> | ReactiveFunction<UseQueryOptions<TResult, TVariables>>
): UseQueryReturn<TResult, TVariables>

export function useQuery<
TResult = any,
TVariables = OperationVariables,
TCacheShape = any
TResult,
TVariables extends OperationVariables
> (
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
variables: TVariables | Ref<TVariables> | ReactiveFunction<TVariables> = null,
options: UseQueryOptions<TResult, TVariables> | Ref<UseQueryOptions<TResult, TVariables>> | ReactiveFunction<UseQueryOptions<TResult, TVariables>> = {},
) {
variables?: TVariables | Ref<TVariables> | ReactiveFunction<TVariables>,
options?: UseQueryOptions<TResult, TVariables> | Ref<UseQueryOptions<TResult, TVariables>> | ReactiveFunction<UseQueryOptions<TResult, TVariables>>,
): UseQueryReturn<TResult, TVariables> {
// Is on server?
const vm = getCurrentInstance()
const isServer = vm.$isServer
Expand Down
80 changes: 72 additions & 8 deletions packages/vue-apollo-composable/src/useResult.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,79 @@
import { Ref, computed } from '@vue/composition-api'
import { ExtractSingleKey } from './util/ExtractSingleKey'

export type UseResultReturn<T> = Readonly<Ref<Readonly<T>>>

/**
* 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<TResult>} result A `result` returned from `useQuery` to resolve.
* @returns Readonly ref with `void` or the resolved `result`.
*/
export function useResult<TResult, TResultKey extends keyof TResult = keyof TResult>(
result: Ref<TResult>
): UseResultReturn<void | ExtractSingleKey<TResult, TResultKey>>

/**
* 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<TResult>} 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<TResult, TDefaultValue, TResultKey extends keyof TResult = keyof TResult>(
result: Ref<TResult>,
defaultValue: TDefaultValue
): UseResultReturn<TDefaultValue | ExtractSingleKey<TResult, TResultKey>>

/**
* 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<TResult>} 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<TResult>,
defaultValue: TDefaultValue | undefined,
pick: (data: TResult) => TReturnValue
): UseResultReturn<TDefaultValue | TReturnValue>

export function useResult<
TReturnValue = any,
TDefaultValue = any,
TResult = any
TResult,
TDefaultValue,
TReturnValue,
> (
result: Ref<TResult>,
defaultValue: TDefaultValue = null,
pick: (data: TResult) => TReturnValue = null,
) {
return computed<TDefaultValue | TReturnValue>(() => {
defaultValue?: TDefaultValue,
pick?: (data: TResult) => TReturnValue,
): UseResultReturn<TResult | TResult[keyof TResult] | TDefaultValue | TReturnValue | undefined> {
return computed(() => {
const value = result.value
if (value) {
if (pick) {
Expand All @@ -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
Expand Down
59 changes: 56 additions & 3 deletions packages/vue-apollo-composable/src/useSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,67 @@ export interface UseSubscriptionOptions <
debounce?: number
}

export interface UseSubscriptionReturn<TResult, TVariables> {
result: Ref<TResult>
loading: Ref<boolean>
error: Ref<Error>
start: () => void
stop: () => void
restart: () => void
document: Ref<DocumentNode>
variables: Ref<TVariables>
options: UseSubscriptionOptions<TResult, TVariables> | Ref<UseSubscriptionOptions<TResult, TVariables>>
subscription: Ref<Observable<FetchResult<TResult, Record<string, any>, Record<string, any>>>>
onResult: (fn: (param?: FetchResult<TResult, Record<string, any>, Record<string, any>>) => void) => {
off: () => void
}
onError: (fn: (param?: Error) => void) => {
off: () => void
}
}


/**
* Use a subscription that does not require variables or options.
* */
export function useSubscription<TResult = any>(
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>
): UseSubscriptionReturn<TResult, undefined>

/**
* Use a subscription that requires options but not variables.
*/
export function useSubscription<TResult = any, TVariables extends undefined = undefined>(
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
variables: TVariables,
options: UseSubscriptionOptions<TResult, TVariables> | Ref<UseSubscriptionOptions<TResult, TVariables>> | ReactiveFunction<UseSubscriptionOptions<TResult, TVariables>>
): UseSubscriptionReturn<TResult, TVariables>

/**
* Use a subscription that requires variables.
*/
export function useSubscription<TResult = any, TVariables extends OperationVariables = OperationVariables>(
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
variables: TVariables | Ref<TVariables> | ReactiveFunction<TVariables>
): UseSubscriptionReturn<TResult, TVariables>

/**
* Use a subscription that requires variables and options.
*/
export function useSubscription<TResult = any, TVariables extends OperationVariables = OperationVariables>(
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
variables: TVariables | Ref<TVariables> | ReactiveFunction<TVariables>,
options: UseSubscriptionOptions<TResult, TVariables> | Ref<UseSubscriptionOptions<TResult, TVariables>> | ReactiveFunction<UseSubscriptionOptions<TResult, TVariables>>
): UseSubscriptionReturn<TResult, TVariables>

export function useSubscription <
TResult = any,
TVariables = OperationVariables
TResult,
TVariables
> (
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
variables: TVariables | Ref<TVariables> | ReactiveFunction<TVariables> = null,
options: UseSubscriptionOptions<TResult, TVariables> | Ref<UseSubscriptionOptions<TResult, TVariables>> | ReactiveFunction<UseSubscriptionOptions<TResult, TVariables>> = null
) {
): UseSubscriptionReturn<TResult, TVariables> {
// Is on server?
const vm = getCurrentInstance()
const isServer = vm.$isServer
Expand Down
9 changes: 9 additions & 0 deletions packages/vue-apollo-composable/src/util/ExtractSingleKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Check if a type is a union, and return true if so, otherwise false.
*/
export type IsUnion<T, U = T> = 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<T, K extends keyof T = keyof T> = IsUnion<K> extends true ? T : T[K]
Loading

0 comments on commit 9b8919e

Please sign in to comment.