Typed hooks

How a data hook or composable gets its record type, what createCheloneHooks binds, and which hooks the registry never reaches.

The data hooks read the app's registered resources directly, so useGetList("posts") is typed with no factory: the name picks the record, and a name the app never registered is rejected at the call site. createCheloneHooks is the other route, for an app that would rather hold the map itself. The context hooks take neither: they read no Register and no resource name, and their caller writes the record type.

The plain hooks

In both adapters: useGetList, useGetOne, useGetMany, useGetManyAggregate, useGetManyReference, useCreate, useUpdate, useDelete, useUpdateMany, useDeleteMany, useGetListLive, useGetOneLive and useGetManyLive. Both bulk hooks build on core's updateManyMutationOptions and deleteManyMutationOptions.

Each takes TResources and TName. TResources defaults to RegisteredResources, which is what the Register augmentation fills (Registering your app), and TName defaults to keyof TResources & string. The resource argument is core's ResourceNameArg<TResources, TName>: a name of the map for a literal, plain string for a name the caller cannot pin, and never for a literal the map lacks.

The two hovers below are the whole difference between a literal and a string.

src/posts.tsx
const 
const list: UseQueryResult<GetListResult<{
    id: number;
    title: string;
}>, Error>
list
=
useGetList<{
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
    readonly comments: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            body: z.ZodString;
        }, z.core.$strip>;
    };
}, "posts">(resource: "posts", params?: GetListParams, options?: ListQueryOptions): UseQueryResult<GetListResult<{
    id: number;
    title: string;
}>, Error>
useGetList
("posts");
const
const row: {
    id: number;
    title: string;
} | undefined
row
=
const list: UseQueryResult<GetListResult<{
    id: number;
    title: string;
}>, Error>
list
.
data: GetListResult<{
    id: number;
    title: string;
}> | undefined
The data from before the failed refetch, which is kept. The last successfully resolved data for the query. `undefined`, since the first fetch failed before any data was cached. `undefined`, since the query has no data yet. The `placeholderData` shown while the query has no data yet.
data
?.
GetListResult<{ id: number; title: string; }>.data: {
    id: number;
    title: string;
}[]
data
[0];
src/posts.tsx
// A name read from context or props arrives as a `string`.
declare const const fromContext: stringfromContext: string;
const const loose: UseQueryResult<GetListResult<LooseRecord>, Error>loose = 
useGetList<{
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
    readonly comments: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            body: z.ZodString;
        }, z.core.$strip>;
    };
}, string>(resource: string, params?: GetListParams, options?: ListQueryOptions): UseQueryResult<GetListResult<LooseRecord>, Error>
useGetList
(const fromContext: stringfromContext);
const
const looseRow: LooseRecord | undefined
looseRow
= const loose: UseQueryResult<GetListResult<LooseRecord>, Error>loose.data: GetListResult<LooseRecord> | undefined
The data from before the failed refetch, which is kept. The last successfully resolved data for the query. `undefined`, since the first fetch failed before any data was cached. `undefined`, since the query has no data yet. The `placeholderData` shown while the query has no data yet.
data
?.GetListResult<LooseRecord>.data: LooseRecord[]data[0];

row is the post or undefined; looseRow is a LooseRecord, whose every field is unknown. When types widen covers what the loose arm costs.

Binding a map by hand

Every data hook still takes its map as a type argument, so a call can name one that is not the registered one:

src/posts.tsx
const 
const typed: UseQueryResult<GetListResult<{
    id: number;
    title: string;
}>, Error>
typed
=
useGetList<{
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}, "posts">(resource: "posts", params?: GetListParams, options?: ListQueryOptions): UseQueryResult<GetListResult<{
    id: number;
    title: string;
}>, Error>
useGetList
<typeof
const resources: {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}
resources
, "posts">("posts");
const
const row: {
    id: number;
    title: string;
} | undefined
row
=
const typed: UseQueryResult<GetListResult<{
    id: number;
    title: string;
}>, Error>
typed
.
data: GetListResult<{
    id: number;
    title: string;
}> | undefined
The data from before the failed refetch, which is kept. The last successfully resolved data for the query. `undefined`, since the first fetch failed before any data was cached. `undefined`, since the query has no data yet. The `placeholderData` shown while the query has no data yet.
data
?.
GetListResult<{ id: number; title: string; }>.data: {
    id: number;
    title: string;
}[]
data
[0];

This works, and it is clumsy: two type arguments at every call site, the map imported into every file that calls, and a call that forgets them silently falls back to RegisteredResources. Writing it once per hook is what the factory is for.

createCheloneHooks

createCheloneHooks is called once, in a module of the app, and returns the data hooks with TResources already written: the name is checked at each call site and the record is inferred from it.

src/hooks.ts
export const 
const hooks: {
    useGetList: <TName extends "posts" | "comments">(resource: ResourceNameArg<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
        readonly comments: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                body: z.ZodString;
            }, z.core.$strip>;
        };
    }, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
        readonly comments: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                body: z.ZodString;
            }, z.core.$strip>;
        };
    }, ResourceNameArg<...>>>, Error>;
    ... 8 more ...;
    useDeleteMany: <TName extends "posts" | "comments">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
        ...;
    }) | ... 6 more ... | {
        ...;
    };
}
hooks
=
createCheloneHooks<{
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
    readonly comments: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            body: z.ZodString;
        }, z.core.$strip>;
    };
}>(): {
    ...;
}
Bind the hooks to a registry type once — full inference at every call site: const hooks = createCheloneHooks<typeof resources>(); hooks.useGetList('posts') // data: Post[]; 'nope' rejects Each wrapper names the resource the hook itself names, so a bound call site types `data` by name and an unbound `string` still reaches the loose arm.
createCheloneHooks
<typeof
const resources: {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
    readonly comments: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            body: z.ZodString;
        }, z.core.$strip>;
    };
}
resources
>();
const
const bound: UseQueryResult<GetListResult<{
    id: number;
    title: string;
}>, Error>
bound
=
const hooks: {
    useGetList: <TName extends "posts" | "comments">(resource: ResourceNameArg<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
        readonly comments: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                body: z.ZodString;
            }, z.core.$strip>;
        };
    }, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
        readonly comments: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                body: z.ZodString;
            }, z.core.$strip>;
        };
    }, ResourceNameArg<...>>>, Error>;
    ... 8 more ...;
    useDeleteMany: <TName extends "posts" | "comments">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
        ...;
    }) | ... 6 more ... | {
        ...;
    };
}
hooks
.
useGetList: <"posts">(resource: "posts", params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<{
    id: number;
    title: string;
}>, Error>
useGetList
("posts");
const
const row: {
    id: number;
    title: string;
} | undefined
row
=
const bound: UseQueryResult<GetListResult<{
    id: number;
    title: string;
}>, Error>
bound
.
data: GetListResult<{
    id: number;
    title: string;
}> | undefined
The data from before the failed refetch, which is kept. The last successfully resolved data for the query. `undefined`, since the first fetch failed before any data was cached. `undefined`, since the query has no data yet. The `placeholderData` shown while the query has no data yet.
data
?.
GetListResult<{ id: number; title: string; }>.data: {
    id: number;
    title: string;
}[]
data
[0];

createCheloneHooks<typeof resources>() binds the map the module holds. createCheloneHooks<RegisteredResources>() is the form that follows Register, for an app that registers once and wants the bound hooks to read whatever that augmentation holds.

A bound call site is stricter than a plain one. Every member constrains TName to keyof TResources & string, so a name the caller cannot pin is rejected instead of falling to the loose arm. For a name read from context or props, use the plain hook, which keeps the string arm:

src/hooks.ts
export const 
const hooks: {
    useGetList: <TName extends "posts" | "comments">(resource: ResourceNameArg<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
        readonly comments: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                body: z.ZodString;
            }, z.core.$strip>;
        };
    }, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
        readonly comments: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                body: z.ZodString;
            }, z.core.$strip>;
        };
    }, ResourceNameArg<...>>>, Error>;
    ... 8 more ...;
    useDeleteMany: <TName extends "posts" | "comments">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
        ...;
    }) | ... 6 more ... | {
        ...;
    };
}
hooks
=
createCheloneHooks<{
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
    readonly comments: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            body: z.ZodString;
        }, z.core.$strip>;
    };
}>(): {
    ...;
}
Bind the hooks to a registry type once — full inference at every call site: const hooks = createCheloneHooks<typeof resources>(); hooks.useGetList('posts') // data: Post[]; 'nope' rejects Each wrapper names the resource the hook itself names, so a bound call site types `data` by name and an unbound `string` still reaches the loose arm.
createCheloneHooks
<typeof
const resources: {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
    readonly comments: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            body: z.ZodString;
        }, z.core.$strip>;
    };
}
resources
>();
declare const const fromContext: stringfromContext: string;
const hooks: {
    useGetList: <TName extends "posts" | "comments">(resource: ResourceNameArg<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
        readonly comments: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                body: z.ZodString;
            }, z.core.$strip>;
        };
    }, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
        readonly comments: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                body: z.ZodString;
            }, z.core.$strip>;
        };
    }, ResourceNameArg<...>>>, Error>;
    ... 8 more ...;
    useDeleteMany: <TName extends "posts" | "comments">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
        ...;
    }) | ... 6 more ... | {
        ...;
    };
}
hooks
.
useGetList: <"posts" | "comments">(resource: "posts" | "comments", params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<{
    id: number;
    title: string;
} | {
    id: number;
    body: string;
}>, Error>
useGetList
(fromContext);
Argument of type 'string' is not assignable to parameter of type '"posts" | "comments"'.

What the factory returns

MemberReactVue
useGetListyesyes
useGetOneyesyes
useGetManyyesyes
useGetManyAggregateyesyes
useGetManyReferenceyesyes
useCreateyesyes
useUpdateyesyes
useUpdateManyyesyes
useDeleteyesyes
useDeleteManyyesyes

useInfiniteGetList is exported by both adapters and is no member of either: its result is the infinite query shape, which the bound members do not model. useFieldValue and the context hooks below are not members either; they take no resource name.

Vue composables

Each Vue member forwards the composable's own signature rather than a fixed one, so params stay MaybeRefOrGetter: a ref or a getter is a legal argument, and a param that changes re-keys the query. data is a ref, so the read is list.data.value.

src/posts.vue
<script setup lang="ts">
const 
const hooks: {
    useGetList: <TName extends "posts">(resource: MaybeRefOrGetter<ResourceNameArg<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, TName>>, params?: MaybeRefOrGetter<GetListParams>, options?: ListQueryOptions) => UseQueryReturnType<GetListResult<RecordOf<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, ResourceNameArg<...>>>, Error>;
    ... 8 more ...;
    useDeleteMany: <TName extends "posts">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => UseMutationReturnType<...> | Undoable<...>;
}
hooks
=
createCheloneHooks<{
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}>(): {
    ...;
}
Bind the composables to a registry type once: const hooks = createCheloneHooks<typeof resources>(); hooks.useGetList('posts') // data: Post[]; 'nope' rejects
createCheloneHooks
<typeof
const resources: {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}
resources
>();
const const page: Ref<number, number>page = ref<number>(value: number): Ref<number, number> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which has a single property `.value` that points to the inner value.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
(1);
// The getter is read on every re-key, so the pagination follows `page`. const
const list: UseQueryReturnType<GetListResult<{
    id: number;
    title: string;
}>, Error>
list
=
const hooks: {
    useGetList: <TName extends "posts">(resource: MaybeRefOrGetter<ResourceNameArg<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, TName>>, params?: MaybeRefOrGetter<GetListParams>, options?: ListQueryOptions) => UseQueryReturnType<GetListResult<RecordOf<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, ResourceNameArg<...>>>, Error>;
    ... 8 more ...;
    useDeleteMany: <TName extends "posts">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => UseMutationReturnType<...> | Undoable<...>;
}
hooks
.
useGetList: <"posts">(resource: MaybeRefOrGetter<"posts">, params?: MaybeRefOrGetter<GetListParams>, options?: ListQueryOptions) => UseQueryReturnType<GetListResult<{
    id: number;
    title: string;
}>, Error>
useGetList
("posts", () => ({
GetListParams.pagination?: Pagination | undefinedpagination: { PagePagination.page: numberpage: const page: Ref<number, number>page.Ref<number, number>.value: numbervalue, PagePagination.perPage: numberperPage: 10 }, })); const
const row: {
    id: number;
    title: string;
} | undefined
row
=
const list: UseQueryReturnType<GetListResult<{
    id: number;
    title: string;
}>, Error>
list
.
data: Ref<undefined, undefined> | Ref<GetListResult<{
    id: number;
    title: string;
}>, GetListResult<{
    id: number;
    title: string;
}>>
The data from before the failed refetch, which is kept. The last successfully resolved data for the query. `undefined`, since the first fetch failed before any data was cached. `undefined`, since the query has no data yet. The `placeholderData` shown while the query has no data yet.
data
.
Ref<T = any, S = T>.value: GetListResult<{
    id: number;
    title: string;
}> | undefined
value
?.
GetListResult<{ id: number; title: string; }>.data: {
    id: number;
    title: string;
}[]
data
[0];
</script> <template> <div: HTMLAttributes & ReservedPropsdiv>{{
const row: {
    id: number;
    title: string;
} & Ref<unknown, unknown> & {
    value: {
        id: number;
        title: string;
    } | undefined;
}
row
?.title: string | undefinedtitle }}</div: HTMLAttributes & ReservedPropsdiv>
</template>

Mutations

useUpdate and useDelete write the record core resolves for the name: RecordOf<TResources, TName> for a name the map has, LooseRecord for a wide string, which is what ResourceRecordArg<TResources, TName> spells out. mutate takes { id, data, previousData }, where data is a Partial of that record.

src/hooks.ts
export const 
const hooks: {
    useGetList: <TName extends "posts">(resource: ResourceNameArg<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, ResourceNameArg<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, TName>>>, Error>;
    ... 8 more ...;
    useDeleteMany: <TName extends "posts">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
        ...;
    }) | ... 6 more ... | {
        ...;
    };
}
hooks
=
createCheloneHooks<{
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}>(): {
    ...;
}
Bind the hooks to a registry type once — full inference at every call site: const hooks = createCheloneHooks<typeof resources>(); hooks.useGetList('posts') // data: Post[]; 'nope' rejects Each wrapper names the resource the hook itself names, so a bound call site types `data` by name and an unbound `string` still reaches the loose arm.
createCheloneHooks
<typeof
const resources: {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}
resources
>();
const hooks: {
    useGetList: <TName extends "posts">(resource: ResourceNameArg<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, ResourceNameArg<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, TName>>>, Error>;
    ... 8 more ...;
    useDeleteMany: <TName extends "posts">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
        ...;
    }) | ... 6 more ... | {
        ...;
    };
}
hooks
.
useUpdate: <"posts">(resource: "posts", options?: MutationModeOptions) => (Override<MutationObserverIdleResult<{
    id: number;
    title: string;
}, Error, UpdateParams<{
    id: number;
    title: string;
}>, {
    previous?: undefined;
    key?: undefined;
    manySnapshot?: undefined;
    rollbackLists?: undefined;
} | {
    previous: {
        id: number;
        title: string;
    } | undefined;
    key: readonly [string, "getOne", {
        id: string;
        meta?: undefined;
    } | {
        id: string;
        meta: {} | null;
    }];
    manySnapshot: ManySnapshot<{
        id: number;
        title: string;
    }>;
    rollbackLists: () => void;
}>, {
    ...;
}> & {
    ...;
}) | ... 6 more ... | {
    ...;
}
useUpdate
("posts").
UndoableMutators<TData, TParams>.mutate: (arg0: UpdateParams<{
    id: number;
    title: string;
}>, arg1?: (MutateOptions<{
    id: number;
    title: string;
}, Error, UpdateParams<{
    id: number;
    title: string;
}>, {
    previous?: undefined;
    key?: undefined;
    manySnapshot?: undefined;
    rollbackLists?: undefined;
} | {
    previous: {
        id: number;
        title: string;
    } | undefined;
    key: readonly [string, "getOne", {
        id: string;
        meta?: undefined;
    } | {
        id: string;
        meta: {} | null;
    }];
    manySnapshot: ManySnapshot<{
        id: number;
        title: string;
    }>;
    rollbackLists: () => void;
}> & UndoableCallbacks<{
    id: number;
    title: string;
}, UpdateParams<...>>) | undefined) => void
The mutation function you can call with variables to trigger the mutation and optionally hooks on additional callback options.
@paramvariables - The variables object to pass to the `mutationFn`.@paramoptions.onSuccess - This function will fire when the mutation is successful and will be passed the mutation's result.@paramoptions.onError - This function will fire if the mutation encounters an error and will be passed the error.@paramoptions.onSettled - This function will fire when the mutation is either successfully fetched or encounters an error and be passed either the data or error.@remarks- If you make multiple requests, `onSuccess` will fire only after the latest call you've made. - All the callback functions (`onSuccess`, `onError`, `onSettled`) are void functions, and the returned value will be ignored.
mutate
({ UpdateParams<TRecord extends ResourceRecord = LooseRecord>.id: Identifierid: 1,
UpdateParams<{ id: number; title: string; }>.data: Partial<{
    id: number;
    title: string;
}>
data
: { title: 1 } });
Type 'number' is not assignable to type 'string'.

With mutationMode: "undoable" the adapter swaps in the pair core builds (undoableUpdateMutators and undoableDeleteMutators), which patches the cache now and defers the provider call. That pair is fire-and-forget: its mutateAsync calls mutate and settles at once, so in the undoable mode it resolves to nothing every time, undo or no undo. The Post | void on mutateAsync is the union of the undoable return and the ordinary one, because the adapter picks the shape from the mode it is handed at runtime and the type cannot see which one a call got. Await the record only in the ordinary modes:

src/hooks.ts
const 
const update: (Override<MutationObserverIdleResult<{
    id: number;
    title: string;
}, Error, UpdateParams<{
    id: number;
    title: string;
}>, {
    previous?: undefined;
    key?: undefined;
    manySnapshot?: undefined;
    rollbackLists?: undefined;
} | {
    previous: {
        id: number;
        title: string;
    } | undefined;
    key: readonly [string, "getOne", {
        id: string;
        meta?: undefined;
    } | {
        id: string;
        meta: {} | null;
    }];
    manySnapshot: ManySnapshot<{
        id: number;
        title: string;
    }>;
    rollbackLists: () => void;
}>, {
    mutate: UseMutateFunction<{
        id: number;
        title: string;
    }, Error, UpdateParams<{
        id: number;
        title: string;
    }>, {
        ...;
    } | {
        ...;
    }>;
}> & {
    ...;
}) | ... 6 more ... | {
    ...;
}
update
=
useUpdate<{
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}, "posts">(resource: "posts", options?: MutationModeOptions): (Override<MutationObserverIdleResult<{
    id: number;
    title: string;
}, Error, UpdateParams<{
    id: number;
    title: string;
}>, {
    previous?: undefined;
    key?: undefined;
    manySnapshot?: undefined;
    rollbackLists?: undefined;
} | {
    previous: {
        id: number;
        title: string;
    } | undefined;
    key: readonly [string, "getOne", {
        id: string;
        meta?: undefined;
    } | {
        id: string;
        meta: {} | null;
    }];
    manySnapshot: ManySnapshot<...>;
    rollbackLists: () => void;
}>, {
    ...;
}> & {
    ...;
}) | ... 6 more ... | {
    ...;
}
useUpdate
("posts", { MutationModeOptions.mutationMode?: MutationMode | undefinedmutationMode: "undoable" });
// The undoable pair settles once the undoable is enqueued. const
const done: Promise<void> | Promise<{
    id: number;
    title: string;
}>
done
=
const update: (Override<MutationObserverIdleResult<{
    id: number;
    title: string;
}, Error, UpdateParams<{
    id: number;
    title: string;
}>, {
    previous?: undefined;
    key?: undefined;
    manySnapshot?: undefined;
    rollbackLists?: undefined;
} | {
    previous: {
        id: number;
        title: string;
    } | undefined;
    key: readonly [string, "getOne", {
        id: string;
        meta?: undefined;
    } | {
        id: string;
        meta: {} | null;
    }];
    manySnapshot: ManySnapshot<{
        id: number;
        title: string;
    }>;
    rollbackLists: () => void;
}>, {
    mutate: UseMutateFunction<{
        id: number;
        title: string;
    }, Error, UpdateParams<{
        id: number;
        title: string;
    }>, {
        ...;
    } | {
        ...;
    }>;
}> & {
    ...;
}) | ... 6 more ... | {
    ...;
}
update
.
mutateAsync: (variables: UpdateParams<{
    id: number;
    title: string;
}>, options?: MutateOptions<{
    id: number;
    title: string;
}, Error, UpdateParams<{
    id: number;
    title: string;
}>, {
    previous?: undefined;
    key?: undefined;
    manySnapshot?: undefined;
    rollbackLists?: undefined;
} | {
    previous: {
        id: number;
        title: string;
    } | undefined;
    key: readonly [string, "getOne", {
        id: string;
        meta?: undefined;
    } | {
        id: string;
        meta: {} | null;
    }];
    manySnapshot: ManySnapshot<{
        id: number;
        title: string;
    }>;
    rollbackLists: () => void;
}> | undefined) => Promise<...> | Promise<...> (+1 overload)
mutateAsync
({ UpdateParams<TRecord extends ResourceRecord = LooseRecord>.id: Identifierid: 1,
UpdateParams<{ id: number; title: string; }>.data: Partial<{
    id: number;
    title: string;
}>
data
: { title?: string | undefinedtitle: "new" } });

packages/react/src/hooks.test-d.ts pins the type rather than the arrival: it asserts mutateAsync equals Post | void, and that assigning it to a Post is an error.

The record context hooks

useRecordContext<RecordType = LooseRecord> in both adapters, useRecordContextRef in Vue (a ComputedRef, since the ambient record can resolve after setup), and useListContext in both. React adds useEditContext, useShowContext and useCreateContext; Vue has no view contexts yet.

None of them reads Register, and none takes a resource name. The caller names the record:

src/record-title.tsx
const 
const record: RecordContextValue<{
    id: number;
    title: string;
}>
record
=
useRecordContext<{
    id: number;
    title: string;
}>(props?: UseRecordContextOptions<{
    id: number;
    title: string;
}> | undefined): RecordContextValue<{
    id: number;
    title: string;
}>
Read the ambient record. A `record` prop always wins over the context value (parity: ra-core's `useRecordContext`), so a caller can target a specific record inline. Returns `undefined` when neither a prop nor an ancestor provider supplies one.
useRecordContext
<
type RegisteredRecord<TName extends RegisteredResourceName> = {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}[TName] extends {
    schema: infer S;
} ? S extends StandardSchemaV1<unknown, infer Output> ? Output : LooseRecord : LooseRecord
The record type of a registered resource, or {@link LooseRecord } when it has no schema.
RegisteredRecord
<"posts">>();
const
const title: string | undefined
title
=
const record: RecordContextValue<{
    id: number;
    title: string;
}>
record
?.title: string | undefinedtitle;

RecordContextValue<RecordType> is RecordType | undefined. Vue's RecordValue<RecordType> is the same union, and it is not a ref: useRecordContext returns the value itself, and useRecordContextRef returns it inside a ComputedRef.

The type is a claim about the provider above it, not a fact read off it: a context value cannot carry a type argument, so the provider publishes the record at LooseRecord and the hook reads it back at whatever the caller wrote. Each adapter marks that read-back with a type-escape comment where the cast sits: useRecordContextRef and useListContext in Vue, useListContext and the edit and create context providers in React. A wrong RecordType here is a wrong claim, and nothing in the type system argues with it.

useFieldValue

useFieldValue<RecordType, TPath> reads the value at a source path off the ambient record. A typed record and a literal source give the value's type; with no record the path is loose and the value is unknown.

src/author.tsx
const 
const post: RecordContextValue<{
    id: number;
    title: string;
}>
post
=
useRecordContext<{
    id: number;
    title: string;
}>(props?: UseRecordContextOptions<{
    id: number;
    title: string;
}> | undefined): RecordContextValue<{
    id: number;
    title: string;
}>
Read the ambient record. A `record` prop always wins over the context value (parity: ra-core's `useRecordContext`), so a caller can target a specific record inline. Returns `undefined` when neither a prop nor an ancestor provider supplies one.
useRecordContext
<
type RegisteredRecord<TName extends RegisteredResourceName> = {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}[TName] extends {
    schema: infer S;
} ? S extends StandardSchemaV1<unknown, infer Output> ? Output : LooseRecord : LooseRecord
The record type of a registered resource, or {@link LooseRecord } when it has no schema.
RegisteredRecord
<"posts">>();
const
const title: string
title
=
useFieldValue<{
    id: number;
    title: string;
}, "title">(params: UseFieldValueOptions<{
    id: number;
    title: string;
}, "title">): string
Get the value of a field of the current record. A typed `record` and a literal `source` give the value's type; `useFieldValue<Post>(...)` alone infers no path, so the value is the union of Post's value types. The type does not include `undefined` for a record that has not loaded: before the ambient record resolves the read is `undefined`, so guard it in a view that renders while loading.
@exampleconst value = useFieldValue({ source: "author.name" });
useFieldValue
({ UseFieldValueOptions<{ id: number; title: string; }, "title">.source: "title"
The field path — dot members and/or bracket/numeric indices.
source
: "title",
UseRecordContextOptions<{ id: number; title: string; }>.record?: RecordContextValue<{
    id: number;
    title: string;
}>
record
:
const post: RecordContextValue<{
    id: number;
    title: string;
}>
post
});
const
const looseTitle: unknown
looseTitle
= useFieldValue<LooseRecord, "title">(params: UseFieldValueOptions<LooseRecord, "title">): unknown
Get the value of a field of the current record. A typed `record` and a literal `source` give the value's type; `useFieldValue<Post>(...)` alone infers no path, so the value is the union of Post's value types. The type does not include `undefined` for a record that has not loaded: before the ambient record resolves the read is `undefined`, so guard it in a view that renders while loading.
@exampleconst value = useFieldValue({ source: "author.name" });
useFieldValue
({ UseFieldValueOptions<LooseRecord, "title">.source: "title"
The field path — dot members and/or bracket/numeric indices.
source
: "title" });

The type does not include undefined for a record that has not loaded. Before the ambient record resolves the read is undefined, so a view that renders while loading guards it. In Vue the same read returns a ComputedRef, which re-renders when the record arrives.

A name the registry lacks

src/hooks.ts
export const 
const hooks: {
    useGetList: <TName extends "posts">(resource: ResourceNameArg<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, ResourceNameArg<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, TName>>>, Error>;
    ... 8 more ...;
    useDeleteMany: <TName extends "posts">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
        ...;
    }) | ... 6 more ... | {
        ...;
    };
}
hooks
=
createCheloneHooks<{
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}>(): {
    ...;
}
Bind the hooks to a registry type once — full inference at every call site: const hooks = createCheloneHooks<typeof resources>(); hooks.useGetList('posts') // data: Post[]; 'nope' rejects Each wrapper names the resource the hook itself names, so a bound call site types `data` by name and an unbound `string` still reaches the loose arm.
createCheloneHooks
<typeof
const resources: {
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}
resources
>();
const hooks: {
    useGetList: <TName extends "posts">(resource: ResourceNameArg<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, TName>, params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<RecordOf<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, ResourceNameArg<{
        readonly posts: {
            readonly schema: z.ZodObject<{
                id: z.ZodNumber;
                title: z.ZodString;
            }, z.core.$strip>;
        };
    }, TName>>>, Error>;
    ... 8 more ...;
    useDeleteMany: <TName extends "posts">(resource: ResourceNameArg<...>, options?: MutationModeOptions) => (Override<...> & {
        ...;
    }) | ... 6 more ... | {
        ...;
    };
}
hooks
.
useGetList: <"posts">(resource: "posts", params?: GetListParams, options?: ListQueryOptions) => UseQueryResult<GetListResult<{
    id: number;
    title: string;
}>, Error>
useGetList
("nope");
Argument of type '"nope"' is not assignable to parameter of type '"posts"'.
// The plain hook rejects it too, at `never`.
useGetList<{
    readonly posts: {
        readonly schema: z.ZodObject<{
            id: z.ZodNumber;
            title: z.ZodString;
        }, z.core.$strip>;
    };
}, "psots">(resource: never, params?: GetListParams, options?: ListQueryOptions): UseQueryResult<GetListResult<LooseRecord>, Error>
useGetList
("psots");
Argument of type '"psots"' is not assignable to parameter of type 'never'.

The bound calls are pinned in packages/vue/src/data/create-chelone-hooks.test-d.ts and packages/react/src/hooks.test.tsx. The plain hooks rest on ResourceNameArg, whose own arms are pinned in packages/core/src/resource/register.test-d.ts.