From 12148e00d83b8f6538ff1eb3fcd043be0635a7f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 15:52:44 +0000 Subject: [PATCH 1/6] fix(core): Type controller.set() values by schema controller.set() typed its value (and updater return) as `{}`, so `set(new schema.All(User), 42)` or `set(UserResource.getList.schema, 'x')` typechecked. Values are now the raw input the schema normalizes: Entity fields, rows for Collection/All, and a Query's inner schema input. Batch set() shares the same row typing. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BN4uSb49CL2pwcZFnTCJkc --- .changeset/controller-set-value-types.md | 22 ++++++++ docs/core/api/Controller.md | 8 +++ packages/core/src/controller/Controller.ts | 9 ++- packages/core/src/controller/setManyTypes.ts | 49 ++++++++++++---- .../src/hooks/__tests__/useController/set.tsx | 56 +++++++++++++++++++ website/blog/2026-10-03-v0.19-batch-set.md | 1 + 6 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 .changeset/controller-set-value-types.md diff --git a/.changeset/controller-set-value-types.md b/.changeset/controller-set-value-types.md new file mode 100644 index 000000000000..77f94e864b01 --- /dev/null +++ b/.changeset/controller-set-value-types.md @@ -0,0 +1,22 @@ +--- +'@data-client/core': patch +'@data-client/react': patch +'@data-client/vue': patch +--- + +Fix `controller.set()` accepting any value + +Values are now typed by the schema: Entities take their fields, while [Collection](https://dataclient.io/rest/api/Collection) +and [All](https://dataclient.io/rest/api/All) take a list of rows. [Query](https://dataclient.io/rest/api/Query) takes +the input of the schema it wraps, not what its `process()` returns. Updater functions must return the same. + +```ts +// Before: these all typechecked, then failed or wrote nothing at runtime +ctrl.set(new schema.All(Todo), 42); +ctrl.set(TodoResource.getList.schema, 'anything'); +ctrl.set(Todo, { id: '5' }, { id: '5', completed: 'yes' }); + +// After: TypeScript errors on the above; these typecheck +ctrl.set(TodoResource.getList.schema, [{ id: '5', completed: true }]); +ctrl.set(new schema.All(Todo), [{ id: '5', completed: true }]); +``` diff --git a/docs/core/api/Controller.md b/docs/core/api/Controller.md index c9331e6d464e..31b127a26236 100644 --- a/docs/core/api/Controller.md +++ b/docs/core/api/Controller.md @@ -378,6 +378,14 @@ ctrl.set( ); ``` +The value is typed by the schema: an [Entity](/rest/api/Entity) takes its fields (numbers and strings may be either), +while a [Collection](/rest/api/Collection) or [All](/rest/api/All) takes a list of rows. A [Query](/rest/api/Query) +takes the input of the schema it wraps, since `set()` normalizes that schema rather than reversing `process()`. + +```ts +ctrl.set(TodoResource.getList.schema, [{ id: '5', completed: true }]); +``` + Functions can be used in the value when derived data is used. This [prevents race conditions](https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state). ```ts diff --git a/packages/core/src/controller/Controller.ts b/packages/core/src/controller/Controller.ts index 230ec9181297..4589f3e96954 100644 --- a/packages/core/src/controller/Controller.ts +++ b/packages/core/src/controller/Controller.ts @@ -33,7 +33,7 @@ import { createSetResponse, } from './actions/index.js'; import ensurePojo from './ensurePojo.js'; -import type { SetManySchema, SetManyValue } from './setManyTypes.js'; +import type { SetManySchema, SetManyValue, SetValue } from './setManyTypes.js'; import type { EndpointUpdateFunction } from './types.js'; import { ReduxMiddlewareAPI } from '../manager/applyManager.js'; import type { GCInterface } from '../state/GCPolicy.js'; @@ -226,12 +226,15 @@ export default class Controller< */ set( schema: S, - ...rest: readonly [...SchemaArgs, (previousValue: Denormalize) => {}] + ...rest: readonly [ + ...SchemaArgs, + (previousValue: Denormalize) => SetValue, + ] ): Promise; set( schema: S, - ...rest: readonly [...SchemaArgs, {}] + ...rest: readonly [...SchemaArgs, SetValue] ): Promise; /** diff --git a/packages/core/src/controller/setManyTypes.ts b/packages/core/src/controller/setManyTypes.ts index 3f7350654c10..dd5b0db3f016 100644 --- a/packages/core/src/controller/setManyTypes.ts +++ b/packages/core/src/controller/setManyTypes.ts @@ -1,4 +1,4 @@ -/** Types for batch `Controller.set([Entity], rows)` */ +/** Value types for `Controller.set()`, including batch `set([Entity], rows)` */ import type { Denormalize } from '@data-client/normalizr'; /** Matches Entity classes (same members Denormalize<> checks). @@ -63,19 +63,44 @@ type SetRow = { readonly [K in Exclude>]?: SetField } : never; -/** Polymorphic rows may carry a discriminator that is not an Entity field */ -type SetRowOf = - Sch extends EntityLike ? SetRow - : SetRow & { readonly [k: string]: unknown }; - export type SetManyValue = S extends readonly (infer E)[] ? true extends IsUnion ? readonly { 'Use a Union schema for several Entity types': never }[] - : readonly SetRowOf>[] - : S extends { readonly schema: infer Sch } ? - Denormalize extends readonly (infer U)[] ? readonly SetRowOf[] - : Denormalize extends { readonly [k: string]: infer U } ? - { readonly [k: string]: SetRowOf } - : never + : SetInput[]> + : SetInput>; + +/** Raw input `set()` normalizes for a Queryable */ +export type SetValue = + NormalizedSchema extends infer N ? + N extends EntityLike ? + SetRow> + : SetInput> : never; + +/** Query normalizes with its inner schema; its process() output is not input. + * Entity is checked first since its static `schema` and `process` match too. */ +type NormalizedSchema = + S extends EntityLike ? S + : S extends { readonly schema: infer Sch; process(...args: any): any } ? Sch + : S; + +/** Raw input for a denormalized value, like a Collection's list or a Union's row */ +type SetInput = + 0 extends 1 & T ? any + : // not distributive, so a Union's members stay together for SetItem + [T] extends [readonly (infer U)[]] ? readonly SetItem[] + : [T] extends [object] ? + string extends keyof T ? + { readonly [k: string]: SetItem } + : SetItem + : SetField; + +/** + * One member of a list or keyed object. Polymorphic rows may carry a + * discriminator that is not an Entity field. + */ +type SetItem = + true extends IsUnion ? SetRow & { readonly [k: string]: unknown } + : U extends object ? SetRow + : SetField; diff --git a/packages/react/src/hooks/__tests__/useController/set.tsx b/packages/react/src/hooks/__tests__/useController/set.tsx index c0d0702e49b1..72fd2bff4c06 100644 --- a/packages/react/src/hooks/__tests__/useController/set.tsx +++ b/packages/react/src/hooks/__tests__/useController/set.tsx @@ -3,6 +3,7 @@ import { schema } from '@data-client/rest'; import { ArticleFromMixin, CoolerArticle, + CoolerArticleResource, FirstUnion, SecondUnion, UnionResource, @@ -252,6 +253,9 @@ describe('set', () => { ]); // @ts-expect-error id is a number controller.set([new schema.Invalidate(CoolerArticle)], [{ id: false }]); + controller.set(byKind, { id: '1' }, { id: '1', kind: 'first' }); + // @ts-expect-error body is a string + controller.set(byKind, { id: '1' }, { id: '1', body: false }); // EntityMixin rows controller.set([ArticleFromMixin], [{ id: 5, title: 'mixin' }]); // @ts-expect-error title is a string @@ -259,6 +263,57 @@ describe('set', () => { }; }); + it('should type values by the schema', async () => { + const { controller } = renderDataClient(() => null); + const list = CoolerArticleResource.getList.schema; + const all = new schema.All(CoolerArticle); + const titles = new schema.Query(all, articles => + articles.map(article => article.title), + ); + let promise: any; + act(() => { + promise = controller.set(list, [{ id: 5, title: 'listed' }]); + }); + await act(() => promise); + act(() => { + promise = controller.set(titles, [{ id: 6, title: 'queried' }]); + }); + await act(() => promise); + const state = controller.getState(); + expect(controller.get(list, state)?.map(({ id }) => id)).toEqual([5]); + expect(controller.get(titles, state)?.sort()).toEqual([ + 'listed', + 'queried', + ]); + + // type tests + () => { + controller.set(all, [{ id: '5', title: 'coerced' }]); + controller.set(list, articles => [ + ...articles.map(({ id }) => ({ id })), + { id: 7 }, + ]); + // @ts-expect-error All takes a list of rows + controller.set(all, 42); + // @ts-expect-error title is a string + controller.set(all, [{ id: 5, title: false }]); + // @ts-expect-error Collections take a list of rows + controller.set(list, 'anything'); + // @ts-expect-error unknown field + controller.set(list, [{ id: 5, bogus: 1 }]); + // @ts-expect-error updaters must return rows + controller.set(list, () => 42); + // @ts-expect-error Queries take their schema's input, not process() output + controller.set(titles, ['listed']); + // @ts-expect-error Entities take an object + controller.set(CoolerArticle, { id: 5 }, 5); + // @ts-expect-error title is a string + controller.set(CoolerArticle, { id: 5 }, { id: 5, title: false }); + // @ts-expect-error updaters must return the Entity's fields + controller.set(CoolerArticle, { id: 5 }, () => ({ title: false })); + }; + }); + it('should update store with error', async () => { const { result, controller } = renderDataClient(() => { return useQuery(CoolerArticle, { id: payload.id }); @@ -266,6 +321,7 @@ describe('set', () => { expect(result.current).toBeUndefined(); let promise: any; act(() => { + // @ts-expect-error testing runtime error promise = controller.set(CoolerArticle, { id: 5 }, 5); }); expect(result.current).toBeUndefined(); diff --git a/website/blog/2026-10-03-v0.19-batch-set.md b/website/blog/2026-10-03-v0.19-batch-set.md index 3874407c78a5..bab85795704d 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -16,6 +16,7 @@ import StackBlitz from '@site/src/components/StackBlitz'; - Fix TypeScript 7 module resolution for package `exports`; imports now resolve to declaration files ([#4019](https://github.com/reactive/data-client/pull/4019)) - [renderDataHook()](/docs/api/renderDataHook) runs provider mount effects when the first render suspends ([#4099](https://github.com/reactive/data-client/pull/4099)) +- [Controller.set()](/docs/api/Controller#set) values are typed by the schema, so `ctrl.set(new schema.All(Todo), 42)` is a TypeScript error ([#4133](https://github.com/reactive/data-client/pull/4133)) - Vue [useSuspense()](/vue/api/useSuspense) and [useLive()](/vue/api/useLive) keep the previous data while new arguments load, instead of returning `undefined` ([#4131](https://github.com/reactive/data-client/pull/4131)) - Vue [useSuspense()](/vue/api/useSuspense), [useDLE()](/vue/api/useDLE) and [useFetch()](/vue/api/useFetch) no longer refetch stale data on every store update, so a `controller.set()` is not overwritten ([#4134](https://github.com/reactive/data-client/pull/4134)) - Vue [useDLE()](/vue/api/useDLE) and [useCache()](/vue/api/useCache) keep expired `invalidIfStale` data through unrelated store updates instead of getting stuck loading ([#4142](https://github.com/reactive/data-client/pull/4142)) From 79cf14a96bb685c0e6dc12a69a2d9b1cd3b87556 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 15:59:55 +0000 Subject: [PATCH 2/6] enhance(core): Simplify controller.set() value types One overload for value or updater; SetRow handles primitives; rename the Query unwrap to InputSchema and exclude Entity with pk?: never. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BN4uSb49CL2pwcZFnTCJkc --- packages/core/src/controller/Controller.ts | 7 +--- packages/core/src/controller/setManyTypes.ts | 39 ++++++++++--------- .../src/hooks/__tests__/useController/set.tsx | 2 - 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/packages/core/src/controller/Controller.ts b/packages/core/src/controller/Controller.ts index 4589f3e96954..eae3fda74072 100644 --- a/packages/core/src/controller/Controller.ts +++ b/packages/core/src/controller/Controller.ts @@ -228,15 +228,10 @@ export default class Controller< schema: S, ...rest: readonly [ ...SchemaArgs, - (previousValue: Denormalize) => SetValue, + SetValue | ((previousValue: Denormalize) => SetValue), ] ): Promise; - set( - schema: S, - ...rest: readonly [...SchemaArgs, SetValue] - ): Promise; - /** * Sets every row of an Array or Values of one Entity (or Union) in one normalize. * @see https://dataclient.io/docs/api/Controller#set-array diff --git a/packages/core/src/controller/setManyTypes.ts b/packages/core/src/controller/setManyTypes.ts index dd5b0db3f016..b47836c1937b 100644 --- a/packages/core/src/controller/setManyTypes.ts +++ b/packages/core/src/controller/setManyTypes.ts @@ -55,34 +55,41 @@ type SetField = : T extends object ? unknown : T; -/** Fields of one row; like EntityFields, but distributive and without key remapping (TS 4.0) */ +/** Fields of one row (or a coerced primitive); like EntityFields, but distributive + * and without key remapping (TS 4.0) */ type SetRow = // EntityMixin and other untyped entities 0 extends 1 & U ? { readonly [k: string]: any } - : U extends unknown ? + : U extends object ? { readonly [K in Exclude>]?: SetField } - : never; + : SetField; export type SetManyValue = S extends readonly (infer E)[] ? true extends IsUnion ? readonly { 'Use a Union schema for several Entity types': never }[] - : SetInput[]> - : SetInput>; + : readonly SetItem>[] + : SetValue; /** Raw input `set()` normalizes for a Queryable */ export type SetValue = - NormalizedSchema extends infer N ? + InputSchema extends infer N ? N extends EntityLike ? SetRow> : SetInput> : never; -/** Query normalizes with its inner schema; its process() output is not input. - * Entity is checked first since its static `schema` and `process` match too. */ -type NormalizedSchema = - S extends EntityLike ? S - : S extends { readonly schema: infer Sch; process(...args: any): any } ? Sch +/** Query normalizes with its inner schema; its process() output is not input */ +type InputSchema = + S extends ( + { + readonly schema: infer Sch; + process(...args: any): any; + // excludes Entity, whose static schema and process() match the members above + pk?: never; + } + ) ? + Sch : S; /** Raw input for a denormalized value, like a Collection's list or a Union's row */ @@ -90,11 +97,8 @@ type SetInput = 0 extends 1 & T ? any : // not distributive, so a Union's members stay together for SetItem [T] extends [readonly (infer U)[]] ? readonly SetItem[] - : [T] extends [object] ? - string extends keyof T ? - { readonly [k: string]: SetItem } - : SetItem - : SetField; + : string extends keyof T ? { readonly [k: string]: SetItem } + : SetItem; /** * One member of a list or keyed object. Polymorphic rows may carry a @@ -102,5 +106,4 @@ type SetInput = */ type SetItem = true extends IsUnion ? SetRow & { readonly [k: string]: unknown } - : U extends object ? SetRow - : SetField; + : SetRow; diff --git a/packages/react/src/hooks/__tests__/useController/set.tsx b/packages/react/src/hooks/__tests__/useController/set.tsx index 72fd2bff4c06..528708276469 100644 --- a/packages/react/src/hooks/__tests__/useController/set.tsx +++ b/packages/react/src/hooks/__tests__/useController/set.tsx @@ -305,8 +305,6 @@ describe('set', () => { controller.set(list, () => 42); // @ts-expect-error Queries take their schema's input, not process() output controller.set(titles, ['listed']); - // @ts-expect-error Entities take an object - controller.set(CoolerArticle, { id: 5 }, 5); // @ts-expect-error title is a string controller.set(CoolerArticle, { id: 5 }, { id: 5, title: false }); // @ts-expect-error updaters must return the Entity's fields From b03b6480efe697135a1e1f4e59a33fe90755ac11 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 17:32:02 +0000 Subject: [PATCH 3/6] enhance(core): Speed up controller.set() value type checking - Infer S from the schema only (SkipInfer), so TS doesn't walk the value against every conditional in SetValue - Merge a Union's members into one row type instead of a union of rows, so each row is one comparison rather than one per member - Keep string/number literal fields exact On a 30-member Union with 1000 updater calls, check time drops from 16.8s to 4.7s (master, with untyped values: 3.3s); other extreme cases are now at or below master. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BN4uSb49CL2pwcZFnTCJkc --- packages/core/src/controller/Controller.ts | 14 ++++++-- packages/core/src/controller/setManyTypes.ts | 37 ++++++++++++++++---- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/core/src/controller/Controller.ts b/packages/core/src/controller/Controller.ts index eae3fda74072..fff264bce3fd 100644 --- a/packages/core/src/controller/Controller.ts +++ b/packages/core/src/controller/Controller.ts @@ -33,7 +33,12 @@ import { createSetResponse, } from './actions/index.js'; import ensurePojo from './ensurePojo.js'; -import type { SetManySchema, SetManyValue, SetValue } from './setManyTypes.js'; +import type { + SkipInfer, + SetManySchema, + SetManyValue, + SetValue, +} from './setManyTypes.js'; import type { EndpointUpdateFunction } from './types.js'; import { ReduxMiddlewareAPI } from '../manager/applyManager.js'; import type { GCInterface } from '../state/GCPolicy.js'; @@ -228,7 +233,10 @@ export default class Controller< schema: S, ...rest: readonly [ ...SchemaArgs, - SetValue | ((previousValue: Denormalize) => SetValue), + SkipInfer< + SetValue | ((previousValue: Denormalize) => SetValue), + S + >, ] ): Promise; @@ -238,7 +246,7 @@ export default class Controller< */ set( schema: S, - value: SetManyValue, + value: SkipInfer, S>, ): Promise; set( diff --git a/packages/core/src/controller/setManyTypes.ts b/packages/core/src/controller/setManyTypes.ts index b47836c1937b..75f139a9d835 100644 --- a/packages/core/src/controller/setManyTypes.ts +++ b/packages/core/src/controller/setManyTypes.ts @@ -48,22 +48,45 @@ type FunctionKeys = { [K in keyof U]: U[K] extends (...args: any) => any ? K : never; }[keyof U]; -/** Raw input for one field: numbers and strings coerce; objects are pre-normalize */ +/** Raw input for one field: numbers and strings coerce (literals stay exact); + * objects are pre-normalize */ type SetField = - T extends number ? T | string - : T extends string ? T | number + T extends number ? + number extends T ? + T | string + : T + : T extends string ? + string extends T ? + T | number + : T : T extends object ? unknown : T; -/** Fields of one row (or a coerced primitive); like EntityFields, but distributive - * and without key remapping (TS 4.0) */ +/** Non-function keys of any member of U */ +type FieldKeys = + U extends unknown ? Exclude> : never; + +/** Input for field K, from each member of U that has it */ +type MemberField = + U extends unknown ? + K extends keyof U ? + SetField + : never + : never; + +/** Fields of one row (or a coerced primitive); like EntityFields, but without + * key remapping (TS 4.0). A Union's members merge into one object type: checking + * a row against it costs one comparison instead of one per member. */ type SetRow = // EntityMixin and other untyped entities 0 extends 1 & U ? { readonly [k: string]: any } - : U extends object ? - { readonly [K in Exclude>]?: SetField } + : [U] extends [object] ? { readonly [K in FieldKeys]?: MemberField } : SetField; +/** Keeps S inferred from the schema alone: inferring it from the value too would + * walk the value's type against every conditional in SetValue (TS 5.4 has NoInfer) */ +export type SkipInfer = [T][S extends unknown ? 0 : never]; + export type SetManyValue = S extends readonly (infer E)[] ? true extends IsUnion ? From 303c6ea1708a33f70e82f71a1ef933fba88b2741 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 17:49:36 +0000 Subject: [PATCH 4/6] fix(core): Unwrap nested Queries in set() value types; document Union row tradeoff Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BN4uSb49CL2pwcZFnTCJkc --- docs/core/api/Controller.md | 9 +++++++++ packages/core/src/controller/setManyTypes.ts | 7 +++++-- packages/react/src/hooks/__tests__/useController/set.tsx | 9 +++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/core/api/Controller.md b/docs/core/api/Controller.md index 31b127a26236..4080d7c5c36d 100644 --- a/docs/core/api/Controller.md +++ b/docs/core/api/Controller.md @@ -386,6 +386,15 @@ takes the input of the schema it wraps, since `set()` normalizes that schema rat ctrl.set(TodoResource.getList.schema, [{ id: '5', completed: true }]); ``` +:::note Type checking limits + +To keep type checking fast for large [Unions](/rest/api/Union), a Union row is checked against the +combined fields of all its members rather than against one member. Each field's type is still checked, +but a row that mixes fields from different members (like `{ type: 'first', secondField: 1 }`) is not +an error. Make sure the fields you set belong to the member the row's discriminator selects. + +::: + Functions can be used in the value when derived data is used. This [prevents race conditions](https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state). ```ts diff --git a/packages/core/src/controller/setManyTypes.ts b/packages/core/src/controller/setManyTypes.ts index f098738877f0..751d0868fce7 100644 --- a/packages/core/src/controller/setManyTypes.ts +++ b/packages/core/src/controller/setManyTypes.ts @@ -85,13 +85,16 @@ export type SetManyValue = /** Raw input `set()` normalizes for a Queryable */ export type SetValue = - InputSchema extends infer N ? + InputSchema>> extends infer N ? N extends EntityInterface ? SetRow> : SetInput> : never; -/** Query normalizes with its inner schema; its process() output is not input */ +/** Query normalizes with its inner schema; its process() output is not input + * + * Applied three times in SetValue to unwrap nested Queries (TS 4.0 has no recursive aliases) + */ type InputSchema = S extends ( { diff --git a/packages/react/src/hooks/__tests__/useController/set.tsx b/packages/react/src/hooks/__tests__/useController/set.tsx index 528708276469..23cd50916864 100644 --- a/packages/react/src/hooks/__tests__/useController/set.tsx +++ b/packages/react/src/hooks/__tests__/useController/set.tsx @@ -270,6 +270,7 @@ describe('set', () => { const titles = new schema.Query(all, articles => articles.map(article => article.title), ); + const titleCount = new schema.Query(titles, list => list.length); let promise: any; act(() => { promise = controller.set(list, [{ id: 5, title: 'listed' }]); @@ -279,12 +280,18 @@ describe('set', () => { promise = controller.set(titles, [{ id: 6, title: 'queried' }]); }); await act(() => promise); + act(() => { + promise = controller.set(titleCount, [{ id: 8, title: 'nested' }]); + }); + await act(() => promise); const state = controller.getState(); expect(controller.get(list, state)?.map(({ id }) => id)).toEqual([5]); expect(controller.get(titles, state)?.sort()).toEqual([ 'listed', + 'nested', 'queried', ]); + expect(controller.get(titleCount, state)).toBe(3); // type tests () => { @@ -305,6 +312,8 @@ describe('set', () => { controller.set(list, () => 42); // @ts-expect-error Queries take their schema's input, not process() output controller.set(titles, ['listed']); + // @ts-expect-error nested Queries take the innermost schema's input + controller.set(titleCount, 3); // @ts-expect-error title is a string controller.set(CoolerArticle, { id: 5 }, { id: 5, title: false }); // @ts-expect-error updaters must return the Entity's fields From 0907ba1c28b65fe2267d8acef22e32d039a3b24f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 17:58:32 +0000 Subject: [PATCH 5/6] docs(blog): Interactive demo of typed controller.set() values Regenerate the playground's @data-client editor types so the demo reflects v0.19 set() typing. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BN4uSb49CL2pwcZFnTCJkc --- website/blog/2026-10-03-v0.19-batch-set.md | 67 +++++++++- .../editor-types/@data-client/core.d.ts | 123 +++++++++++++++--- .../editor-types/@data-client/endpoint.d.ts | 101 +++++++------- .../editor-types/@data-client/graphql.d.ts | 101 +++++++------- .../editor-types/@data-client/normalizr.d.ts | 12 +- .../editor-types/@data-client/react.d.ts | 41 +++--- .../@data-client/react/nextjs.d.ts | 4 +- .../@data-client/react/redux.d.ts | 5 +- .../editor-types/@data-client/react/ssr.d.ts | 8 +- .../editor-types/@data-client/rest.d.ts | 103 +++++++-------- 10 files changed, 349 insertions(+), 216 deletions(-) diff --git a/website/blog/2026-10-03-v0.19-batch-set.md b/website/blog/2026-10-03-v0.19-batch-set.md index 070ea327aa39..7d47d3699270 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -8,6 +8,7 @@ draft: true import DiffEditor from '@site/src/components/DiffEditor'; import StackBlitz from '@site/src/components/StackBlitz'; +import TypeScriptEditor from '@site/src/components/TypeScriptEditor'; **New APIs:** @@ -17,7 +18,7 @@ import StackBlitz from '@site/src/components/StackBlitz'; - Fix TypeScript 7 module resolution for package `exports`; imports now resolve to declaration files ([#4019](https://github.com/reactive/data-client/pull/4019)) - [renderDataHook()](/docs/api/renderDataHook) runs provider mount effects when the first render suspends ([#4099](https://github.com/reactive/data-client/pull/4099)) -- [Controller.set()](/docs/api/Controller#set) values are typed by the schema, so `ctrl.set(new schema.All(Todo), 42)` is a TypeScript error ([#4133](https://github.com/reactive/data-client/pull/4133)) +- [Controller.set() values are typed by the schema](/blog/2026/10/03/v0.19-batch-set#typed-set), so `ctrl.set(new schema.All(Todo), 42)` is a TypeScript error ([#4133](https://github.com/reactive/data-client/pull/4133)) - Vue [useSuspense()](/vue/api/useSuspense) and [useLive()](/vue/api/useLive) keep the previous data while new arguments load, instead of returning `undefined` ([#4131](https://github.com/reactive/data-client/pull/4131)) - Vue [useSuspense()](/vue/api/useSuspense) and [useLive()](/vue/api/useLive) send fetch errors after arguments change to `onErrorCaptured()` instead of an unhandled promise rejection ([#4135](https://github.com/reactive/data-client/pull/4135)) - Vue [useSuspense()](/vue/api/useSuspense), [useDLE()](/vue/api/useDLE) and [useFetch()](/vue/api/useFetch) no longer refetch stale data on every store update, so a `controller.set()` is not overwritten ([#4134](https://github.com/reactive/data-client/pull/4134)) @@ -108,6 +109,70 @@ is the Array schema, so match `action.schema[0]` for `[Ticker]` rather than the ::: +### Typed set() values {#typed-set} + +[Controller.set()](/docs/api/Controller#set) previously accepted any value for a schema, so a typo or a wrong +field type only surfaced as bad data at runtime. Values are now typed by the schema: an +[Entity](/rest/api/Entity) takes its fields, while a [Collection](/rest/api/Collection), [All](/rest/api/All) or +Array takes a list of rows. Every field is optional, since `set()` merges into what is already stored, and +numbers and strings are interchangeable just like in API responses. +[#4133](https://github.com/reactive/data-client/pull/4133) + +Hover the red underlines to see each error. + + + +```ts title="Todo" collapsed +import { Entity, resource } from '@data-client/rest'; + +export class Todo extends Entity { + id = 0; + userId = 0; + title = ''; + completed = false; + + static key = 'Todo'; +} + +export const TodoResource = resource({ + urlPrefix: 'https://jsonplaceholder.typicode.com', + path: '/todos/:id', + schema: Todo, +}); +``` + +```ts title="updateTodos" +import type { Controller } from '@data-client/react'; +import { schema } from '@data-client/rest'; +import { Todo, TodoResource } from './Todo'; + +export function updateTodos(ctrl: Controller) { + // ✅ rows are partial Todos; ids may be strings or numbers + ctrl.set(TodoResource.getList.schema, [{ id: '5', completed: true }]); + ctrl.set(Todo, { id: 5 }, todo => ({ completed: !todo.completed })); + ctrl.set([Todo], [{ id: 1, title: 'first' }, { id: 2 }]); + + // ❌ All takes a list of rows + ctrl.set(new schema.All(Todo), 42); + // ❌ completed is a boolean + ctrl.set(Todo, { id: 5 }, { id: 5, completed: 'yes' }); + // ❌ Todo has no done field + ctrl.set(TodoResource.getList.schema, [{ id: 5, done: true }]); + // ❌ updaters must return Todo fields + ctrl.set(Todo, { id: 5 }, todo => ({ title: todo.completed })); +} +``` + + + +A [Query](/rest/api/Query) takes the input of the schema it wraps, since `set()` normalizes that schema rather than +reversing `process()`. To keep type checking fast for large [Unions](/rest/api/Union), a Union row is checked +against the combined fields of all its members, so a row mixing fields from different members is not an error; see +[type checking limits](/docs/api/Controller#set). + +If code that previously compiled now fails here, it was writing data its schema doesn't describe. Fix the value, +or widen the Entity's field types if the data really can take that shape. + ## Other improvements ### Entity pk() args {#entity-pk-args} diff --git a/website/src/components/Playground/editor-types/@data-client/core.d.ts b/website/src/components/Playground/editor-types/@data-client/core.d.ts index 91d81d7e1479..4ecd94b5f5dd 100644 --- a/website/src/components/Playground/editor-types/@data-client/core.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/core.d.ts @@ -13,10 +13,7 @@ type Serializable = (value: any) => T; interface SchemaSimple { - normalize(input: any, parent: any, key: any, args: any[], visit: (...args: any) => any, delegate: { - getEntity: any; - setEntity: any; - }, + normalize(input: any, parent: any, key: any, delegate: INormalizeDelegate, /** The nearest enclosing entity-like schema (one with `pk`), if any. * Tracked automatically by the visit walker. */ parentEntity?: any): any; @@ -53,6 +50,11 @@ interface NormalizedIndex { }; }; } +/** Visits next data + schema while recurisvely normalizing */ +interface Visit { + (schema: any, value: any, parent: any, key: any): any; + creating?: boolean; +} /** Used in denormalize. Lookup to find an entity in the store table */ interface EntityPath { key: string; @@ -101,6 +103,10 @@ interface IDenormalizeDelegate { } /** Helpers during schema.normalize() */ interface INormalizeDelegate { + /** Recursive normalize of nested schemas */ + visit: Visit; + /** Raw endpoint args for this normalize call */ + readonly args: readonly any[]; /** Action meta-data for this normalize call */ readonly meta: { fetchedAt: number; @@ -411,22 +417,94 @@ type FetchFunction = (...args: A) => Pr declare class AbortOptimistic extends Error { } +/** Value types for `Controller.set()`, including batch `set([Entity], rows)` */ + +/** What one row normalizes to: a reference to one stored entity */ +type EntityRef = string | { + readonly id: string; + readonly schema: string; +}; +/** Schemas that write each row to one stored entity: Entity, Union, or Invalidate (batch delete). + * Query, All and Collection don't: they normalize to lists, or Collection keys by args batch set() lacks. */ +type SetEntitySchema = EntityInterface | { + _normalizeNullable(): EntityRef | undefined; + pk?: never; +}; +/** `[Entity]`, `schema.Array(Entity)` or `schema.Values(Entity)` (or of a Union or Invalidate) */ +type SetManySchema = readonly SetEntitySchema[] | { + readonly schema: SetEntitySchema | Record; + schemaKey(): string; + queryKey(...args: any): undefined; + pk?: never; +}; +type IsUnion = T extends unknown ? [ + U +] extends [T] ? false : true : never; +type FunctionKeys = { + [K in keyof U]: U[K] extends (...args: any) => any ? K : never; +}[keyof U]; +/** Raw input for one field: numbers and strings coerce (literals stay exact); + * objects are pre-normalize */ +type SetField = T extends number ? number extends T ? T | string : T : T extends string ? string extends T ? T | number : T : T extends object ? unknown : T; +/** Non-function keys of any member of U */ +type FieldKeys = U extends unknown ? Exclude> : never; +/** Input for field K, from each member of U that has it */ +type MemberField = U extends unknown ? K extends keyof U ? SetField : never : never; +/** Fields of one row (or a coerced primitive); like EntityFields, but without + * key remapping (TS 4.0). A Union's members merge into one object type: checking + * a row against it costs one comparison instead of one per member. */ +type SetRow = 0 extends 1 & U ? { + readonly [k: string]: any; +} : [U] extends [object] ? { + readonly [K in FieldKeys]?: MemberField; +} : SetField; +/** Keeps S inferred from the schema alone: inferring it from the value too would + * walk the value's type against every conditional in SetValue (TS 5.4 has NoInfer) */ +type SkipInfer = [T][S extends unknown ? 0 : never]; +type SetManyValue = S extends readonly (infer E)[] ? true extends IsUnion ? readonly { + 'Use a Union schema for several Entity types': never; +}[] : readonly SetItem>[] : SetValue; +/** Raw input `set()` normalizes for a Queryable */ +type SetValue = InputSchema>> extends infer N ? N extends EntityInterface ? SetRow> : SetInput> : never; +/** Query normalizes with its inner schema; its process() output is not input + * + * Applied three times in SetValue to unwrap nested Queries (TS 4.0 has no recursive aliases) + */ +type InputSchema = S extends ({ + readonly schema: infer Sch; + process(...args: any): any; + pk?: never; +}) ? Sch : S; +/** Raw input for a denormalized value, like a Collection's list or a Union's row */ +type SetInput = 0 extends 1 & T ? any : [ + T +] extends [readonly (infer U)[]] ? readonly SetItem[] : string extends keyof T ? { + readonly [k: string]: SetItem; +} : SetItem; +/** + * One member of a list or keyed object. Polymorphic rows may carry a + * discriminator that is not an Entity field. + */ +type SetItem = true extends IsUnion ? SetRow & { + readonly [k: string]: unknown; +} : SetRow; + type ResultEntry = E['schema'] extends undefined | null ? ResolveType : Normalize; type EndpointUpdateFunction = Record> = (source: ResultEntry, ...args: any) => { [K in keyof Updaters]: (result: Updaters[K]) => Updaters[K]; }; -declare const FETCH: "rdc/fetch"; -declare const SET: "rdc/set"; -declare const SET_RESPONSE: "rdc/setresponse"; -declare const OPTIMISTIC: "rdc/optimistic"; -declare const RESET: "rdc/reset"; -declare const SUBSCRIBE: "rdc/subscribe"; -declare const UNSUBSCRIBE: "rdc/unsubscribe"; -declare const INVALIDATE: "rdc/invalidate"; -declare const INVALIDATEALL: "rdc/invalidateall"; -declare const EXPIREALL: "rdc/expireall"; -declare const GC: "rdc/gc"; +declare const FETCH: 'rdc/fetch'; +declare const SET: 'rdc/set'; +declare const SET_RESPONSE: 'rdc/setresponse'; +declare const OPTIMISTIC: 'rdc/optimistic'; +declare const RESET: 'rdc/reset'; +declare const SUBSCRIBE: 'rdc/subscribe'; +declare const UNSUBSCRIBE: 'rdc/unsubscribe'; +declare const INVALIDATE: 'rdc/invalidate'; +declare const INVALIDATEALL: 'rdc/invalidateall'; +declare const EXPIREALL: 'rdc/expireall'; +declare const GC: 'rdc/gc'; declare const FETCH_TYPE: "rdc/fetch"; declare const SET_TYPE: "rdc/set"; declare const SET_RESPONSE_TYPE: "rdc/setresponse"; @@ -739,14 +817,14 @@ declare class Controller { */ fetch: ; - }>(endpoint: E, ...args: readonly [...Parameters]) => E["schema"] extends undefined | null ? ReturnType : Promise>; + }>(endpoint: E, ...args: readonly [...Parameters]) => E['schema'] extends undefined | null ? ReturnType : Promise>; /** * Fetches only if endpoint is considered 'stale'; otherwise returns undefined * @see https://dataclient.io/docs/api/Controller#fetchIfStale */ fetchIfStale: ; - }>(endpoint: E, ...args: readonly [...Parameters]) => E["schema"] extends undefined | null ? ReturnType | ResolveType : Promise> | Denormalize; + }>(endpoint: E, ...args: readonly [...Parameters]) => E['schema'] extends undefined | null ? ReturnType | ResolveType : Promise> | Denormalize; /** * Forces refetching and suspense on useSuspense with the same Endpoint and parameters. * @see https://dataclient.io/docs/api/Controller#invalidate @@ -777,8 +855,15 @@ declare class Controller { * Sets value for the Queryable and args. * @see https://dataclient.io/docs/api/Controller#set */ - set(schema: S, ...rest: readonly [...SchemaArgs, (previousValue: Denormalize) => {}]): Promise; - set(schema: S, ...rest: readonly [...SchemaArgs, {}]): Promise; + set(schema: S, ...rest: readonly [ + ...SchemaArgs, + SkipInfer | ((previousValue: Denormalize) => SetValue), S> + ]): Promise; + /** + * Sets every row of an Array or Values of one Entity (or Union) in one normalize. + * @see https://dataclient.io/docs/api/Controller#set-array + */ + set(schema: S, value: SkipInfer, S>): Promise; /** * Sets response for the Endpoint and args. * @see https://dataclient.io/docs/api/Controller#setResponse diff --git a/website/src/components/Playground/editor-types/@data-client/endpoint.d.ts b/website/src/components/Playground/editor-types/@data-client/endpoint.d.ts index 63b592809798..811e159e2f8b 100644 --- a/website/src/components/Playground/editor-types/@data-client/endpoint.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/endpoint.d.ts @@ -160,12 +160,16 @@ interface SnapshotInterface { } type ExpiryStatusInterface = 1 | 2 | 3; +/** Removes Rem's length worth of leading elements from Orig */ +type RemoveArray = Rem extends [any, ...infer RestRem] ? Orig extends [any, ...infer RestOrig] ? RemoveArray : never : Orig; + /** Get the Params type for a given Shape */ type EndpointParam = E extends (first: infer A, ...rest: any) => any ? A : E extends { key: (first: infer A, ...rest: any) => any; } ? A : never; /** What the function's promise resolves to */ type ResolveType any> = ReturnType extends Promise ? R : never; + type PartialParameters any> = T extends (...args: infer P) => any ? Partial

: never; type EndpointToFunction Promise> = (this: E, ...args: Parameters) => ReturnType; @@ -208,17 +212,12 @@ interface SchemaSimple { * @param input The value being normalized. * @param parent The parent object/array/dictionary containing `input`. * @param key The key under which `input` lives on `parent`. - * @param args The endpoint args for this normalize call. - * @param visit Recursive visitor for nested schemas. - * @param delegate Store accessors for reading/writing entities. + * @param delegate Recursive visitor, endpoint args, and store accessors. * @param parentEntity Nearest enclosing entity-like schema (one with `pk`), * tracked automatically by the visit walker. `Scalar` * uses this to discover its entity binding. */ - normalize(input: any, parent: any, key: any, args: any[], visit: (...args: any) => any, delegate: { - getEntity: any; - setEntity: any; - }, parentEntity?: any): any; + normalize(input: any, parent: any, key: any, delegate: INormalizeDelegate, parentEntity?: any): any; denormalize(input: {}, delegate: IDenormalizeDelegate): T; queryKey(args: Args, unvisit: (...args: any) => any, delegate: { getEntity: any; @@ -270,14 +269,13 @@ interface EntityTable { * Schemas that recurse via `visit` should pass their own * `input` (or the surrounding container) here. * @param key The key under which `value` lives on `parent`. - * @param args The endpoint args for this normalize call. * * The walker internally tracks the nearest enclosing entity-like schema and * forwards it to `schema.normalize` as a trailing `parentEntity` argument — - * see `SchemaSimple.normalize`. Consumers of `visit` don't pass it. + * see `SchemaSimple.normalize`. */ interface Visit { - (schema: any, value: any, parent: any, key: any, args: readonly any[]): any; + (schema: any, value: any, parent: any, key: any): any; creating?: boolean; } /** Used in denormalize. Lookup to find an entity in the store table */ @@ -331,6 +329,10 @@ interface IDenormalizeDelegate { } /** Helpers during schema.normalize() */ interface INormalizeDelegate { + /** Recursive normalize of nested schemas */ + visit: Visit; + /** Raw endpoint args for this normalize call */ + readonly args: readonly any[]; /** Action meta-data for this normalize call */ readonly meta: { fetchedAt: number; @@ -447,7 +449,6 @@ interface ExtendableEndpointConstructor { new & E, params?: any, body?: any) => Promise, S extends Schema | undefined = undefined, M extends boolean | undefined = false, E extends Record = {}>(RestFetch: F, options?: Readonly> & E): EndpointInstanceInterface & E; readonly prototype: Function; } -type RemoveArray = Rem extends [any, ...infer RestRem] ? Orig extends [any, ...infer RestOrig] ? RemoveArray : never : Orig; declare let Endpoint: EndpointConstructor; @@ -498,7 +499,7 @@ interface IEntityClass { * @param [key] When normalizing, the key where this entity was found * @param [args] ...args sent to Endpoint */ - pk IEntityInstance & InstanceType) & IEntityClass & TBase>(this: T, value: Partial>, parent?: any, key?: string, args?: any[]): string | number | undefined; + pk IEntityInstance & ConstructorInstance) & IEntityClass & TBase>(this: T, value: Partial>, parent?: any, key?: string, args?: readonly any[]): string | number | undefined; /** Return true to merge incoming data; false keeps existing entity * * @see https://dataclient.io/docs/api/Entity#shouldUpdate @@ -559,22 +560,19 @@ interface IEntityClass { * * @param [props] Plain Object of properties to assign. */ - fromJS IEntityInstance & InstanceType) & IEntityClass & TBase>(this: T, props?: Partial>): AbstractInstanceType; + fromJS IEntityInstance & ConstructorInstance) & IEntityClass & TBase>(this: T, props?: Partial>): AbstractInstanceType; /** Called when denormalizing an entity to create an instance when 'valid' * * @param [props] Plain Object of properties to assign. * @see https://dataclient.io/rest/api/Entity#createIfValid */ - createIfValid IEntityInstance & InstanceType) & IEntityClass & TBase>(this: T, props: Partial>): AbstractInstanceType | undefined; + createIfValid IEntityInstance & ConstructorInstance) & IEntityClass & TBase>(this: T, props: Partial>): AbstractInstanceType | undefined; /** Do any transformations when first receiving input * * @see https://dataclient.io/rest/api/Entity#process */ process(input: any, parent: any, key: string | undefined, args: any[]): any; - normalize(input: any, parent: any, key: string | undefined, args: any[], visit: (...args: any) => any, snapshot: { - getEntity: any; - setEntity: any; - }): any; + normalize(input: any, parent: any, key: string | undefined, delegate: INormalizeDelegate): any; /** Do any transformations when first receiving input * * @see https://dataclient.io/rest/api/Entity#validate @@ -585,7 +583,7 @@ interface IEntityClass { * @see https://dataclient.io/rest/api/Entity#queryKey */ queryKey(args: readonly any[], unvisit: any, delegate: IQueryDelegate): any; - denormalize IEntityInstance & InstanceType) & IEntityClass & TBase>(this: T, input: any, delegate: IDenormalizeDelegate): AbstractInstanceType; + denormalize IEntityInstance & ConstructorInstance) & IEntityClass & TBase>(this: T, input: any, delegate: IDenormalizeDelegate): AbstractInstanceType; /** All instance defaults set */ readonly defaults: any; } @@ -600,6 +598,11 @@ interface IEntityInstance { pk(parent?: any, key?: string, args?: readonly any[]): string | number | undefined; } type Constructor = abstract new (...args: any[]) => {}; +/** InstanceType<> for abstract constructor types, which TypeScript 4.2's InstanceType rejects + * + * Unlike AbstractInstanceType<>, this resolves constructor type aliases (not just classes) + */ +type ConstructorInstance any> = T extends abstract new (...args: any) => infer R ? R : any; type IDClass = abstract new (...args: any[]) => { id: string | number | undefined; }; @@ -624,9 +627,9 @@ interface RequiredPKOptions extends EntityOptions(Base: TBase, opt?: EntityOptions>): IEntityClass & TBase; -declare function EntityMixin(Base: TBase, opt?: EntityOptions>): IEntityClass & TBase & (new (...args: any[]) => IEntityInstance); -declare function EntityMixin(Base: TBase, opt: RequiredPKOptions>): IEntityClass & TBase & (new (...args: any[]) => IEntityInstance); +declare function EntityMixin(Base: TBase, opt?: EntityOptions>): IEntityClass & TBase; +declare function EntityMixin(Base: TBase, opt?: EntityOptions>): IEntityClass & TBase & (new (...args: any[]) => IEntityInstance); +declare function EntityMixin(Base: TBase, opt: RequiredPKOptions>): IEntityClass & TBase & (new (...args: any[]) => IEntityInstance); declare class PolymorphicSchema { private _schemaAttribute; @@ -637,7 +640,9 @@ declare class PolymorphicSchema { getSchemaAttribute(input: any, parent: any, key: any): any; inferSchema(input: any, parent: any, key: any): any; schemaKey(): string; - normalizeValue(value: any, parent: any, key: any, args: any[], visit: Visit): any; + normalizeValue(value: any, parent: any, key: any, delegate: { + visit: Visit; + }): any; denormalizeValue(value: any, unvisit: any): any; } @@ -667,7 +672,7 @@ declare class Invalidate ? string | ((input: any, parent: any, key: any) => string) : undefined); get key(): string; - normalize(input: any, parent: any, key: string | undefined, args: any[], visit: (...args: any) => any, delegate: INormalizeDelegate): string | { + normalize(input: any, parent: any, key: string | undefined, delegate: INormalizeDelegate): string | { id: string; schema: string; }; @@ -713,7 +718,7 @@ declare class Lazy implements SchemaSimple { * @param {Schema} schema - The inner schema (e.g., [Building], Building, Collection) */ constructor(schema: S); - normalize(input: any, parent: any, key: any, args: any[], visit: (...args: any) => any, _delegate: any): any; + normalize(input: any, parent: any, key: any, delegate: any): any; denormalize(input: {}, _delegate: IDenormalizeDelegate): any; queryKey(_args: readonly any[], _unvisit: (...args: any) => any, _delegate: any): undefined; /** Queryable schema for use with useQuery() to resolve lazy relationships */ @@ -869,7 +874,7 @@ declare class Scalar implements Mergeable { date: number; expiresAt: number; }; - normalize(input: any, parent: any, key: any, args: any[], visit: Visit, delegate: INormalizeDelegate, parentEntity: any): any; + normalize(input: any, parent: any, key: any, delegate: INormalizeDelegate, parentEntity: any): any; denormalize(input: any, delegate: IDenormalizeDelegate): any; /** * Returns the cpks of cells matching the current lens, or undefined. @@ -954,7 +959,7 @@ interface CollectionInterface any, delegate: INormalizeDelegate, parentEntity?: any): string; + normalize(input: any, parent: Parent, key: string, delegate: INormalizeDelegate, parentEntity?: any): string; /** Creates new instance copying over defined values of arguments * * @see https://dataclient.io/docs/api/Collection#merge @@ -1002,7 +1007,7 @@ interface CollectionInterface any | undefined; - denormalize(input: any, args: readonly any[], unvisit: (schema: any, input: any) => any): ReturnType; + denormalize(input: any, delegate: IDenormalizeDelegate): ReturnType; _denormalizeNullable(): ReturnType; _normalizeNullable(): ReturnType; /** Schema to place at the *end* of this Collection @@ -1037,6 +1042,7 @@ interface CollectionConstructor { } type StrategyFunction = (value: any, parent: any, key: string) => T; type SchemaFunction = (value: Args, parent: any, key: string) => K; +type SchemaAttribute = K | SchemaFunction; type MergeFunction = (entityA: any, entityB: any) => any; type SchemaAttributeFunction = (value: any, parent: any, key: string) => S; type UnionResult = { @@ -1069,18 +1075,14 @@ declare class Array$1 implements SchemaClass { input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): (S extends EntityMap ? UnionResult : Normalize)[]; _normalizeNullable(): - | (S extends EntityMap ? UnionResult : Normalize)[] - | undefined; + (S extends EntityMap ? UnionResult : Normalize)[] | undefined; _denormalizeNullable(): - | (S extends EntityMap ? T : Denormalize)[] - | undefined; + (S extends EntityMap ? T : Denormalize)[] | undefined; denormalize( input: {}, @@ -1123,18 +1125,14 @@ declare class All< input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): (S extends EntityMap ? UnionResult : Normalize)[]; _normalizeNullable(): - | (S extends EntityMap ? UnionResult : Normalize)[] - | undefined; + (S extends EntityMap ? UnionResult : Normalize)[] | undefined; _denormalizeNullable(): - | (S extends EntityMap ? T : Denormalize)[] - | undefined; + (S extends EntityMap ? T : Denormalize)[] | undefined; denormalize( input: {}, @@ -1167,8 +1165,6 @@ declare class Object$1< input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): NormalizeObject; @@ -1238,8 +1234,8 @@ interface UnionConstructor { */ interface UnionInstance< Choices extends EntityMap = any, - Args extends EntityFields> = - EntityFields>, + // unconstrained: TypeScript 4.2-4.4 can't prove UnionConstructor's Args satisfy EntityFields + Args = EntityFields>, > { readonly _hoistable: true; define(definition: Schema): void; @@ -1251,16 +1247,13 @@ interface UnionInstance< input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): UnionResult; _normalizeNullable(): UnionResult | undefined; _denormalizeNullable(): - | AbstractInstanceType - | undefined; + AbstractInstanceType | undefined; denormalize( input: {}, @@ -1322,8 +1315,6 @@ declare class Values implements SchemaClass { input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): Record< string, @@ -1403,6 +1394,7 @@ type schema_d_Query = SchemaAttribute; type schema_d_SchemaAttributeFunction = SchemaAttributeFunction; type schema_d_SchemaClass = SchemaClass; type schema_d_SchemaFunction = SchemaFunction; @@ -1412,15 +1404,14 @@ type schema_d_Union> = Union; declare const schema_d_Union: typeof Union; type schema_d_UnionConstructor = UnionConstructor; -type schema_d_UnionInstance> = - EntityFields>> = UnionInstance; +type schema_d_UnionInstance>> = UnionInstance; type schema_d_UnionResult = UnionResult; declare const schema_d_UnionRoot: typeof UnionRoot; type schema_d_Values = Values; declare const schema_d_Values: typeof Values; declare const schema_d_unshift: typeof unshift; declare namespace schema_d { - export { schema_d_All as All, Array$1 as Array, schema_d_Collection as Collection, type schema_d_CollectionArrayAdder as CollectionArrayAdder, type schema_d_CollectionArrayOrValuesAdder as CollectionArrayOrValuesAdder, type schema_d_CollectionConstructor as CollectionConstructor, type schema_d_CollectionFromSchema as CollectionFromSchema, type schema_d_CollectionInterface as CollectionInterface, schema_d_CollectionRoot as CollectionRoot, type schema_d_CollectionValuesAdder as CollectionValuesAdder, type schema_d_DefaultArgs as DefaultArgs, EntityMixin as Entity, type schema_d_EntityInterface as EntityInterface, type schema_d_EntityMap as EntityMap, schema_d_EntityMixin as EntityMixin, schema_d_Invalidate as Invalidate, schema_d_Lazy as Lazy, type schema_d_MergeFunction as MergeFunction, Object$1 as Object, schema_d_Query as Query, schema_d_Scalar as Scalar, type schema_d_SchemaAttributeFunction as SchemaAttributeFunction, type schema_d_SchemaClass as SchemaClass, type schema_d_SchemaFunction as SchemaFunction, type schema_d_StrategyFunction as StrategyFunction, schema_d_Union as Union, type schema_d_UnionConstructor as UnionConstructor, type schema_d_UnionInstance as UnionInstance, type schema_d_UnionResult as UnionResult, schema_d_UnionRoot as UnionRoot, schema_d_Values as Values, schema_d_unshift as unshift }; + export { schema_d_All as All, Array$1 as Array, schema_d_Collection as Collection, type schema_d_CollectionArrayAdder as CollectionArrayAdder, type schema_d_CollectionArrayOrValuesAdder as CollectionArrayOrValuesAdder, type schema_d_CollectionConstructor as CollectionConstructor, type schema_d_CollectionFromSchema as CollectionFromSchema, type schema_d_CollectionInterface as CollectionInterface, schema_d_CollectionRoot as CollectionRoot, type schema_d_CollectionValuesAdder as CollectionValuesAdder, type schema_d_DefaultArgs as DefaultArgs, EntityMixin as Entity, type schema_d_EntityInterface as EntityInterface, type schema_d_EntityMap as EntityMap, schema_d_EntityMixin as EntityMixin, schema_d_Invalidate as Invalidate, schema_d_Lazy as Lazy, type schema_d_MergeFunction as MergeFunction, Object$1 as Object, schema_d_Query as Query, schema_d_Scalar as Scalar, type schema_d_SchemaAttribute as SchemaAttribute, type schema_d_SchemaAttributeFunction as SchemaAttributeFunction, type schema_d_SchemaClass as SchemaClass, type schema_d_SchemaFunction as SchemaFunction, type schema_d_StrategyFunction as StrategyFunction, schema_d_Union as Union, type schema_d_UnionConstructor as UnionConstructor, type schema_d_UnionInstance as UnionInstance, type schema_d_UnionResult as UnionResult, schema_d_UnionRoot as UnionRoot, schema_d_Values as Values, schema_d_unshift as unshift }; } declare const Entity_base: IEntityClass { @@ -1457,7 +1448,9 @@ declare abstract class Entity extends Entity_base { * @param [key] When normalizing, the key where this entity was found * @param [args] ...args sent to Endpoint */ - static pk: (this: T, value: Partial>, parent?: any, key?: string, args?: any[]) => string | number | undefined; + static pk: { + pk(this: T, value: Partial>, parent?: any, key?: string, args?: readonly any[]): string | number | undefined; + }['pk']; /** Do any transformations when first receiving input * * @see https://dataclient.io/rest/api/Entity#process diff --git a/website/src/components/Playground/editor-types/@data-client/graphql.d.ts b/website/src/components/Playground/editor-types/@data-client/graphql.d.ts index 84ecb353d8a5..4f2d3c6f81df 100644 --- a/website/src/components/Playground/editor-types/@data-client/graphql.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/graphql.d.ts @@ -160,12 +160,16 @@ interface SnapshotInterface { } type ExpiryStatusInterface = 1 | 2 | 3; +/** Removes Rem's length worth of leading elements from Orig */ +type RemoveArray = Rem extends [any, ...infer RestRem] ? Orig extends [any, ...infer RestOrig] ? RemoveArray : never : Orig; + /** Get the Params type for a given Shape */ type EndpointParam = E extends (first: infer A, ...rest: any) => any ? A : E extends { key: (first: infer A, ...rest: any) => any; } ? A : never; /** What the function's promise resolves to */ type ResolveType any> = ReturnType extends Promise ? R : never; + type PartialParameters any> = T extends (...args: infer P) => any ? Partial

: never; type EndpointToFunction Promise> = (this: E, ...args: Parameters) => ReturnType; @@ -208,17 +212,12 @@ interface SchemaSimple { * @param input The value being normalized. * @param parent The parent object/array/dictionary containing `input`. * @param key The key under which `input` lives on `parent`. - * @param args The endpoint args for this normalize call. - * @param visit Recursive visitor for nested schemas. - * @param delegate Store accessors for reading/writing entities. + * @param delegate Recursive visitor, endpoint args, and store accessors. * @param parentEntity Nearest enclosing entity-like schema (one with `pk`), * tracked automatically by the visit walker. `Scalar` * uses this to discover its entity binding. */ - normalize(input: any, parent: any, key: any, args: any[], visit: (...args: any) => any, delegate: { - getEntity: any; - setEntity: any; - }, parentEntity?: any): any; + normalize(input: any, parent: any, key: any, delegate: INormalizeDelegate, parentEntity?: any): any; denormalize(input: {}, delegate: IDenormalizeDelegate): T; queryKey(args: Args, unvisit: (...args: any) => any, delegate: { getEntity: any; @@ -270,14 +269,13 @@ interface EntityTable { * Schemas that recurse via `visit` should pass their own * `input` (or the surrounding container) here. * @param key The key under which `value` lives on `parent`. - * @param args The endpoint args for this normalize call. * * The walker internally tracks the nearest enclosing entity-like schema and * forwards it to `schema.normalize` as a trailing `parentEntity` argument — - * see `SchemaSimple.normalize`. Consumers of `visit` don't pass it. + * see `SchemaSimple.normalize`. */ interface Visit { - (schema: any, value: any, parent: any, key: any, args: readonly any[]): any; + (schema: any, value: any, parent: any, key: any): any; creating?: boolean; } /** Used in denormalize. Lookup to find an entity in the store table */ @@ -331,6 +329,10 @@ interface IDenormalizeDelegate { } /** Helpers during schema.normalize() */ interface INormalizeDelegate { + /** Recursive normalize of nested schemas */ + visit: Visit; + /** Raw endpoint args for this normalize call */ + readonly args: readonly any[]; /** Action meta-data for this normalize call */ readonly meta: { fetchedAt: number; @@ -447,7 +449,6 @@ interface ExtendableEndpointConstructor { new & E, params?: any, body?: any) => Promise, S extends Schema | undefined = undefined, M extends boolean | undefined = false, E extends Record = {}>(RestFetch: F, options?: Readonly> & E): EndpointInstanceInterface & E; readonly prototype: Function; } -type RemoveArray = Rem extends [any, ...infer RestRem] ? Orig extends [any, ...infer RestOrig] ? RemoveArray : never : Orig; declare let Endpoint: EndpointConstructor; @@ -498,7 +499,7 @@ interface IEntityClass { * @param [key] When normalizing, the key where this entity was found * @param [args] ...args sent to Endpoint */ - pk IEntityInstance & InstanceType) & IEntityClass & TBase>(this: T, value: Partial>, parent?: any, key?: string, args?: any[]): string | number | undefined; + pk IEntityInstance & ConstructorInstance) & IEntityClass & TBase>(this: T, value: Partial>, parent?: any, key?: string, args?: readonly any[]): string | number | undefined; /** Return true to merge incoming data; false keeps existing entity * * @see https://dataclient.io/docs/api/Entity#shouldUpdate @@ -559,22 +560,19 @@ interface IEntityClass { * * @param [props] Plain Object of properties to assign. */ - fromJS IEntityInstance & InstanceType) & IEntityClass & TBase>(this: T, props?: Partial>): AbstractInstanceType; + fromJS IEntityInstance & ConstructorInstance) & IEntityClass & TBase>(this: T, props?: Partial>): AbstractInstanceType; /** Called when denormalizing an entity to create an instance when 'valid' * * @param [props] Plain Object of properties to assign. * @see https://dataclient.io/rest/api/Entity#createIfValid */ - createIfValid IEntityInstance & InstanceType) & IEntityClass & TBase>(this: T, props: Partial>): AbstractInstanceType | undefined; + createIfValid IEntityInstance & ConstructorInstance) & IEntityClass & TBase>(this: T, props: Partial>): AbstractInstanceType | undefined; /** Do any transformations when first receiving input * * @see https://dataclient.io/rest/api/Entity#process */ process(input: any, parent: any, key: string | undefined, args: any[]): any; - normalize(input: any, parent: any, key: string | undefined, args: any[], visit: (...args: any) => any, snapshot: { - getEntity: any; - setEntity: any; - }): any; + normalize(input: any, parent: any, key: string | undefined, delegate: INormalizeDelegate): any; /** Do any transformations when first receiving input * * @see https://dataclient.io/rest/api/Entity#validate @@ -585,7 +583,7 @@ interface IEntityClass { * @see https://dataclient.io/rest/api/Entity#queryKey */ queryKey(args: readonly any[], unvisit: any, delegate: IQueryDelegate): any; - denormalize IEntityInstance & InstanceType) & IEntityClass & TBase>(this: T, input: any, delegate: IDenormalizeDelegate): AbstractInstanceType; + denormalize IEntityInstance & ConstructorInstance) & IEntityClass & TBase>(this: T, input: any, delegate: IDenormalizeDelegate): AbstractInstanceType; /** All instance defaults set */ readonly defaults: any; } @@ -600,6 +598,11 @@ interface IEntityInstance { pk(parent?: any, key?: string, args?: readonly any[]): string | number | undefined; } type Constructor = abstract new (...args: any[]) => {}; +/** InstanceType<> for abstract constructor types, which TypeScript 4.2's InstanceType rejects + * + * Unlike AbstractInstanceType<>, this resolves constructor type aliases (not just classes) + */ +type ConstructorInstance any> = T extends abstract new (...args: any) => infer R ? R : any; type IDClass = abstract new (...args: any[]) => { id: string | number | undefined; }; @@ -624,9 +627,9 @@ interface RequiredPKOptions extends EntityOptions(Base: TBase, opt?: EntityOptions>): IEntityClass & TBase; -declare function EntityMixin(Base: TBase, opt?: EntityOptions>): IEntityClass & TBase & (new (...args: any[]) => IEntityInstance); -declare function EntityMixin(Base: TBase, opt: RequiredPKOptions>): IEntityClass & TBase & (new (...args: any[]) => IEntityInstance); +declare function EntityMixin(Base: TBase, opt?: EntityOptions>): IEntityClass & TBase; +declare function EntityMixin(Base: TBase, opt?: EntityOptions>): IEntityClass & TBase & (new (...args: any[]) => IEntityInstance); +declare function EntityMixin(Base: TBase, opt: RequiredPKOptions>): IEntityClass & TBase & (new (...args: any[]) => IEntityInstance); declare class PolymorphicSchema { private _schemaAttribute; @@ -637,7 +640,9 @@ declare class PolymorphicSchema { getSchemaAttribute(input: any, parent: any, key: any): any; inferSchema(input: any, parent: any, key: any): any; schemaKey(): string; - normalizeValue(value: any, parent: any, key: any, args: any[], visit: Visit): any; + normalizeValue(value: any, parent: any, key: any, delegate: { + visit: Visit; + }): any; denormalizeValue(value: any, unvisit: any): any; } @@ -667,7 +672,7 @@ declare class Invalidate ? string | ((input: any, parent: any, key: any) => string) : undefined); get key(): string; - normalize(input: any, parent: any, key: string | undefined, args: any[], visit: (...args: any) => any, delegate: INormalizeDelegate): string | { + normalize(input: any, parent: any, key: string | undefined, delegate: INormalizeDelegate): string | { id: string; schema: string; }; @@ -713,7 +718,7 @@ declare class Lazy implements SchemaSimple { * @param {Schema} schema - The inner schema (e.g., [Building], Building, Collection) */ constructor(schema: S); - normalize(input: any, parent: any, key: any, args: any[], visit: (...args: any) => any, _delegate: any): any; + normalize(input: any, parent: any, key: any, delegate: any): any; denormalize(input: {}, _delegate: IDenormalizeDelegate): any; queryKey(_args: readonly any[], _unvisit: (...args: any) => any, _delegate: any): undefined; /** Queryable schema for use with useQuery() to resolve lazy relationships */ @@ -869,7 +874,7 @@ declare class Scalar implements Mergeable { date: number; expiresAt: number; }; - normalize(input: any, parent: any, key: any, args: any[], visit: Visit, delegate: INormalizeDelegate, parentEntity: any): any; + normalize(input: any, parent: any, key: any, delegate: INormalizeDelegate, parentEntity: any): any; denormalize(input: any, delegate: IDenormalizeDelegate): any; /** * Returns the cpks of cells matching the current lens, or undefined. @@ -954,7 +959,7 @@ interface CollectionInterface any, delegate: INormalizeDelegate, parentEntity?: any): string; + normalize(input: any, parent: Parent, key: string, delegate: INormalizeDelegate, parentEntity?: any): string; /** Creates new instance copying over defined values of arguments * * @see https://dataclient.io/docs/api/Collection#merge @@ -1002,7 +1007,7 @@ interface CollectionInterface any | undefined; - denormalize(input: any, args: readonly any[], unvisit: (schema: any, input: any) => any): ReturnType; + denormalize(input: any, delegate: IDenormalizeDelegate): ReturnType; _denormalizeNullable(): ReturnType; _normalizeNullable(): ReturnType; /** Schema to place at the *end* of this Collection @@ -1037,6 +1042,7 @@ interface CollectionConstructor { } type StrategyFunction = (value: any, parent: any, key: string) => T; type SchemaFunction = (value: Args, parent: any, key: string) => K; +type SchemaAttribute = K | SchemaFunction; type MergeFunction = (entityA: any, entityB: any) => any; type SchemaAttributeFunction = (value: any, parent: any, key: string) => S; type UnionResult = { @@ -1069,18 +1075,14 @@ declare class Array$1 implements SchemaClass { input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): (S extends EntityMap ? UnionResult : Normalize)[]; _normalizeNullable(): - | (S extends EntityMap ? UnionResult : Normalize)[] - | undefined; + (S extends EntityMap ? UnionResult : Normalize)[] | undefined; _denormalizeNullable(): - | (S extends EntityMap ? T : Denormalize)[] - | undefined; + (S extends EntityMap ? T : Denormalize)[] | undefined; denormalize( input: {}, @@ -1123,18 +1125,14 @@ declare class All< input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): (S extends EntityMap ? UnionResult : Normalize)[]; _normalizeNullable(): - | (S extends EntityMap ? UnionResult : Normalize)[] - | undefined; + (S extends EntityMap ? UnionResult : Normalize)[] | undefined; _denormalizeNullable(): - | (S extends EntityMap ? T : Denormalize)[] - | undefined; + (S extends EntityMap ? T : Denormalize)[] | undefined; denormalize( input: {}, @@ -1167,8 +1165,6 @@ declare class Object$1< input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): NormalizeObject; @@ -1238,8 +1234,8 @@ interface UnionConstructor { */ interface UnionInstance< Choices extends EntityMap = any, - Args extends EntityFields> = - EntityFields>, + // unconstrained: TypeScript 4.2-4.4 can't prove UnionConstructor's Args satisfy EntityFields + Args = EntityFields>, > { readonly _hoistable: true; define(definition: Schema): void; @@ -1251,16 +1247,13 @@ interface UnionInstance< input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): UnionResult; _normalizeNullable(): UnionResult | undefined; _denormalizeNullable(): - | AbstractInstanceType - | undefined; + AbstractInstanceType | undefined; denormalize( input: {}, @@ -1322,8 +1315,6 @@ declare class Values implements SchemaClass { input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): Record< string, @@ -1403,6 +1394,7 @@ type schema_d_Query = SchemaAttribute; type schema_d_SchemaAttributeFunction = SchemaAttributeFunction; type schema_d_SchemaClass = SchemaClass; type schema_d_SchemaFunction = SchemaFunction; @@ -1412,15 +1404,14 @@ type schema_d_Union> = Union; declare const schema_d_Union: typeof Union; type schema_d_UnionConstructor = UnionConstructor; -type schema_d_UnionInstance> = - EntityFields>> = UnionInstance; +type schema_d_UnionInstance>> = UnionInstance; type schema_d_UnionResult = UnionResult; declare const schema_d_UnionRoot: typeof UnionRoot; type schema_d_Values = Values; declare const schema_d_Values: typeof Values; declare const schema_d_unshift: typeof unshift; declare namespace schema_d { - export { schema_d_All as All, Array$1 as Array, schema_d_Collection as Collection, type schema_d_CollectionArrayAdder as CollectionArrayAdder, type schema_d_CollectionArrayOrValuesAdder as CollectionArrayOrValuesAdder, type schema_d_CollectionConstructor as CollectionConstructor, type schema_d_CollectionFromSchema as CollectionFromSchema, type schema_d_CollectionInterface as CollectionInterface, schema_d_CollectionRoot as CollectionRoot, type schema_d_CollectionValuesAdder as CollectionValuesAdder, type schema_d_DefaultArgs as DefaultArgs, EntityMixin as Entity, type schema_d_EntityInterface as EntityInterface, type schema_d_EntityMap as EntityMap, schema_d_EntityMixin as EntityMixin, schema_d_Invalidate as Invalidate, schema_d_Lazy as Lazy, type schema_d_MergeFunction as MergeFunction, Object$1 as Object, schema_d_Query as Query, schema_d_Scalar as Scalar, type schema_d_SchemaAttributeFunction as SchemaAttributeFunction, type schema_d_SchemaClass as SchemaClass, type schema_d_SchemaFunction as SchemaFunction, type schema_d_StrategyFunction as StrategyFunction, schema_d_Union as Union, type schema_d_UnionConstructor as UnionConstructor, type schema_d_UnionInstance as UnionInstance, type schema_d_UnionResult as UnionResult, schema_d_UnionRoot as UnionRoot, schema_d_Values as Values, schema_d_unshift as unshift }; + export { schema_d_All as All, Array$1 as Array, schema_d_Collection as Collection, type schema_d_CollectionArrayAdder as CollectionArrayAdder, type schema_d_CollectionArrayOrValuesAdder as CollectionArrayOrValuesAdder, type schema_d_CollectionConstructor as CollectionConstructor, type schema_d_CollectionFromSchema as CollectionFromSchema, type schema_d_CollectionInterface as CollectionInterface, schema_d_CollectionRoot as CollectionRoot, type schema_d_CollectionValuesAdder as CollectionValuesAdder, type schema_d_DefaultArgs as DefaultArgs, EntityMixin as Entity, type schema_d_EntityInterface as EntityInterface, type schema_d_EntityMap as EntityMap, schema_d_EntityMixin as EntityMixin, schema_d_Invalidate as Invalidate, schema_d_Lazy as Lazy, type schema_d_MergeFunction as MergeFunction, Object$1 as Object, schema_d_Query as Query, schema_d_Scalar as Scalar, type schema_d_SchemaAttribute as SchemaAttribute, type schema_d_SchemaAttributeFunction as SchemaAttributeFunction, type schema_d_SchemaClass as SchemaClass, type schema_d_SchemaFunction as SchemaFunction, type schema_d_StrategyFunction as StrategyFunction, schema_d_Union as Union, type schema_d_UnionConstructor as UnionConstructor, type schema_d_UnionInstance as UnionInstance, type schema_d_UnionResult as UnionResult, schema_d_UnionRoot as UnionRoot, schema_d_Values as Values, schema_d_unshift as unshift }; } declare const Entity_base: IEntityClass { @@ -1457,7 +1448,9 @@ declare abstract class Entity extends Entity_base { * @param [key] When normalizing, the key where this entity was found * @param [args] ...args sent to Endpoint */ - static pk: (this: T, value: Partial>, parent?: any, key?: string, args?: any[]) => string | number | undefined; + static pk: { + pk(this: T, value: Partial>, parent?: any, key?: string, args?: readonly any[]): string | number | undefined; + }['pk']; /** Do any transformations when first receiving input * * @see https://dataclient.io/rest/api/Entity#process diff --git a/website/src/components/Playground/editor-types/@data-client/normalizr.d.ts b/website/src/components/Playground/editor-types/@data-client/normalizr.d.ts index cdfd4b43f051..dfb483735410 100644 --- a/website/src/components/Playground/editor-types/@data-client/normalizr.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/normalizr.d.ts @@ -13,10 +13,7 @@ type Serializable = (value: any) => T; interface SchemaSimple { - normalize(input: any, parent: any, key: any, args: any[], visit: (...args: any) => any, delegate: { - getEntity: any; - setEntity: any; - }, + normalize(input: any, parent: any, key: any, delegate: INormalizeDelegate, /** The nearest enclosing entity-like schema (one with `pk`), if any. * Tracked automatically by the visit walker. */ parentEntity?: any): any; @@ -60,7 +57,8 @@ interface EntityTable { } /** Visits next data + schema while recurisvely normalizing */ interface Visit { - (schema: any, value: any, parent: any, key: any, args: readonly any[]): any; + (schema: any, value: any, parent: any, key: any): any; + creating?: boolean; } /** Used in denormalize. Lookup to find an entity in the store table */ interface EntityPath { @@ -114,6 +112,10 @@ interface IDenormalizeDelegate { } /** Helpers during schema.normalize() */ interface INormalizeDelegate { + /** Recursive normalize of nested schemas */ + visit: Visit; + /** Raw endpoint args for this normalize call */ + readonly args: readonly any[]; /** Action meta-data for this normalize call */ readonly meta: { fetchedAt: number; diff --git a/website/src/components/Playground/editor-types/@data-client/react.d.ts b/website/src/components/Playground/editor-types/@data-client/react.d.ts index 117ee2e0b3d5..218667c8cb94 100644 --- a/website/src/components/Playground/editor-types/@data-client/react.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/react.d.ts @@ -1,19 +1,19 @@ import * as _data_client_core from '@data-client/core'; import { NetworkManager, Manager, State, Controller, GCInterface, DevToolsManager, DevToolsConfig, SubscriptionManager, EndpointInterface, FetchFunction, Schema, ResolveType, Denormalize, DenormalizeNullable, Queryable, NI, SchemaArgs, NetworkError, UnknownError, ErrorTypes as ErrorTypes$1, __INTERNAL__, actions, applyManager, createReducer, initManager } from '@data-client/core'; export { AbstractInstanceType, ActionTypes, Controller, CreateCountRef, DataClientDispatch, DefaultConnectionListener, Denormalize, DenormalizeNullable, DevToolsManager, Dispatch, EndpointExtraOptions, EndpointInterface, EntityInterface, ErrorTypes, ExpiryStatus, FetchAction, FetchFunction, GCInterface, GCOptions, GCPolicy, GenericDispatch, InvalidateAction, LogoutManager, Manager, Middleware, MiddlewareAPI, NetworkError, NetworkManager, Normalize, NormalizeNullable, PK, PollingSubscription, Queryable, ResetAction, ResolveType, Schema, SchemaArgs, SchemaClass, SetAction, SetResponseAction, State, SubscribeAction, SubscriptionManager, UnknownError, UnsubscribeAction, UpdateFunction, actionTypes } from '@data-client/core'; -import * as react_jsx_runtime from 'react/jsx-runtime'; -import React, { JSX, Context } from 'react'; +import * as React from 'react'; +import React__default, { JSX, Context } from 'react'; /** Can help prevent stuttering by waiting for idle for sideEffect free fetches */ declare class WebIdlingNetworkManager extends NetworkManager { } -declare function BackupLoading(): react_jsx_runtime.JSX.Element; +declare function BackupLoading(): React__default.JSX.Element; type DevToolsPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; interface ProviderProps { - children: React.ReactNode; + children: React__default.ReactNode; managers?: Manager[]; initialState?: State; Controller?: typeof Controller; @@ -44,18 +44,25 @@ type ConstructorArgs; //# sourceMappingURL=UniversalSuspense.d.ts.map +declare const ErrorFallback: ({ error, className, }: { + error: Error; + resetErrorBoundary: () => void; + className?: string; +}) => React.JSX.Element; +//# sourceMappingURL=ErrorFallback.d.ts.map + interface ErrorBoundaryProps { - children: React.ReactNode; + children: React__default.ReactNode; /** className prop sent to fallbackComponent */ className?: string; /** Renders when an error is caught */ - fallbackComponent: React.ComponentType<{ + fallbackComponent: React__default.ComponentType<{ error: E; resetErrorBoundary: () => void; className?: string; @@ -70,13 +77,9 @@ interface ErrorState { * Reusable React error boundary component * @see https://dataclient.io/docs/api/ErrorBoundary */ -declare class ErrorBoundary extends React.Component, ErrorState> { +declare class ErrorBoundary extends React__default.Component, ErrorState> { static defaultProps: { - fallbackComponent: ({ error, className, }: { - error: Error; - resetErrorBoundary: () => void; - className?: string; - }) => react_jsx_runtime.JSX.Element; + fallbackComponent: typeof ErrorFallback; }; static getDerivedStateFromError(error: Error): { error: Error; @@ -99,8 +102,8 @@ declare function AsyncBoundary({ children, errorComponent, fallback, ...errorPro declare const _default: typeof AsyncBoundary; interface Props { - children: React.ReactNode; - fallback?: React.ReactNode; + children: React__default.ReactNode; + fallback?: React__default.ReactNode; errorClassName?: string; /** Renders when an error is caught */ errorComponent?: ErrorBoundaryProps['fallbackComponent']; @@ -295,9 +298,9 @@ declare namespace internal_d { export { internal_d_INVALID as INVALID, internal_d_MemoCache as MemoCache, internal_d_actions as actions, internal_d_applyManager as applyManager, internal_d_createReducer as createReducer, internal_d_initManager as initManager, internal_d_initialState as initialState, internal_d_useCacheState as useCacheState }; } -type ReducerAction> = R extends React.Reducer ? A : never; +type ReducerAction> = R extends React__default.Reducer ? A : never; /** Turns a dispatch function into one that resolves once its been commited */ -declare function usePromisifiedDispatch>(dispatch: React.Dispatch>, state: React.ReducerState): (action: ReducerAction) => Promise; +declare function usePromisifiedDispatch>(dispatch: React__default.Dispatch>, state: React__default.ReducerState): (action: ReducerAction) => Promise; export { _default as AsyncBoundary, BackupLoading, DataProvider as CacheProvider, ControllerContext, DataProvider, type DevToolsPosition, ErrorBoundary, WebIdlingNetworkManager as IdlingNetworkManager, ErrorBoundary as NetworkErrorBoundary, type ProviderProps, StateContext, type Store, StoreContext, UniversalSuspense, internal_d as __INTERNAL__, getDefaultManagers, useCache, useCancelling, useController, useDLE, useDebounce, useError, useFetch, useLive, useLoading, usePromisifiedDispatch, useQuery, useSubscription, useSuspense }; diff --git a/website/src/components/Playground/editor-types/@data-client/react/nextjs.d.ts b/website/src/components/Playground/editor-types/@data-client/react/nextjs.d.ts index a6997a7743d4..0afbf7d12dd0 100644 --- a/website/src/components/Playground/editor-types/@data-client/react/nextjs.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/react/nextjs.d.ts @@ -1,10 +1,10 @@ -import React$1, { JSX, ComponentProps } from 'react'; +import react, { JSX, ComponentProps } from 'react'; import { Manager, State, Controller, GCInterface } from '@data-client/core'; type DevToolsPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; interface ProviderProps$1 { - children: React$1.ReactNode; + children: react.ReactNode; managers?: Manager[]; initialState?: State; Controller?: typeof Controller; diff --git a/website/src/components/Playground/editor-types/@data-client/react/redux.d.ts b/website/src/components/Playground/editor-types/@data-client/react/redux.d.ts index 09cb15c4020f..78e23d10b455 100644 --- a/website/src/components/Playground/editor-types/@data-client/react/redux.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/react/redux.d.ts @@ -1,7 +1,6 @@ import * as _data_client_core from '@data-client/core'; import { ActionTypes, State, Manager, Controller, GCInterface, Dispatch as Dispatch$1 } from '@data-client/core'; export { applyManager, createReducer, initialState } from '@data-client/core'; -import * as react_jsx_runtime from 'react/jsx-runtime'; import React from 'react'; /** @@ -151,7 +150,7 @@ type ReducersMapObject { * Like DataProvider, but for an external store * @see https://dataclient.io/docs/api/ExternalDataProvider */ -declare function ExternalDataProvider({ children, store, selector, controller, devButton, hasDevManager, }: Props): react_jsx_runtime.JSX.Element; +declare function ExternalDataProvider({ children, store, selector, controller, devButton, hasDevManager, }: Props): React.JSX.Element; declare const mapMiddleware: (selector: (state: any) => State) => (...middlewares: Middleware[]) => M; //# sourceMappingURL=mapMiddleware.d.ts.map diff --git a/website/src/components/Playground/editor-types/@data-client/react/ssr.d.ts b/website/src/components/Playground/editor-types/@data-client/react/ssr.d.ts index f6408e597dbe..936482a8fd81 100644 --- a/website/src/components/Playground/editor-types/@data-client/react/ssr.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/react/ssr.d.ts @@ -1,6 +1,6 @@ import * as _data_client_core from '@data-client/core'; import { Manager, State, Controller } from '@data-client/core'; -import * as react_jsx_runtime from 'react/jsx-runtime'; +import * as react from 'react'; /** * An *action* is a plain object that represents an intention to change the @@ -156,7 +156,7 @@ interface Store { declare function createPersistedStore(managers?: Manager[], hasDevManager?: boolean): readonly [({ children }: { children: React.ReactNode; -}) => react_jsx_runtime.JSX.Element, () => State, Controller<_data_client_core.DataClientDispatch>, Store, _data_client_core.ActionTypes, unknown> & { +}) => react.JSX.Element, () => State, Controller<_data_client_core.DataClientDispatch>, Store, _data_client_core.ActionTypes, unknown> & { dispatch: unknown; }]; @@ -165,12 +165,12 @@ declare const getInitialData: (id?: string) => any; declare function createServerDataComponent(useReadyCacheState: () => State, id?: string): ({ nonce }: { nonce?: string | undefined; -}) => react_jsx_runtime.JSX.Element; +}) => react.JSX.Element; declare const ServerData: ({ data, nonce, id, }: { data: State; id?: string; nonce?: string | undefined; -}) => react_jsx_runtime.JSX.Element | null; +}) => react.JSX.Element | null; export { ServerData, awaitInitialData, createPersistedStore, createServerDataComponent, getInitialData }; diff --git a/website/src/components/Playground/editor-types/@data-client/rest.d.ts b/website/src/components/Playground/editor-types/@data-client/rest.d.ts index 49875188a970..73b26c94c2b2 100644 --- a/website/src/components/Playground/editor-types/@data-client/rest.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/rest.d.ts @@ -162,12 +162,16 @@ interface SnapshotInterface { } type ExpiryStatusInterface = 1 | 2 | 3; +/** Removes Rem's length worth of leading elements from Orig */ +type RemoveArray = Rem extends [any, ...infer RestRem] ? Orig extends [any, ...infer RestOrig] ? RemoveArray : never : Orig; + /** Get the Params type for a given Shape */ type EndpointParam = E extends (first: infer A, ...rest: any) => any ? A : E extends { key: (first: infer A, ...rest: any) => any; } ? A : never; /** What the function's promise resolves to */ type ResolveType any> = ReturnType extends Promise ? R : never; + type PartialParameters any> = T extends (...args: infer P) => any ? Partial

: never; type EndpointToFunction Promise> = (this: E, ...args: Parameters) => ReturnType; @@ -210,17 +214,12 @@ interface SchemaSimple { * @param input The value being normalized. * @param parent The parent object/array/dictionary containing `input`. * @param key The key under which `input` lives on `parent`. - * @param args The endpoint args for this normalize call. - * @param visit Recursive visitor for nested schemas. - * @param delegate Store accessors for reading/writing entities. + * @param delegate Recursive visitor, endpoint args, and store accessors. * @param parentEntity Nearest enclosing entity-like schema (one with `pk`), * tracked automatically by the visit walker. `Scalar` * uses this to discover its entity binding. */ - normalize(input: any, parent: any, key: any, args: any[], visit: (...args: any) => any, delegate: { - getEntity: any; - setEntity: any; - }, parentEntity?: any): any; + normalize(input: any, parent: any, key: any, delegate: INormalizeDelegate, parentEntity?: any): any; denormalize(input: {}, delegate: IDenormalizeDelegate): T; queryKey(args: Args, unvisit: (...args: any) => any, delegate: { getEntity: any; @@ -272,14 +271,13 @@ interface EntityTable { * Schemas that recurse via `visit` should pass their own * `input` (or the surrounding container) here. * @param key The key under which `value` lives on `parent`. - * @param args The endpoint args for this normalize call. * * The walker internally tracks the nearest enclosing entity-like schema and * forwards it to `schema.normalize` as a trailing `parentEntity` argument — - * see `SchemaSimple.normalize`. Consumers of `visit` don't pass it. + * see `SchemaSimple.normalize`. */ interface Visit { - (schema: any, value: any, parent: any, key: any, args: readonly any[]): any; + (schema: any, value: any, parent: any, key: any): any; creating?: boolean; } /** Used in denormalize. Lookup to find an entity in the store table */ @@ -333,6 +331,10 @@ interface IDenormalizeDelegate { } /** Helpers during schema.normalize() */ interface INormalizeDelegate { + /** Recursive normalize of nested schemas */ + visit: Visit; + /** Raw endpoint args for this normalize call */ + readonly args: readonly any[]; /** Action meta-data for this normalize call */ readonly meta: { fetchedAt: number; @@ -445,7 +447,6 @@ interface ExtendableEndpointConstructor { new & E, params?: any, body?: any) => Promise, S extends Schema | undefined = undefined, M extends boolean | undefined = false, E extends Record = {}>(RestFetch: F, options?: Readonly> & E): EndpointInstanceInterface & E; readonly prototype: Function; } -type RemoveArray = Rem extends [any, ...infer RestRem] ? Orig extends [any, ...infer RestOrig] ? RemoveArray : never : Orig; declare let Endpoint: EndpointConstructor; @@ -496,7 +497,7 @@ interface IEntityClass { * @param [key] When normalizing, the key where this entity was found * @param [args] ...args sent to Endpoint */ - pk IEntityInstance & InstanceType) & IEntityClass & TBase>(this: T, value: Partial>, parent?: any, key?: string, args?: any[]): string | number | undefined; + pk IEntityInstance & ConstructorInstance) & IEntityClass & TBase>(this: T, value: Partial>, parent?: any, key?: string, args?: readonly any[]): string | number | undefined; /** Return true to merge incoming data; false keeps existing entity * * @see https://dataclient.io/docs/api/Entity#shouldUpdate @@ -557,22 +558,19 @@ interface IEntityClass { * * @param [props] Plain Object of properties to assign. */ - fromJS IEntityInstance & InstanceType) & IEntityClass & TBase>(this: T, props?: Partial>): AbstractInstanceType; + fromJS IEntityInstance & ConstructorInstance) & IEntityClass & TBase>(this: T, props?: Partial>): AbstractInstanceType; /** Called when denormalizing an entity to create an instance when 'valid' * * @param [props] Plain Object of properties to assign. * @see https://dataclient.io/rest/api/Entity#createIfValid */ - createIfValid IEntityInstance & InstanceType) & IEntityClass & TBase>(this: T, props: Partial>): AbstractInstanceType | undefined; + createIfValid IEntityInstance & ConstructorInstance) & IEntityClass & TBase>(this: T, props: Partial>): AbstractInstanceType | undefined; /** Do any transformations when first receiving input * * @see https://dataclient.io/rest/api/Entity#process */ process(input: any, parent: any, key: string | undefined, args: any[]): any; - normalize(input: any, parent: any, key: string | undefined, args: any[], visit: (...args: any) => any, snapshot: { - getEntity: any; - setEntity: any; - }): any; + normalize(input: any, parent: any, key: string | undefined, delegate: INormalizeDelegate): any; /** Do any transformations when first receiving input * * @see https://dataclient.io/rest/api/Entity#validate @@ -583,7 +581,7 @@ interface IEntityClass { * @see https://dataclient.io/rest/api/Entity#queryKey */ queryKey(args: readonly any[], unvisit: any, delegate: IQueryDelegate): any; - denormalize IEntityInstance & InstanceType) & IEntityClass & TBase>(this: T, input: any, delegate: IDenormalizeDelegate): AbstractInstanceType; + denormalize IEntityInstance & ConstructorInstance) & IEntityClass & TBase>(this: T, input: any, delegate: IDenormalizeDelegate): AbstractInstanceType; /** All instance defaults set */ readonly defaults: any; } @@ -598,6 +596,11 @@ interface IEntityInstance { pk(parent?: any, key?: string, args?: readonly any[]): string | number | undefined; } type Constructor = abstract new (...args: any[]) => {}; +/** InstanceType<> for abstract constructor types, which TypeScript 4.2's InstanceType rejects + * + * Unlike AbstractInstanceType<>, this resolves constructor type aliases (not just classes) + */ +type ConstructorInstance any> = T extends abstract new (...args: any) => infer R ? R : any; type IDClass = abstract new (...args: any[]) => { id: string | number | undefined; }; @@ -622,9 +625,9 @@ interface RequiredPKOptions extends EntityOptions(Base: TBase, opt?: EntityOptions>): IEntityClass & TBase; -declare function EntityMixin(Base: TBase, opt?: EntityOptions>): IEntityClass & TBase & (new (...args: any[]) => IEntityInstance); -declare function EntityMixin(Base: TBase, opt: RequiredPKOptions>): IEntityClass & TBase & (new (...args: any[]) => IEntityInstance); +declare function EntityMixin(Base: TBase, opt?: EntityOptions>): IEntityClass & TBase; +declare function EntityMixin(Base: TBase, opt?: EntityOptions>): IEntityClass & TBase & (new (...args: any[]) => IEntityInstance); +declare function EntityMixin(Base: TBase, opt: RequiredPKOptions>): IEntityClass & TBase & (new (...args: any[]) => IEntityInstance); declare class PolymorphicSchema { private _schemaAttribute; @@ -635,7 +638,9 @@ declare class PolymorphicSchema { getSchemaAttribute(input: any, parent: any, key: any): any; inferSchema(input: any, parent: any, key: any): any; schemaKey(): string; - normalizeValue(value: any, parent: any, key: any, args: any[], visit: Visit): any; + normalizeValue(value: any, parent: any, key: any, delegate: { + visit: Visit; + }): any; denormalizeValue(value: any, unvisit: any): any; } @@ -665,7 +670,7 @@ declare class Invalidate ? string | ((input: any, parent: any, key: any) => string) : undefined); get key(): string; - normalize(input: any, parent: any, key: string | undefined, args: any[], visit: (...args: any) => any, delegate: INormalizeDelegate): string | { + normalize(input: any, parent: any, key: string | undefined, delegate: INormalizeDelegate): string | { id: string; schema: string; }; @@ -711,7 +716,7 @@ declare class Lazy implements SchemaSimple { * @param {Schema} schema - The inner schema (e.g., [Building], Building, Collection) */ constructor(schema: S); - normalize(input: any, parent: any, key: any, args: any[], visit: (...args: any) => any, _delegate: any): any; + normalize(input: any, parent: any, key: any, delegate: any): any; denormalize(input: {}, _delegate: IDenormalizeDelegate): any; queryKey(_args: readonly any[], _unvisit: (...args: any) => any, _delegate: any): undefined; /** Queryable schema for use with useQuery() to resolve lazy relationships */ @@ -867,7 +872,7 @@ declare class Scalar implements Mergeable { date: number; expiresAt: number; }; - normalize(input: any, parent: any, key: any, args: any[], visit: Visit, delegate: INormalizeDelegate, parentEntity: any): any; + normalize(input: any, parent: any, key: any, delegate: INormalizeDelegate, parentEntity: any): any; denormalize(input: any, delegate: IDenormalizeDelegate): any; /** * Returns the cpks of cells matching the current lens, or undefined. @@ -952,7 +957,7 @@ interface CollectionInterface any, delegate: INormalizeDelegate, parentEntity?: any): string; + normalize(input: any, parent: Parent, key: string, delegate: INormalizeDelegate, parentEntity?: any): string; /** Creates new instance copying over defined values of arguments * * @see https://dataclient.io/docs/api/Collection#merge @@ -1000,7 +1005,7 @@ interface CollectionInterface any | undefined; - denormalize(input: any, args: readonly any[], unvisit: (schema: any, input: any) => any): ReturnType; + denormalize(input: any, delegate: IDenormalizeDelegate): ReturnType; _denormalizeNullable(): ReturnType; _normalizeNullable(): ReturnType; /** Schema to place at the *end* of this Collection @@ -1035,6 +1040,7 @@ interface CollectionConstructor { } type StrategyFunction = (value: any, parent: any, key: string) => T; type SchemaFunction = (value: Args, parent: any, key: string) => K; +type SchemaAttribute = K | SchemaFunction; type MergeFunction = (entityA: any, entityB: any) => any; type SchemaAttributeFunction = (value: any, parent: any, key: string) => S; type UnionResult = { @@ -1067,18 +1073,14 @@ declare class Array$1 implements SchemaClass { input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): (S extends EntityMap ? UnionResult : Normalize)[]; _normalizeNullable(): - | (S extends EntityMap ? UnionResult : Normalize)[] - | undefined; + (S extends EntityMap ? UnionResult : Normalize)[] | undefined; _denormalizeNullable(): - | (S extends EntityMap ? T : Denormalize)[] - | undefined; + (S extends EntityMap ? T : Denormalize)[] | undefined; denormalize( input: {}, @@ -1121,18 +1123,14 @@ declare class All< input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): (S extends EntityMap ? UnionResult : Normalize)[]; _normalizeNullable(): - | (S extends EntityMap ? UnionResult : Normalize)[] - | undefined; + (S extends EntityMap ? UnionResult : Normalize)[] | undefined; _denormalizeNullable(): - | (S extends EntityMap ? T : Denormalize)[] - | undefined; + (S extends EntityMap ? T : Denormalize)[] | undefined; denormalize( input: {}, @@ -1165,8 +1163,6 @@ declare class Object$1< input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): NormalizeObject; @@ -1236,8 +1232,8 @@ interface UnionConstructor { */ interface UnionInstance< Choices extends EntityMap = any, - Args extends EntityFields> = - EntityFields>, + // unconstrained: TypeScript 4.2-4.4 can't prove UnionConstructor's Args satisfy EntityFields + Args = EntityFields>, > { readonly _hoistable: true; define(definition: Schema): void; @@ -1249,16 +1245,13 @@ interface UnionInstance< input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): UnionResult; _normalizeNullable(): UnionResult | undefined; _denormalizeNullable(): - | AbstractInstanceType - | undefined; + AbstractInstanceType | undefined; denormalize( input: {}, @@ -1320,8 +1313,6 @@ declare class Values implements SchemaClass { input: any, parent: any, key: any, - args: any[], - visit: (...args: any) => any, delegate: INormalizeDelegate, ): Record< string, @@ -1401,6 +1392,7 @@ type schema_d_Query = SchemaAttribute; type schema_d_SchemaAttributeFunction = SchemaAttributeFunction; type schema_d_SchemaClass = SchemaClass; type schema_d_SchemaFunction = SchemaFunction; @@ -1410,15 +1402,14 @@ type schema_d_Union> = Union; declare const schema_d_Union: typeof Union; type schema_d_UnionConstructor = UnionConstructor; -type schema_d_UnionInstance> = - EntityFields>> = UnionInstance; +type schema_d_UnionInstance>> = UnionInstance; type schema_d_UnionResult = UnionResult; declare const schema_d_UnionRoot: typeof UnionRoot; type schema_d_Values = Values; declare const schema_d_Values: typeof Values; declare const schema_d_unshift: typeof unshift; declare namespace schema_d { - export { schema_d_All as All, Array$1 as Array, schema_d_Collection as Collection, type schema_d_CollectionArrayAdder as CollectionArrayAdder, type schema_d_CollectionArrayOrValuesAdder as CollectionArrayOrValuesAdder, type schema_d_CollectionConstructor as CollectionConstructor, type schema_d_CollectionFromSchema as CollectionFromSchema, type schema_d_CollectionInterface as CollectionInterface, schema_d_CollectionRoot as CollectionRoot, type schema_d_CollectionValuesAdder as CollectionValuesAdder, type schema_d_DefaultArgs as DefaultArgs, EntityMixin as Entity, type schema_d_EntityInterface as EntityInterface, type schema_d_EntityMap as EntityMap, schema_d_EntityMixin as EntityMixin, schema_d_Invalidate as Invalidate, schema_d_Lazy as Lazy, type schema_d_MergeFunction as MergeFunction, Object$1 as Object, schema_d_Query as Query, schema_d_Scalar as Scalar, type schema_d_SchemaAttributeFunction as SchemaAttributeFunction, type schema_d_SchemaClass as SchemaClass, type schema_d_SchemaFunction as SchemaFunction, type schema_d_StrategyFunction as StrategyFunction, schema_d_Union as Union, type schema_d_UnionConstructor as UnionConstructor, type schema_d_UnionInstance as UnionInstance, type schema_d_UnionResult as UnionResult, schema_d_UnionRoot as UnionRoot, schema_d_Values as Values, schema_d_unshift as unshift }; + export { schema_d_All as All, Array$1 as Array, schema_d_Collection as Collection, type schema_d_CollectionArrayAdder as CollectionArrayAdder, type schema_d_CollectionArrayOrValuesAdder as CollectionArrayOrValuesAdder, type schema_d_CollectionConstructor as CollectionConstructor, type schema_d_CollectionFromSchema as CollectionFromSchema, type schema_d_CollectionInterface as CollectionInterface, schema_d_CollectionRoot as CollectionRoot, type schema_d_CollectionValuesAdder as CollectionValuesAdder, type schema_d_DefaultArgs as DefaultArgs, EntityMixin as Entity, type schema_d_EntityInterface as EntityInterface, type schema_d_EntityMap as EntityMap, schema_d_EntityMixin as EntityMixin, schema_d_Invalidate as Invalidate, schema_d_Lazy as Lazy, type schema_d_MergeFunction as MergeFunction, Object$1 as Object, schema_d_Query as Query, schema_d_Scalar as Scalar, type schema_d_SchemaAttribute as SchemaAttribute, type schema_d_SchemaAttributeFunction as SchemaAttributeFunction, type schema_d_SchemaClass as SchemaClass, type schema_d_SchemaFunction as SchemaFunction, type schema_d_StrategyFunction as StrategyFunction, schema_d_Union as Union, type schema_d_UnionConstructor as UnionConstructor, type schema_d_UnionInstance as UnionInstance, type schema_d_UnionResult as UnionResult, schema_d_UnionRoot as UnionRoot, schema_d_Values as Values, schema_d_unshift as unshift }; } declare const Entity_base: IEntityClass { @@ -1455,7 +1446,9 @@ declare abstract class Entity extends Entity_base { * @param [key] When normalizing, the key where this entity was found * @param [args] ...args sent to Endpoint */ - static pk: (this: T, value: Partial>, parent?: any, key?: string, args?: any[]) => string | number | undefined; + static pk: { + pk(this: T, value: Partial>, parent?: any, key?: string, args?: readonly any[]): string | number | undefined; + }['pk']; /** Do any transformations when first receiving input * * @see https://dataclient.io/rest/api/Entity#process @@ -1630,7 +1623,7 @@ interface RestInstance : undefined : undefined; + } & O, Extract> : undefined : undefined; /** Create a new item (POST) and `push` to the end * @see https://dataclient.io/rest/api/RestEndpoint#push */ From e19689454fb85b81fb2f3111c0faf9a00c28c19a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 17:59:11 +0000 Subject: [PATCH 6/6] docs(blog): Move typed set() section under Other improvements Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BN4uSb49CL2pwcZFnTCJkc --- website/blog/2026-10-03-v0.19-batch-set.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/blog/2026-10-03-v0.19-batch-set.md b/website/blog/2026-10-03-v0.19-batch-set.md index 7d47d3699270..9b3f423822ec 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -109,6 +109,8 @@ is the Array schema, so match `action.schema[0]` for `[Ticker]` rather than the ::: +## Other improvements + ### Typed set() values {#typed-set} [Controller.set()](/docs/api/Controller#set) previously accepted any value for a schema, so a typo or a wrong @@ -173,8 +175,6 @@ against the combined fields of all its members, so a row mixing fields from diff If code that previously compiled now fails here, it was writing data its schema doesn't describe. Fix the value, or widen the Entity's field types if the data really can take that shape. -## Other improvements - ### Entity pk() args {#entity-pk-args} [Entity.pk()](/rest/api/Entity#pk) now types its `args` parameter as `readonly any[]`, so Entity classes can be passed