-
-
Notifications
You must be signed in to change notification settings - Fork 132
Description
Description
When using @zenstackhq/tanstack-query with the new slicing ORM API introduced in v3.4, there are two related TypeScript type issues:
Issue 1: Model accessors require non-null assertion
Model accessors returned from useClientQueries() are typed as potentially undefined, requiring non-null assertions (!) to use them even though they are always defined at runtime.
Issue 2: Procedure types cannot be narrowed
TypeScript cannot properly narrow the type when accessing procedures via $procs. The procedure type is a union of { useQuery... } and { useMutation... } based on whether the procedure has mutation: true, but TypeScript cannot discriminate between them at the access site.
Environment
- ZenStack version: 3.4.4 (and related packages)
- @zenstackhq/tanstack-query version: 3.4.4
- TypeScript version: 5.x
- Framework: Next.js
- Package Manager: npm
Steps to Reproduce
Issue 1: Model accessors require non-null assertion
- Use
useClientQueriesfrom@zenstackhq/tanstack-query/react:
import { useClientQueries } from '@zenstackhq/tanstack-query/react';
import { schema } from '@repo/zenstack-schema/lite';
export function useZenstack() {
return useClientQueries(schema);
}- Try to access a model hook:
const zenstack = useZenstack();
// TypeScript Error: 'zenstack.client' is possibly 'undefined'
const { data } = zenstack.client.useFindMany({ where: { ... } });
// Workaround: requires non-null assertion
const { data } = zenstack.client!.useFindMany({ where: { ... } });Issue 2: Procedure type narrowing
- Define procedures in your ZModel schema, some with
mutationand some without:
// Query procedure (no mutation flag)
procedure getLatestCarloChat(dealershipId: String): CarloChat
// Mutation procedure
mutation procedure createCarloChat(dealershipId: String): CarloChat- Use
useClientQueriesfrom@zenstackhq/tanstack-query/react:
import { useClientQueries } from '@zenstackhq/tanstack-query/react';
import { schema } from '@repo/zenstack-schema/lite';
export function useZenstack() {
return useClientQueries(schema);
}- Try to access a procedure:
const zenstack = useZenstack();
// TypeScript Error: Property 'useQuery' does not exist on type '...'
const { data } = zenstack.$procs.getLatestCarloChat.useQuery({
args: { dealershipId: '...' }
});Expected Behavior
Issue 1: Model accessors
Model accessors like zenstack.client, zenstack.vehicle, etc. should be typed as always defined (non-optional) since they are always present at runtime.
Issue 2: Procedure narrowing
TypeScript should be able to infer that getLatestCarloChat (without mutation: true) has useQuery and useSuspenseQuery methods, while createCarloChat (with mutation: true) has useMutation.
Actual Behavior
Issue 1: Model accessors
Model accessors are typed as T | undefined, requiring non-null assertions:
// Error without '!'
zenstack.client.useFindMany(...)
// Works with '!'
zenstack.client!.useFindMany(...)Issue 2: Procedure narrowing
The type of zenstack.$procs.getLatestCarloChat is a union:
{ useQuery: ...; useSuspenseQuery: ... } | { useMutation: ... }TypeScript cannot narrow this union because the discrimination happens at the mapped type level based on whether the procedure definition has mutation: true, but this information is lost when accessing the property dynamically.
Current Workaround
Issue 1: Model accessors
Use non-null assertion operator when accessing model hooks:
// Instead of: zenstack.client.useFindMany(...)
zenstack.client!.useFindMany(...)
zenstack.vehicle!.useCreate(...)Issue 2: Procedures
We've had to use type assertions with helper types:
// Helper types
export type QueryProcedureHook<TReturn> = {
useQuery: (
input: { args: Record<string, unknown> },
options?: { enabled?: boolean; gcTime?: number },
) => UseQueryResult<TReturn>;
useSuspenseQuery: (
input: { args: Record<string, unknown> },
options?: Record<string, unknown>,
) => UseQueryResult<TReturn>;
};
export type MutationProcedureHook<TReturn, TArgs = Record<string, unknown>> = {
useMutation: (options?: {
onSuccess?: (data: TReturn) => void;
onError?: (error: Error) => void;
}) => UseMutationResult<TReturn, Error, { args: TArgs }>;
};
// Usage
const proc = zenstack.$procs!.getLatestCarloChat as unknown as QueryProcedureHook<CarloChat>;
const { data } = proc.useQuery({ args: { dealershipId } });Suggested Solutions
Issue 1: Model accessors
- Make model accessors non-optional: The generated type should not include
| undefinedfor model accessors since they are always present at runtime when the schema includes the model.
Issue 2: Procedure narrowing
-
Generate discriminated procedure types: Instead of a single mapped type for all procedures, generate separate typed accessors for query vs mutation procedures.
-
Use branded types or type predicates: Add runtime-checkable discriminators that TypeScript can use for narrowing.
-
Split $procs into $queries and $mutations: This would allow TypeScript to infer the correct type without needing discrimination.
-
Generate individual procedure hooks: Instead of a single
$procsobject, generate named exports for each procedure with the correct type.
Related
- ZenStack Slicing ORM API: https://zenstack.dev/docs/orm/advanced/slicing
- This affects any project using TanStack Query hooks with custom procedures
Additional Context
These issues affect a significant portion of our codebase. The workarounds work but:
- Issue 1: Requires adding
!assertions throughout the codebase which is error-prone and verbose - Issue 2: Type assertions lose type safety for procedure arguments and return types