Can you please help typing this without breaking the rules of hooks?
How can I type this custom React hook that internally calls useQuery with two different query option functions, so that TypeScript infers the correct return type based on a "type" parameter?
When I move the useQuery call from the if statement, I get a TS error.
The above solution satisfies TS, but it feels odd and it theoretically(type is stable between renders) breaks the rules of hook. It would be very nice, if somebody can help here.
Thank you!
// useDocuments.ts
import { invoiceDocumentsOptions } from "./invoice-documents/invoiceDocumentsOptions";
import { baseDocumentsOptions } from "./base-documents/baseDocumentsOptions";
import type { UseQueryResult } from "@tanstack/react-query";
import type {
InvoiceDocumentsTable_DocumentFragment,
DocumentsTable_DocumentFragment,
} from "@/gql/graphql";
type DocumentQueryParams =
| {
type: "invoiceDocuments";
documentTypeId: string;
}
| {
type: "baseDocuments";
documentTypeId: string;
};
type InvoiceDocumentsResult = {
totalPages: number;
documents: [InvoiceDocumentsTable_DocumentFragment];
};
type BaseDocumentsResult = {
totalPages: number;
documents: [DocumentsTable_DocumentFragment];
};
export function useDocuments({
type,
documentTypeId,
}: DocumentQueryParams): UseQueryResult<
InvoiceDocumentsResult | BaseDocumentsResult,
Error
> {
const variables = { documentTypeId }
if (type === "baseDocuments") {
const queryOptions = baseDocumentsOptions(variables);
return useQuery(queryOptions) as UseQueryResult<BaseDocumentsResult, Error>;
}
if (type === "invoiceDocuments") {
const queryOptions = invoiceDocumentsOptions(variables);
return useQuery(queryOptions) as UseQueryResult<
InvoiceDocumentsResult,
Error
>;
}
const queryOptions = baseDocumentsOptions(variables);
return useQuery(queryOptions) as UseQueryResult<BaseDocumentsResult, Error>;
}
// useDocuments.ts
import { invoiceDocumentsOptions } from "./invoice-documents/invoiceDocumentsOptions";
import { baseDocumentsOptions } from "./base-documents/baseDocumentsOptions";
import type { UseQueryResult } from "@tanstack/react-query";
import type {
InvoiceDocumentsTable_DocumentFragment,
DocumentsTable_DocumentFragment,
} from "@/gql/graphql";
type DocumentQueryParams =
| {
type: "invoiceDocuments";
documentTypeId: string;
}
| {
type: "baseDocuments";
documentTypeId: string;
};
type InvoiceDocumentsResult = {
totalPages: number;
documents: [InvoiceDocumentsTable_DocumentFragment];
};
type BaseDocumentsResult = {
totalPages: number;
documents: [DocumentsTable_DocumentFragment];
};
export function useDocuments({
type,
documentTypeId,
}: DocumentQueryParams): UseQueryResult<
InvoiceDocumentsResult | BaseDocumentsResult,
Error
> {
const variables = { documentTypeId }
if (type === "baseDocuments") {
const queryOptions = baseDocumentsOptions(variables);
return useQuery(queryOptions) as UseQueryResult<BaseDocumentsResult, Error>;
}
if (type === "invoiceDocuments") {
const queryOptions = invoiceDocumentsOptions(variables);
return useQuery(queryOptions) as UseQueryResult<
InvoiceDocumentsResult,
Error
>;
}
const queryOptions = baseDocumentsOptions(variables);
return useQuery(queryOptions) as UseQueryResult<BaseDocumentsResult, Error>;
}
6 Replies
plain-purpleOP•2mo ago
I have two working solutions.
First
Here's solution with minimal typing. Eslint warns, but the type is stable for a component. So we ignore it.
Second
Here's a types one, but with casts (trade-off)
Maybe somebody with deeper TS knowledge can post his opinion. That would be great!
Thanks
import { useVariables } from "./useDocumentsState";
import { invoiceDocumentsOptions } from "./invoice-documents/invoiceDocumentsOptions";
import { baseDocumentsOptions } from "./base-documents/baseDocumentsOptions";
import { useQuery } from "@tanstack/react-query";
export function useDocuments({
type,
documentTypeId,
documentStateId,
itemsPerPage = 100,
}: {
type: "invoiceDocuments" | "baseDocuments";
documentTypeId: string;
documentStateId: string;
itemsPerPage?: number;
}) {
const variables = useVariables({
documentTypeId,
documentStateId,
itemsPerPage,
});
if (type === "invoiceDocuments") {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useQuery(invoiceDocumentsOptions(itemsPerPage, variables));
} else if (type === "baseDocuments") {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useQuery(baseDocumentsOptions(itemsPerPage, variables));
}
throw new Error(`Unknown document type: ${type}`);
}
import { useVariables } from "./useDocumentsState";
import { invoiceDocumentsOptions } from "./invoice-documents/invoiceDocumentsOptions";
import { baseDocumentsOptions } from "./base-documents/baseDocumentsOptions";
import { useQuery } from "@tanstack/react-query";
export function useDocuments({
type,
documentTypeId,
documentStateId,
itemsPerPage = 100,
}: {
type: "invoiceDocuments" | "baseDocuments";
documentTypeId: string;
documentStateId: string;
itemsPerPage?: number;
}) {
const variables = useVariables({
documentTypeId,
documentStateId,
itemsPerPage,
});
if (type === "invoiceDocuments") {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useQuery(invoiceDocumentsOptions(itemsPerPage, variables));
} else if (type === "baseDocuments") {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useQuery(baseDocumentsOptions(itemsPerPage, variables));
}
throw new Error(`Unknown document type: ${type}`);
}
import { useVariables } from "./useDocumentsState";
import { invoiceDocumentsOptions } from "./invoice-documents/invoiceDocumentsOptions";
import { baseDocumentsOptions } from "./base-documents/baseDocumentsOptions";
import { useQuery } from "@tanstack/react-query";
import type { UseQueryResult, UseQueryOptions } from "@tanstack/react-query";
import type {
InvoiceDocumentsTable_DocumentFragment,
DocumentsTable_DocumentFragment,
} from "@/gql/graphql";
type InvoiceDocumentsResult = {
totalPages: number;
documents: InvoiceDocumentsTable_DocumentFragment[];
};
type BaseDocumentsResult = {
totalPages: number;
documents: DocumentsTable_DocumentFragment[];
};
type DocumentTypeMap = {
invoiceDocuments: InvoiceDocumentsResult;
baseDocuments: BaseDocumentsResult;
};
type DocumentParams<T extends keyof DocumentTypeMap> = {
type: T;
documentTypeId: string;
documentStateId: string;
itemsPerPage?: number;
};
const queryOptionsMap = {
invoiceDocuments: invoiceDocumentsOptions,
baseDocuments: baseDocumentsOptions,
};
export function useDocuments<T extends keyof DocumentTypeMap>({
type,
documentTypeId,
documentStateId,
itemsPerPage = 100,
}: DocumentParams<T>): UseQueryResult<DocumentTypeMap[T], Error> {
const variables = useVariables({
documentTypeId,
documentStateId,
itemsPerPage,
});
const queryOptions = queryOptionsMap[type](itemsPerPage, variables);
return useQuery(
queryOptions as UseQueryOptions<DocumentTypeMap[T], Error>
) as UseQueryResult<DocumentTypeMap[T], Error>;
}
import { useVariables } from "./useDocumentsState";
import { invoiceDocumentsOptions } from "./invoice-documents/invoiceDocumentsOptions";
import { baseDocumentsOptions } from "./base-documents/baseDocumentsOptions";
import { useQuery } from "@tanstack/react-query";
import type { UseQueryResult, UseQueryOptions } from "@tanstack/react-query";
import type {
InvoiceDocumentsTable_DocumentFragment,
DocumentsTable_DocumentFragment,
} from "@/gql/graphql";
type InvoiceDocumentsResult = {
totalPages: number;
documents: InvoiceDocumentsTable_DocumentFragment[];
};
type BaseDocumentsResult = {
totalPages: number;
documents: DocumentsTable_DocumentFragment[];
};
type DocumentTypeMap = {
invoiceDocuments: InvoiceDocumentsResult;
baseDocuments: BaseDocumentsResult;
};
type DocumentParams<T extends keyof DocumentTypeMap> = {
type: T;
documentTypeId: string;
documentStateId: string;
itemsPerPage?: number;
};
const queryOptionsMap = {
invoiceDocuments: invoiceDocumentsOptions,
baseDocuments: baseDocumentsOptions,
};
export function useDocuments<T extends keyof DocumentTypeMap>({
type,
documentTypeId,
documentStateId,
itemsPerPage = 100,
}: DocumentParams<T>): UseQueryResult<DocumentTypeMap[T], Error> {
const variables = useVariables({
documentTypeId,
documentStateId,
itemsPerPage,
});
const queryOptions = queryOptionsMap[type](itemsPerPage, variables);
return useQuery(
queryOptions as UseQueryOptions<DocumentTypeMap[T], Error>
) as UseQueryResult<DocumentTypeMap[T], Error>;
}
rising-crimson•2mo ago
Have you tried overloading?
type InvoiceDocumentParams = {
type: "invoiceDocuments";
documentTypeId: string;
}
type BaseDocumentParams = {
type: "baseDocuments";
documentTypeId: string;
}
type InvoiceDocumentsResult = {
totalPages: number;
documents: [InvoiceDocumentsTable_DocumentFragment];
};
type BaseDocumentsResult = {
totalPages: number;
documents: [DocumentsTable_DocumentFragment];
};
export function useDocuments(props: InvoiceDocumentParams): UseQueryResult<
InvoiceDocumentsResult
Error
>;
export function useDocuments(props: BaseDocumentParams): UseQueryResult<
BaseDocumentsResult
Error
>;
export function useDocuments({ type, documentTypeId }: InvoiceDocumentParams | BaseDocumentParams): UseQueryResult<InvoiceDocumentsResult, Error> | UseQueryResult<BaseDocumentsResult, Error> {
const variables = useVariables({
documentTypeId,
documentStateId,
itemsPerPage,
});
return useQuery(type === "invoiceDocuments" ? invoiceDocumentsOptions(itemsPerPage, variables) : baseDocumentsOptions(itemsPerPage, variables))
}
type InvoiceDocumentParams = {
type: "invoiceDocuments";
documentTypeId: string;
}
type BaseDocumentParams = {
type: "baseDocuments";
documentTypeId: string;
}
type InvoiceDocumentsResult = {
totalPages: number;
documents: [InvoiceDocumentsTable_DocumentFragment];
};
type BaseDocumentsResult = {
totalPages: number;
documents: [DocumentsTable_DocumentFragment];
};
export function useDocuments(props: InvoiceDocumentParams): UseQueryResult<
InvoiceDocumentsResult
Error
>;
export function useDocuments(props: BaseDocumentParams): UseQueryResult<
BaseDocumentsResult
Error
>;
export function useDocuments({ type, documentTypeId }: InvoiceDocumentParams | BaseDocumentParams): UseQueryResult<InvoiceDocumentsResult, Error> | UseQueryResult<BaseDocumentsResult, Error> {
const variables = useVariables({
documentTypeId,
documentStateId,
itemsPerPage,
});
return useQuery(type === "invoiceDocuments" ? invoiceDocumentsOptions(itemsPerPage, variables) : baseDocumentsOptions(itemsPerPage, variables))
}
optimistic-gold•2mo ago
overloading should work
plain-purpleOP•2mo ago
Thank you for your help! I tried it, but still have typing issues.
export type InvoiceDocumentsResult = {
type: "invoiceDocuments";
totalPages: number;
documents: FragmentType<typeof InvoiceDocumentsTable_DocumentFragmentDoc>[];
};
export type BaseDocumentsResult = {
type: "baseDocuments";
totalPages: number;
documents: FragmentType<typeof BaseDocumentsTable_DocumentFragmentDoc>[];
};
type DocumentTypeMap = {
invoiceDocuments: InvoiceDocumentsResult;
baseDocuments: BaseDocumentsResult;
};
export type DocumentTypeUnion = DocumentTypeMap[keyof DocumentTypeMap];
type InvoiceDocumentsProps = {
type: "invoiceDocuments";
itemsPerPage?: number;
};
type BaseDocumentsProps = {
type: "baseDocuments";
itemsPerPage?: number;
};
export function useDocuments(
props: InvoiceDocumentsProps
): UseQueryResult<InvoiceDocumentsResult, Error>;
export function useDocuments(
props: BaseDocumentsProps
): UseQueryResult<BaseDocumentsResult, Error>;
export function useDocuments({
type,
itemsPerPage = 100,
}: BaseDocumentsProps | InvoiceDocumentsProps):
| UseQueryResult<InvoiceDocumentsResult, Error>
| UseQueryResult<BaseDocumentsResult, Error> {
const variables = useVariables({
itemsPerPage,
});
let queryOptions = null;
switch (type) {
case "baseDocuments": {
queryOptions = baseDocumentsOptions(itemsPerPage, variables);
break;
}
case "invoiceDocuments":
queryOptions = invoiceDocumentsOptions(itemsPerPage, variables);
break;
}
return useQuery(queryOptions);
// here I have the typing issues:
// Types of property '__typename' are incompatible.
// Type '"InvoiceDocument" | undefined' is not assignable to type '"Document" | undefined'.
// Type '"InvoiceDocument"' is not assignable to type '"Document"'.ts(2769)
//Types of property 'type' are incompatible.
// Type 'string' is not assignable to type '"invoiceDocuments"'.ts(2322)
}
export type InvoiceDocumentsResult = {
type: "invoiceDocuments";
totalPages: number;
documents: FragmentType<typeof InvoiceDocumentsTable_DocumentFragmentDoc>[];
};
export type BaseDocumentsResult = {
type: "baseDocuments";
totalPages: number;
documents: FragmentType<typeof BaseDocumentsTable_DocumentFragmentDoc>[];
};
type DocumentTypeMap = {
invoiceDocuments: InvoiceDocumentsResult;
baseDocuments: BaseDocumentsResult;
};
export type DocumentTypeUnion = DocumentTypeMap[keyof DocumentTypeMap];
type InvoiceDocumentsProps = {
type: "invoiceDocuments";
itemsPerPage?: number;
};
type BaseDocumentsProps = {
type: "baseDocuments";
itemsPerPage?: number;
};
export function useDocuments(
props: InvoiceDocumentsProps
): UseQueryResult<InvoiceDocumentsResult, Error>;
export function useDocuments(
props: BaseDocumentsProps
): UseQueryResult<BaseDocumentsResult, Error>;
export function useDocuments({
type,
itemsPerPage = 100,
}: BaseDocumentsProps | InvoiceDocumentsProps):
| UseQueryResult<InvoiceDocumentsResult, Error>
| UseQueryResult<BaseDocumentsResult, Error> {
const variables = useVariables({
itemsPerPage,
});
let queryOptions = null;
switch (type) {
case "baseDocuments": {
queryOptions = baseDocumentsOptions(itemsPerPage, variables);
break;
}
case "invoiceDocuments":
queryOptions = invoiceDocumentsOptions(itemsPerPage, variables);
break;
}
return useQuery(queryOptions);
// here I have the typing issues:
// Types of property '__typename' are incompatible.
// Type '"InvoiceDocument" | undefined' is not assignable to type '"Document" | undefined'.
// Type '"InvoiceDocument"' is not assignable to type '"Document"'.ts(2769)
//Types of property 'type' are incompatible.
// Type 'string' is not assignable to type '"invoiceDocuments"'.ts(2322)
}
rising-crimson•2mo ago
//Types of property 'type' are incompatible. // Type 'string' is not assignable to type '"invoiceDocuments"'.ts(2322)This is possibly an issue on your typing between
InvoiceDocumentsResult and the actual type from invoiceDocumentsOptions
e.g. invoiceDocumentsOptions's queryFn returns {type: string}.
// here I have the typing issues: // Types of property '__typename' are incompatible. // Type '"InvoiceDocument" | undefined' is not assignable to type '"Document" | undefined'. // Type '"InvoiceDocument"' is not assignable to type '"Document"'.ts(2769)All the types related with ("Document", "InvoiceDocument") are not in the code you provide though so I cannot help unless you provide more info.
plain-purpleOP•2mo ago
Ok thanks, here are the baseDocumentOptions and invoiceDocumentOptions fns:
// baseDocumentOptions.ts
import { gqlClient, extractData } from "@/features/query/gqlClient";
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { graphql } from "@/gql";
import type { DocumentsQueryVariables } from "@/gql/graphql";
const BaseDocumentsQuery = graphql(`
query Documents(
$after: String
$first: Int
$filter: DocumentsFilterInput!
$sort: [SortInput!]
) {
documents(after: $after, first: $first, filter: $filter, sort: $sort) {
totalCount
nodes {
...BaseDocumentsTable_Document
}
}
}
`);
export const baseDocumentsOptions = (
itemsPerPage: number,
variables: DocumentsQueryVariables
) => {
return queryOptions({
queryKey: ["baseDocuments", "list", { ...variables, itemsPerPage }],
placeholderData: keepPreviousData,
queryFn: async () => {
const documentsResult = extractData(
await gqlClient.gql(BaseDocumentsQuery).send(variables)
).documents;
return {
type: "baseDocuments",
totalPages: Math.ceil(documentsResult.totalCount / itemsPerPage),
documents: (documentsResult.nodes || []).filter(
(node) => node !== null
),
};
},
});
};
// baseDocumentOptions.ts
import { gqlClient, extractData } from "@/features/query/gqlClient";
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { graphql } from "@/gql";
import type { DocumentsQueryVariables } from "@/gql/graphql";
const BaseDocumentsQuery = graphql(`
query Documents(
$after: String
$first: Int
$filter: DocumentsFilterInput!
$sort: [SortInput!]
) {
documents(after: $after, first: $first, filter: $filter, sort: $sort) {
totalCount
nodes {
...BaseDocumentsTable_Document
}
}
}
`);
export const baseDocumentsOptions = (
itemsPerPage: number,
variables: DocumentsQueryVariables
) => {
return queryOptions({
queryKey: ["baseDocuments", "list", { ...variables, itemsPerPage }],
placeholderData: keepPreviousData,
queryFn: async () => {
const documentsResult = extractData(
await gqlClient.gql(BaseDocumentsQuery).send(variables)
).documents;
return {
type: "baseDocuments",
totalPages: Math.ceil(documentsResult.totalCount / itemsPerPage),
documents: (documentsResult.nodes || []).filter(
(node) => node !== null
),
};
},
});
};
// invoiceDocumentOptions.ts
import { gqlClient, extractData } from "@/features/query/gqlClient";
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { graphql } from "@/gql";
import type { InvoiceDocumentsQueryVariables } from "@/gql/graphql";
const InvoiceDocumentsQuery = graphql(`
query InvoiceDocuments(
$after: String
$first: Int
$filter: InvoiceDocumentsFilterInput!
$sort: [SortInput!]
) {
invoiceDocuments(
after: $after
first: $first
filter: $filter
sort: $sort
) {
totalCount
nodes {
...InvoiceDocumentsTable_Document
}
}
}
`);
export const invoiceDocumentsOptions = (
itemsPerPage: number,
variables: InvoiceDocumentsQueryVariables
) => {
return queryOptions({
queryKey: ["invoiceDocuments", "list", { ...variables, itemsPerPage }],
placeholderData: keepPreviousData,
queryFn: async () => {
const documentsResult = extractData(
await gqlClient.gql(InvoiceDocumentsQuery).send(variables)
).invoiceDocuments;
return {
type: "invoiceDocuments",
totalPages: Math.ceil(documentsResult.totalCount / itemsPerPage),
documents: (documentsResult.nodes || []).filter(
(node) => node !== null
),
};
},
});
};
// invoiceDocumentOptions.ts
import { gqlClient, extractData } from "@/features/query/gqlClient";
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { graphql } from "@/gql";
import type { InvoiceDocumentsQueryVariables } from "@/gql/graphql";
const InvoiceDocumentsQuery = graphql(`
query InvoiceDocuments(
$after: String
$first: Int
$filter: InvoiceDocumentsFilterInput!
$sort: [SortInput!]
) {
invoiceDocuments(
after: $after
first: $first
filter: $filter
sort: $sort
) {
totalCount
nodes {
...InvoiceDocumentsTable_Document
}
}
}
`);
export const invoiceDocumentsOptions = (
itemsPerPage: number,
variables: InvoiceDocumentsQueryVariables
) => {
return queryOptions({
queryKey: ["invoiceDocuments", "list", { ...variables, itemsPerPage }],
placeholderData: keepPreviousData,
queryFn: async () => {
const documentsResult = extractData(
await gqlClient.gql(InvoiceDocumentsQuery).send(variables)
).invoiceDocuments;
return {
type: "invoiceDocuments",
totalPages: Math.ceil(documentsResult.totalCount / itemsPerPage),
documents: (documentsResult.nodes || []).filter(
(node) => node !== null
),
};
},
});
};