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
This commit is contained in:
Brian Bugh
2020-01-10 17:16:51 -06:00
committed by Guillaume Chau
parent 9d64fca7bc
commit 32d1f7568e
17 changed files with 1118 additions and 23 deletions
+3 -1
View File
@@ -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"
@@ -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)
@@ -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<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)
@@ -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()
+61 -6
View File
@@ -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
@@ -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) {
@@ -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
@@ -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
@@ -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]
@@ -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";
};
};
@@ -0,0 +1,9 @@
export type ExactType<T, U> = 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<TActual, TExpected>(expected: ExactType<TActual, TExpected>) {}
@@ -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"
]
}
@@ -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<typeof withType, UseApolloClientReturn<"cacheShape">>(withType);
assertExactType<typeof store, "cacheShape">(store);
}
@@ -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<typeof useQueryLoadingReturn, Readonly<Ref<boolean>>>(useQueryLoadingReturn);
}
{
const useMutationLoadingReturn = useMutationLoading();
assertExactType<typeof useMutationLoadingReturn, Readonly<Ref<boolean>>>(
useMutationLoadingReturn
);
}
{
const useSubscriptionLoadingReturn = useSubscriptionLoading();
assertExactType<typeof useSubscriptionLoadingReturn, Readonly<Ref<boolean>>>(
useSubscriptionLoadingReturn
);
}
{
const useGlobalQueryLoadingReturn = useGlobalQueryLoading();
assertExactType<typeof useGlobalQueryLoadingReturn, Readonly<Ref<boolean>>>(
useGlobalQueryLoadingReturn
);
}
{
const useGlobalMutationLoadingReturn = useGlobalMutationLoading();
assertExactType<typeof useGlobalMutationLoadingReturn, Readonly<Ref<boolean>>>(
useGlobalMutationLoadingReturn
);
}
{
const useGlobalSubscriptionLoadingReturn = useGlobalSubscriptionLoading();
assertExactType<typeof useGlobalSubscriptionLoadingReturn, Readonly<Ref<boolean>>>(
useGlobalSubscriptionLoadingReturn
);
}
@@ -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<typeof param, FetchResult<any> | 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<ExampleUpdateMutation>(ExampleDocument);
useMutationOnlyMutationType.onDone(param => {
assertExactType<typeof param, FetchResult<ExampleUpdateMutation> | undefined>(param);
assertExactType<typeof param.data.exampleUpdate, ExampleUpdatePayload>(
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<ExampleUpdateMutation>(
ExampleDocument,
{
fetchPolicy: "cache-first"
}
);
useMutationOnlyMutationTypeWithOptions.onDone(param => {
assertExactType<typeof param, FetchResult<ExampleUpdateMutation> | undefined>(param);
assertExactType<typeof param.data.exampleUpdate, ExampleUpdatePayload>(
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<ExampleUpdateMutation, ExampleUpdateMutationVariables>(
ExampleDocument,
{ variables: { id: "1", example: { name: "new" } } }
);
useMutationAllTyped.mutate({ id: "2", example: { name: "remix" } }, {});
useMutationAllTyped.onDone(param => {
assertExactType<typeof param, FetchResult<ExampleUpdateMutation> | undefined>(param);
assertExactType<typeof param.data.exampleUpdate, ExampleUpdatePayload>(
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<ExampleUpdateMutation, ExampleUpdateMutationVariables>(
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<ExampleUpdateMutation>) => {
mutationResult.data?.exampleUpdate;
},
updateQueries: {
query: (result, options) => {
options.mutationResult.data?.exampleUpdate;
return {};
}
},
variables: withOptionsVariables
}
);
withOptions.onDone(param => {
assertExactType<typeof param, FetchResult<ExampleUpdateMutation> | undefined>(param);
assertExactType<typeof param.data.exampleUpdate, ExampleUpdatePayload>(
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<ExampleUpdateMutation, ExampleUpdateMutationVariables>(ExampleDocument)
// // @ts-expect-error
// // With everything typed:
// // - TResult should be the mutation type
// // - TVariables should be *strongly typed*
// const expectedFailureWrongVars
// = useMutation<ExampleUpdateMutation, ExampleUpdateMutationVariables>(ExampleDocument, {
// variables: {
// invalidKey: 'wrong'
// }
// })
@@ -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<typeof useQueryNoTypesVariables, undefined>(useQueryNoTypesVariables);
}
// =============================================================================
// With only query type:
// - TResult should be the query type
// - TVariables should be `undefined`
// =============================================================================
{
const useQueryOnlyQueryType = useQuery<ExampleQuery>(ExampleDocument);
const useQueryOnlyQueryTypeResult = useQueryOnlyQueryType.result.value;
assertExactType<typeof useQueryOnlyQueryTypeResult, ExampleQuery>(useQueryOnlyQueryTypeResult);
const useQueryOnlyQueryTypeVariables = useQueryOnlyQueryType.variables.value;
assertExactType<typeof useQueryOnlyQueryTypeVariables, undefined>(useQueryOnlyQueryTypeVariables);
}
// =============================================================================
// With only query type but passing in variables:
// - TResult should be the query type
// - TVariables should be OperationVariables
// =============================================================================
{
const useQueryWithVars = useQuery<ExampleQuery>(ExampleDocument, { id: "asdf" });
const useQueryWithVarsResult = useQueryWithVars.result.value;
assertExactType<typeof useQueryWithVarsResult, ExampleQuery>(useQueryWithVarsResult);
const useQueryWithVarsVariables = useQueryWithVars.variables.value;
assertExactType<typeof useQueryWithVarsVariables, OperationVariables>(useQueryWithVarsVariables);
}
// =============================================================================
// With all types
// - TResult should be the query type
// - TVariables should be the variables type
// =============================================================================
{
const useQueryAllTyped = useQuery<ExampleQuery, ExampleQueryVariables>(ExampleDocument, {
id: "k3x47b"
});
const useQueryAllTypedResult = useQueryAllTyped.result.value;
assertExactType<typeof useQueryAllTypedResult, ExampleQuery>(useQueryAllTypedResult);
const useQueryAllTypedVariables = useQueryAllTyped.variables.value;
assertExactType<typeof useQueryAllTypedVariables, ExampleQueryVariables>(
useQueryAllTypedVariables
);
}
// =============================================================================
// With query types, and no variables
// - TResult should be the query type
// - TVariables should be `undefined`
// =============================================================================
{
const useQueryOnlyQueryTypeNoVarsWithOptions = useQuery<ExampleQuery>(
ExampleDocument,
undefined,
{
clientId: "89E3Yh"
}
);
const useQueryOnlyQueryTypeNoVarsWithOptionsResult =
useQueryOnlyQueryTypeNoVarsWithOptions.result.value;
assertExactType<typeof useQueryOnlyQueryTypeNoVarsWithOptionsResult, ExampleQuery>(
useQueryOnlyQueryTypeNoVarsWithOptionsResult
);
const useQueryOnlyQueryTypeNoVarsWithOptionsVariables =
useQueryOnlyQueryTypeNoVarsWithOptions.variables.value;
assertExactType<typeof useQueryOnlyQueryTypeNoVarsWithOptionsVariables, undefined>(
useQueryOnlyQueryTypeNoVarsWithOptionsVariables
);
}
// =============================================================================
// With query types, variables, and options
// - TResult should be the query type
// - TVariables should be the variables type
// =============================================================================
{
const useQueryWithOptions = useQuery<ExampleQuery, ExampleQueryVariables>(
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<typeof useQueryWithOptionsResult, ExampleQuery>(useQueryWithOptionsResult);
const useQueryWithOptionsVariables = useQueryWithOptions.variables.value;
assertExactType<typeof useQueryWithOptionsVariables, ExampleQueryVariables>(
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<ExampleQuery>(ExampleDocument, 'failme')
// // @ts-expect-error - should require variables to be OperationType
// const useQueryBadVariables = useQuery<ExampleQuery, ExampleQueryVariables>(ExampleDocument, 'failme')
// // @ts-expect-error - this should expect two arguments
// const useQueryAllTypedMissingVariables = useQuery<ExampleQuery, ExampleQueryVariables>(ExampleDocument)
@@ -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<SingleKeyExampleQuery, ExampleQueryVariables>(ExampleDocument, {
id: "j37rV7"
});
const { result: singleKeyResult } = singleKeyQuery;
const multiKeyQuery = useQuery<MultiKeyExampleQuery, ExampleQueryVariables>(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<SingleKeyExampleQuery["example"] | void>
>(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<MultiKeyExampleQuery | void>
>(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<SingleKeyExampleQuery["example"] | "secret">
>(useResult_WithDefaultValue_SingleKey);
if (typeof useResult_WithDefaultValue_SingleKey.value === "string") {
const result = useResult_WithDefaultValue_SingleKey.value;
assertExactType<typeof result, "secret">(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<MultiKeyExampleQuery | "secret">
>(useResult_WithDefaultValue_MultiKey);
if (typeof useResult_WithDefaultValue_MultiKey.value === "string") {
const result = useResult_WithDefaultValue_MultiKey.value;
assertExactType<typeof result, "secret">(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();
// }
// }
@@ -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<typeof useSubscription_NoTypesVariables, undefined>(
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<ExampleUpdatedSubscription>(
ExampleDocument
);
// Result type should match the passed in subscription type
const useSubscription_OnlySubscriptionTypeResult =
useSubscription_OnlySubscriptionType.result.value;
assertExactType<typeof useSubscription_OnlySubscriptionTypeResult, ExampleUpdatedSubscription>(
useSubscription_OnlySubscriptionTypeResult
);
// Variables type should be `undefined`
const useSubscription_OnlySubscriptionTypeVariables =
useSubscription_OnlySubscriptionType.variables.value;
assertExactType<typeof useSubscription_OnlySubscriptionTypeVariables, undefined>(
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<ExampleUpdatedSubscription>(ExampleDocument, {
id: "asdf"
});
// Result type should match the passed in subscription type
const useSubscription_WithVarsResult = useSubscription_WithVars.result.value;
assertExactType<typeof useSubscription_WithVarsResult, ExampleUpdatedSubscription>(
useSubscription_WithVarsResult
);
// Variables type should match the passed in variables type
const useSubscription_WithVarsVariables = useSubscription_WithVars.variables.value;
assertExactType<typeof useSubscription_WithVarsVariables, OperationVariables>(
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<typeof useSubscription_AllTypedResult, ExampleUpdatedSubscription>(
useSubscription_AllTypedResult
);
// Variables type should match the passed in variables type
const useSubscription_AllTypedVariables = useSubscription_AllTyped.variables.value;
assertExactType<typeof useSubscription_AllTypedVariables, ExampleUpdatedSubscriptionVariables>(
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<typeof useSubscription_WithOptionsResult, ExampleUpdatedSubscription>(
useSubscription_WithOptionsResult
);
const useSubscription_WithOptionsVariables = useSubscription_WithOptions.variables.value;
assertExactType<typeof useSubscription_WithOptionsVariables, ExampleUpdatedSubscriptionVariables>(
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<ExampleUpdatedSubscription>(ExampleDocument, 'failme')
// // @ts-expect-error - should require variables to be OperationType
// const useSubscriptionBadVariables = useSubscription<ExampleUpdatedSubscription, ExampleUpdatedSubscriptionVariables>(ExampleDocument, 'failme')
// // @ts-expect-error - this should expect two arguments
// const useSubscription_AllTypedMissingVariables = useSubscription<ExampleUpdatedSubscription, ExampleUpdatedSubscriptionVariables>(ExampleDocument)
@@ -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<never>;
assertExactType<typeof what, never>(what);
// ExtractSingleKey
// When the passed in type has a single key, it should return the type of that key
let singleKeyQuery: ExtractSingleKey<SingleKeyExampleQuery>;
assertExactType<typeof singleKeyQuery, SingleKeyExampleQuery["example"]>(singleKeyQuery);
// When the passed in type has multiple keys, it should return the type
let multiKeyQuery: ExtractSingleKey<MultiKeyExampleQuery>;
assertExactType<typeof multiKeyQuery, MultiKeyExampleQuery>(multiKeyQuery);