From 49c3cdb98f23981c025b719ce735918759bb98a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 02:43:30 +0000 Subject: [PATCH 1/7] fix(types): Apply newer legacy type overlays to older TS outputs endpoint and normalizr legacy outputs (ts4.2, ts4.0, ts3.4) now inherit every newer version's src-*-types overlay, as rest already did. This replaces the TS 5.4 NoInfer builtin with the NI = T fallback for TS < 4.8 consumers. normalizr re-exports memo types by name instead of `export type *` (TS 5.0 syntax that downlevel-dts leaves as is). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MpCgcZt8G8R5KARZ7z5CKE --- .changeset/legacy-ts-noinfer.md | 22 ++++++++++++++++++++++ .cursor/rules/ci-config.mdc | 2 +- packages/normalizr/src/index.ts | 7 ++++++- packages/rest/package.json | 2 +- scripts/build-legacy-types.sh | 27 +++++++++------------------ 5 files changed, 39 insertions(+), 21 deletions(-) create mode 100644 .changeset/legacy-ts-noinfer.md diff --git a/.changeset/legacy-ts-noinfer.md b/.changeset/legacy-ts-noinfer.md new file mode 100644 index 000000000000..d27ac8b61936 --- /dev/null +++ b/.changeset/legacy-ts-noinfer.md @@ -0,0 +1,22 @@ +--- +'@data-client/endpoint': patch +'@data-client/normalizr': patch +'@data-client/rest': patch +'@data-client/graphql': patch +'@data-client/react': patch +'@data-client/vue': patch +--- + +Fix types for TypeScript 4.x when `skipLibCheck` is off + +TypeScript 4.0 through 4.7 no longer report `Cannot find name 'NoInfer'` from `@data-client/endpoint` or `@data-client/normalizr`, and TypeScript 4.0 through 4.9 no longer report `Only named exports may use 'export type'` from `@data-client/normalizr`. + +```ts +import { Entity } from '@data-client/endpoint'; +import { normalize } from '@data-client/normalizr'; + +// Before (TypeScript 4.7, skipLibCheck: false): +// error TS2304: Cannot find name 'NoInfer'. +// error TS1383: Only named exports may use 'export type'. +// After: no errors +``` diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 44174b5e7a7b..937fdf532d2b 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -12,7 +12,7 @@ alwaysApply: false - Jobs halt via the `halt-unless-relevant-change` command based on flags computed once in `setup` (`.ci-esmodule-relevant`, `.ci-tests-relevant`) and transported via `save_cache`/`restore_cache` (keyed on `CIRCLE_SHA1`) so jobs can halt before paying `attach_workspace`. Missing/unreadable flags fail open (jobs run). On the default branch both flags are always true (a push may carry several commits). The diff uses `--no-renames` so moving a file out of a relevant dir still counts. - `esmodule` (validate-esmodule-browser-build, esmodule-types*): a denylist, so new paths fail open. False only when every changed path is provably outside the esmodule jobs' inputs: the shared `DOCS_ONLY` paths (also the `tests` denylist) plus `.vscode/`, `plans/`, root `__tests__/` (excluded by every `tsconfig.compile.json`), `eslint.config.mjs`, `jest.config.js`, `examples/*.md`, and examples the jobs never build (`benchmark`, `benchmark-react`, `coin-app`, `nextjs`, `normalizr-github`, `normalizr-redux`, `test-bundlesize`, `vue-todo-app`). Only add a path if no esmodule job (or the `setup` builds feeding them) reads it. - `tests` (lint, typecheck, unit_tests, node_matrix): false only when every changed path is docs/website/tooling (`website/`, `docs/`, `.changeset/`, `.cursor/`, `.agents/`, `.claude/`, `.github/`, root `*.md`), except `website/src/components/Playground/` (has unit tests). When both flags are false, `setup` halts before install. -- Legacy TS types (`ci:build:legacy-types`, consumed by `esmodule-types`) build inside `setup` (`ci:build:setup:esmodule`) only when the esmodule flag is set; there is no separate job, to keep a job hop off the critical path. In CI it builds only endpoint/normalizr/rest outputs for TS >= 4.0 (`LEGACY_MIN_TS=4.0`), since the oldest TS in the `esmodule-types` matrix is 4.0; release builds (`build:types`) still emit every version. `scripts/build-legacy-types.sh` builds each TS version concurrently; rest passes `--inherit-newer-overlays` so its `src-4.1-types` also apply to `ts4.0` (endpoint/normalizr don't pass it; each version gets only its own overlays). Any change to legacy types building must leave `ts*/` output byte-identical to master (diff it). +- Legacy TS types (`ci:build:legacy-types`, consumed by `esmodule-types`) build inside `setup` (`ci:build:setup:esmodule`) only when the esmodule flag is set; there is no separate job, to keep a job hop off the critical path. In CI it builds only endpoint/normalizr/rest outputs for TS >= 4.0 (`LEGACY_MIN_TS=4.0`), since the oldest TS in the `esmodule-types` matrix is 4.0; release builds (`build:types`) still emit every version. `scripts/build-legacy-types.sh` builds each TS version concurrently; each `ts/` gets the downleveled `lib`, then every newer version's `src-*-types` overlay, then its own. Any change to legacy types building must leave `ts*/` output byte-identical to master (diff it) unless it intentionally changes published types (then add a changeset). - Never `git fetch --depth` the base branch in the relevance check: a shallow fetch severs the merge base and the three-dot diff fails. - Changing root `package.json` `workspaces` requires updating the `setup` job's workspace trimming step. diff --git a/packages/normalizr/src/index.ts b/packages/normalizr/src/index.ts index 3eb4d4da9619..3c6ac4bce05f 100644 --- a/packages/normalizr/src/index.ts +++ b/packages/normalizr/src/index.ts @@ -19,7 +19,12 @@ export type { export type { NI } from './NoInfer.js'; export * from './endpoint/types.js'; export * from './interface.js'; -export type * from './memo/types.js'; +export type { + EntityCache, + EndpointsCache, + DenormGetEntity, + IMemoPolicy, +} from './memo/types.js'; export * from './Expiry.js'; export { INVALID } from './denormalize/symbol.js'; export { validateQueryKey } from './buildQueryKey.js'; diff --git a/packages/rest/package.json b/packages/rest/package.json index 63b837a0a850..5faf4e204233 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -123,7 +123,7 @@ "build:js:browser": "BROWSERSLIST_ENV=legacy yarn g:rollup", "build:bundle": "yarn g:runs build:js:\\* && echo '{\"type\":\"commonjs\"}' > dist/package.json", "build:clean": "yarn g:clean ts4.0 ts4.1 index.d.ts next.d.ts", - "build:legacy-types": "yarn g:legacy-types --inherit-newer-overlays 4.1 4.0", + "build:legacy-types": "yarn g:legacy-types 4.1 4.0", "build": "run build:lib && run build:legacy:lib && run build:bundle", "dev": "NODE_ENV=production BROWSERSLIST_ENV='2020' POLYFILL_TARGETS='chrome>88,safari>14' yarn g:babel --out-dir lib -w", "prepare": "run build:lib", diff --git a/scripts/build-legacy-types.sh b/scripts/build-legacy-types.sh index 89e972517a49..b54f85420c51 100755 --- a/scripts/build-legacy-types.sh +++ b/scripts/build-legacy-types.sh @@ -1,17 +1,9 @@ #!/bin/bash set -e -# --inherit-newer-overlays: custom types for a version also apply to every -# version listed after it. Without it each version gets only its own. -inherit=false -if [ "$1" = "--inherit-newer-overlays" ]; then - inherit=true - shift -fi - # Check if at least one directory is provided if [ $# -eq 0 ]; then - echo "Usage: $0 [--inherit-newer-overlays] [version2] ..." + echo "Usage: $0 [version2] ..." exit 1 fi @@ -28,19 +20,18 @@ copy_types() { done } -# Each output dir gets the downleveled lib, then (with --inherit-newer-overlays) -# earlier versions' custom types, then its own. Each output dir only depends on -# lib and src-*-types, so versions build concurrently. +# Versions are listed newest first, and custom types for a version also apply +# to every version listed after it. So each output dir gets the downleveled lib, +# then earlier versions' custom types, then its own. Each output dir only +# depends on lib and src-*-types, so versions build concurrently. build_version() { local version="$1" shift "$downlevel_dts" lib "ts$version" --to="$version" - if [ "$inherit" = true ]; then - for earlier in "$@" - do - copy_types "./src-$earlier-types" "./ts$version" - done - fi + for earlier in "$@" + do + copy_types "./src-$earlier-types" "./ts$version" + done copy_types "./src-$version-types" "./ts$version" } From d7eca37741fa7054651c7723a9a750f06e1d4338 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 02:43:56 +0000 Subject: [PATCH 2/7] docs(blog): Note legacy TypeScript types fix Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MpCgcZt8G8R5KARZ7z5CKE --- website/blog/2026-10-03-v0.19-batch-set.md | 1 + 1 file changed, 1 insertion(+) 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 2c0f61f114e9..d369ce495270 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)) +- Fix `Cannot find name 'NoInfer'` and `export type` errors on TypeScript 4.x with `skipLibCheck` off ([#4138](https://github.com/reactive/data-client/pull/4138)) {/* truncate */} From f4e9a0bb11f4f952f3de8c16dffc1cdf797fe353 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 03:00:12 +0000 Subject: [PATCH 3/7] fix(types): Fix endpoint/rest legacy type errors with skipLibCheck off - TS 4.0: unroll RemoveArray and approximate PartialArray (recursive conditional types need 4.1) in new src-4.0-types overlays - TS <4.2: rewrite `abstract new` to `new` in the downleveled output and drop the stale src-4.0-types Entity/EntityTypes copies - TS 4.2: ConstructorInstance<> replaces InstanceType<> on abstract TBase - TS 4.2-4.4: UnionInstance Args is unconstrained - TS 4.0-4.5: rest getPage narrows paginationField with Extract<> - CI: esmodule-types also typechecks a skipLibCheck: false consumer Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01AMEHZGboXdMFRkF4D2ktki --- .changeset/legacy-ts-endpoint-libcheck.md | 30 ++++ .circleci/config.yml | 2 + .cursor/rules/ci-config.mdc | 2 +- .../todo-app/tsconfig.typetest-libcheck.json | 15 ++ examples/todo-app/typetest-libcheck.ts | 44 +++++ .../endpoint/src-4.0-types/endpointTypes.d.ts | 170 ++++++++++++++++++ .../src-4.0-types/schemas/Entity.d.ts | 45 ----- .../src-4.0-types/schemas/EntityTypes.d.ts | 164 ----------------- packages/endpoint/src-4.0-types/utility.d.ts | 10 ++ packages/endpoint/src/schema.d.ts | 4 +- packages/endpoint/src/schemas/EntityMixin.ts | 9 +- packages/endpoint/src/schemas/EntityRecord.ts | 8 +- packages/endpoint/src/schemas/EntityTypes.ts | 11 +- packages/rest/src/RestEndpointTypes.ts | 3 +- scripts/build-legacy-types.sh | 12 +- website/blog/2026-10-03-v0.19-batch-set.md | 1 + 16 files changed, 305 insertions(+), 225 deletions(-) create mode 100644 .changeset/legacy-ts-endpoint-libcheck.md create mode 100644 examples/todo-app/tsconfig.typetest-libcheck.json create mode 100644 examples/todo-app/typetest-libcheck.ts create mode 100644 packages/endpoint/src-4.0-types/endpointTypes.d.ts delete mode 100644 packages/endpoint/src-4.0-types/schemas/Entity.d.ts delete mode 100644 packages/endpoint/src-4.0-types/schemas/EntityTypes.d.ts create mode 100644 packages/endpoint/src-4.0-types/utility.d.ts diff --git a/.changeset/legacy-ts-endpoint-libcheck.md b/.changeset/legacy-ts-endpoint-libcheck.md new file mode 100644 index 000000000000..6fde9e1a81a8 --- /dev/null +++ b/.changeset/legacy-ts-endpoint-libcheck.md @@ -0,0 +1,30 @@ +--- +'@data-client/endpoint': patch +'@data-client/rest': patch +'@data-client/graphql': patch +--- + +Fix more TypeScript 4.x errors when `skipLibCheck` is off + +[Entity](https://dataclient.io/rest/api/Entity), [Endpoint](https://dataclient.io/rest/api/Endpoint), [Union](https://dataclient.io/rest/api/Union) and [RestEndpoint](https://dataclient.io/rest/api/RestEndpoint) declarations no longer report errors on TypeScript 4.0 through 4.5. On TypeScript 4.0 and 4.1, an `Entity` can be an `Endpoint` schema again. + +```ts +import { Endpoint, Entity, schema } from '@data-client/endpoint'; + +class User extends Entity { + id = ''; + type = 'users'; +} +const getUser = new Endpoint( + (id: string) => fetch(`/users/${id}`).then(res => res.json()), + { schema: User }, +); +const feed = new schema.Union({ users: User }, 'type'); + +// Before (TypeScript 4.0, skipLibCheck: false): +// error TS2456: Type alias 'RemoveArray' circularly references itself. +// error TS2322: Type 'typeof User' is not assignable to type 'EntityInterface'. +// Before (TypeScript 4.2): +// error TS2344: Type 'TBase' does not satisfy the constraint 'new (...args: any) => any'. +// After: no errors +``` diff --git a/.circleci/config.yml b/.circleci/config.yml index 86d6393a13d2..b42b06379396 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -329,6 +329,8 @@ jobs: fi yarn run tsc --project examples/todo-app/tsconfig.typetest.json yarn run tsc --project examples/github-app/tsconfig.typetest.json + # skipLibCheck: false, so the legacy typesVersions outputs get checked too + yarn run tsc --project examples/todo-app/tsconfig.typetest-libcheck.json esmodule-types-latest: executor: node diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 937fdf532d2b..abe098fd0ea6 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -12,7 +12,7 @@ alwaysApply: false - Jobs halt via the `halt-unless-relevant-change` command based on flags computed once in `setup` (`.ci-esmodule-relevant`, `.ci-tests-relevant`) and transported via `save_cache`/`restore_cache` (keyed on `CIRCLE_SHA1`) so jobs can halt before paying `attach_workspace`. Missing/unreadable flags fail open (jobs run). On the default branch both flags are always true (a push may carry several commits). The diff uses `--no-renames` so moving a file out of a relevant dir still counts. - `esmodule` (validate-esmodule-browser-build, esmodule-types*): a denylist, so new paths fail open. False only when every changed path is provably outside the esmodule jobs' inputs: the shared `DOCS_ONLY` paths (also the `tests` denylist) plus `.vscode/`, `plans/`, root `__tests__/` (excluded by every `tsconfig.compile.json`), `eslint.config.mjs`, `jest.config.js`, `examples/*.md`, and examples the jobs never build (`benchmark`, `benchmark-react`, `coin-app`, `nextjs`, `normalizr-github`, `normalizr-redux`, `test-bundlesize`, `vue-todo-app`). Only add a path if no esmodule job (or the `setup` builds feeding them) reads it. - `tests` (lint, typecheck, unit_tests, node_matrix): false only when every changed path is docs/website/tooling (`website/`, `docs/`, `.changeset/`, `.cursor/`, `.agents/`, `.claude/`, `.github/`, root `*.md`), except `website/src/components/Playground/` (has unit tests). When both flags are false, `setup` halts before install. -- Legacy TS types (`ci:build:legacy-types`, consumed by `esmodule-types`) build inside `setup` (`ci:build:setup:esmodule`) only when the esmodule flag is set; there is no separate job, to keep a job hop off the critical path. In CI it builds only endpoint/normalizr/rest outputs for TS >= 4.0 (`LEGACY_MIN_TS=4.0`), since the oldest TS in the `esmodule-types` matrix is 4.0; release builds (`build:types`) still emit every version. `scripts/build-legacy-types.sh` builds each TS version concurrently; each `ts/` gets the downleveled `lib`, then every newer version's `src-*-types` overlay, then its own. Any change to legacy types building must leave `ts*/` output byte-identical to master (diff it) unless it intentionally changes published types (then add a changeset). +- Legacy TS types (`ci:build:legacy-types`, consumed by `esmodule-types`) build inside `setup` (`ci:build:setup:esmodule`) only when the esmodule flag is set; there is no separate job, to keep a job hop off the critical path. In CI it builds only endpoint/normalizr/rest outputs for TS >= 4.0 (`LEGACY_MIN_TS=4.0`), since the oldest TS in the `esmodule-types` matrix is 4.0; release builds (`build:types`) still emit every version. `scripts/build-legacy-types.sh` builds each TS version concurrently; each `ts/` gets the downleveled `lib` (with `abstract new` rewritten to `new` below 4.2, which `downlevel-dts` misses), then every newer version's `src-*-types` overlay, then its own. `esmodule-types` also runs `examples/todo-app/tsconfig.typetest-libcheck.json` (`skipLibCheck: false`, `types: []`) so errors inside the legacy outputs fail CI; the other typetests use `skipLibCheck: true`. Any change to legacy types building must leave `ts*/` output byte-identical to master (diff it) unless it intentionally changes published types (then add a changeset). - Never `git fetch --depth` the base branch in the relevance check: a shallow fetch severs the merge base and the three-dot diff fails. - Changing root `package.json` `workspaces` requires updating the `setup` job's workspace trimming step. diff --git a/examples/todo-app/tsconfig.typetest-libcheck.json b/examples/todo-app/tsconfig.typetest-libcheck.json new file mode 100644 index 000000000000..47dd263fe943 --- /dev/null +++ b/examples/todo-app/tsconfig.typetest-libcheck.json @@ -0,0 +1,15 @@ +{ + // Standalone so it type-checks only @data-client declarations (and their deps) + // on every TypeScript in the esmodule-types matrix. + "compilerOptions": { + "target": "es2020", + "module": "esnext", + "lib": ["dom", "esnext"], + "moduleResolution": "node", + "strict": true, + "skipLibCheck": false, + "types": [], + "noEmit": true + }, + "files": ["typetest-libcheck.ts"] +} diff --git a/examples/todo-app/typetest-libcheck.ts b/examples/todo-app/typetest-libcheck.ts new file mode 100644 index 000000000000..ff7a12505085 --- /dev/null +++ b/examples/todo-app/typetest-libcheck.ts @@ -0,0 +1,44 @@ +// Type-checks published declarations (including legacy typesVersions outputs) +// with skipLibCheck: false; see tsconfig.typetest-libcheck.json +import { Endpoint, Entity, schema } from '@data-client/endpoint'; +import { denormalize, MemoCache, normalize } from '@data-client/normalizr'; +import { resource, RestEndpoint } from '@data-client/rest'; + +class User extends Entity { + id = ''; + name = ''; + type = 'users'; +} +class Post extends Entity { + id = ''; + title = ''; + author = User.fromJS(); + static schema = { author: User }; +} + +const getUser = new Endpoint( + (id: string) => Promise.resolve({ id, name: 'a', type: 'users' }), + { schema: User }, +); +const twoArgs = new Endpoint((a: string, b: number) => + Promise.resolve({ id: a, title: String(b) }), +); +const bound = twoArgs.bind(null, 'x'); +bound(5); +// @ts-expect-error +bound('wrong'); + +const feed = new schema.Union({ users: User }, 'type'); +const PostResource = resource({ + path: '/posts/:id', + schema: Post, + paginationField: 'cursor', +}); +PostResource.getList.getPage({ cursor: 'a' }); +const search = new RestEndpoint({ path: '/search' }); + +const memo = new MemoCache(); +const { result, entities } = normalize(Post, { id: '1', title: 'hi' }); +denormalize(Post, result, entities); + +export { getUser, feed, search, memo }; diff --git a/packages/endpoint/src-4.0-types/endpointTypes.d.ts b/packages/endpoint/src-4.0-types/endpointTypes.d.ts new file mode 100644 index 000000000000..0703e0c611e1 --- /dev/null +++ b/packages/endpoint/src-4.0-types/endpointTypes.d.ts @@ -0,0 +1,170 @@ +// relaxed constraints on call,apply,bind *this* +// RemoveArray is unrolled (recursive conditional types need TypeScript 4.1) + +/* eslint-disable @typescript-eslint/ban-types */ +import type { EndpointInterface, Schema } from './interface.js'; +import type { + EndpointExtraOptions, + FetchFunction, + PartialParameters, +} from './types.js'; +export interface EndpointOptions< + F extends FetchFunction = FetchFunction, + S extends Schema | undefined = undefined, + M extends boolean | undefined = false, +> extends EndpointExtraOptions { + key?: (...args: Parameters) => string; + sideEffect?: M; + schema?: S; + [k: string]: any; +} +export interface EndpointExtendOptions< + F extends FetchFunction = FetchFunction, + S extends Schema | undefined = Schema | undefined, + M extends boolean | undefined = boolean | undefined, +> extends EndpointOptions { + fetch?: FetchFunction; +} +export type ParamFromFetch = + F extends (params: infer P, body?: any) => Promise ? P : never; +export type KeyofEndpointInstance = keyof EndpointInstance; +export type ExtendedEndpoint< + O extends EndpointExtendOptions, + E extends EndpointInstance< + FetchFunction, + Schema | undefined, + boolean | undefined + >, + F extends FetchFunction, +> = EndpointInstance< + 'fetch' extends keyof O ? Exclude : E['fetch'], + 'schema' extends keyof O ? O['schema'] : E['schema'], + 'sideEffect' extends keyof O ? O['sideEffect'] : E['sideEffect'] +> & + Omit & + Omit; +/** + * Defines an async data source. + * @see https://dataclient.io/docs/api/Endpoint + */ +export interface EndpointInstance< + F extends (...args: any) => Promise = FetchFunction, + S extends Schema | undefined = Schema | undefined, + M extends boolean | undefined = boolean | undefined, +> extends EndpointInstanceInterface { + extend< + E extends EndpointInstance< + (...args: any) => Promise, + Schema | undefined, + boolean | undefined + >, + O extends EndpointExtendOptions & + Partial>> & + Record, + >( + this: E, + options: Readonly, + ): ExtendedEndpoint; +} +/** + * Defines an async data source. + * @see https://dataclient.io/docs/api/Endpoint + */ +export interface EndpointInstanceInterface< + F extends FetchFunction = FetchFunction, + S extends Schema | undefined = Schema | undefined, + M extends boolean | undefined = boolean | undefined, +> extends EndpointInterface { + constructor: EndpointConstructor; + /** + * Calls the function, substituting the specified object for the this value of the function, and the specified array for the arguments of the function. + * @param thisArg The object to be used as the this object. + * @param argArray A set of arguments to be passed to the function. + */ + apply( + this: E, + thisArg: any, + argArray?: Parameters, + ): ReturnType; + /** + * Calls a method of an object, substituting another object for the current object. + * @param thisArg The object to be used as the current object. + * @param argArray A list of arguments to be passed to the method. + */ + call( + this: E, + thisArg: any, + ...argArray: Parameters + ): ReturnType; + /** + * For a given function, creates a bound function that has the same body as the original function. + * The this object of the bound function is associated with the specified object, and has the specified initial parameters. + * @param thisArg An object to which the this keyword can refer inside the new function. + * @param argArray A list of arguments to be passed to the new function. + */ + bind>( + this: E, + thisArg: any, + ...args: readonly [...P] + ): EndpointInstance< + (...args: readonly [...RemoveArray, P>]) => ReturnType, + S, + M + > & + Omit>; + /** Returns a string representation of a function. */ + toString(): string; + prototype: any; + readonly length: number; + arguments: any; + caller: F; + key(...args: Parameters): string; + readonly sideEffect: M; + readonly schema: S; + fetch: F; + /** @see https://dataclient.io/rest/api/Endpoint#testKey */ + testKey(key: string): boolean; +} +export interface EndpointConstructor { + new < + F extends ( + this: EndpointInstance & E, + params?: any, + body?: any, + ) => Promise, + S extends Schema | undefined = undefined, + M extends boolean | undefined = false, + E extends Record = {}, + >( + fetchFunction: F, + options?: EndpointOptions & E, + ): EndpointInstance & E; + readonly prototype: Function; +} +export interface ExtendableEndpointConstructor { + new < + F extends ( + this: EndpointInstanceInterface & 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 [] ? Orig + : Rem extends [any] ? + Orig extends [any, ...infer R] ? R : never + : Rem extends [any, any] ? + Orig extends [any, any, ...infer R] ? R : never + : Rem extends [any, any, any] ? + Orig extends [any, any, any, ...infer R] ? R : never + : any[]; +export {}; +//# sourceMappingURL=endpointTypes.d.ts.map diff --git a/packages/endpoint/src-4.0-types/schemas/Entity.d.ts b/packages/endpoint/src-4.0-types/schemas/Entity.d.ts deleted file mode 100644 index eb14bb91a7fe..000000000000 --- a/packages/endpoint/src-4.0-types/schemas/Entity.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -// we just removed instances of 'abstract new' -import { AbstractInstanceType } from '../normal.js'; -declare const Entity_base: import("./EntityTypes.js").IEntityClass { - pk(parent?: any, key?: string, args?: readonly any[]): string | number | undefined; -}> & (new (...args: any[]) => { - pk(parent?: any, key?: string, args?: readonly any[]): string | number | undefined; -}); -/** - * Entity defines a single (globally) unique object. - * @see https://dataclient.io/rest/api/Entity - */ -export default abstract class Entity extends Entity_base { - /** Control how automatic schema validation is handled - * - * `undefined`: Defaults - throw error in worst offense - * 'warn': only ever warn - * 'silent': Don't bother with processing at all - * - * Note: this only applies to non-nested members. - */ - protected static automaticValidation?: 'warn' | 'silent'; - /** Factory method to convert from Plain JS Objects. - * - * @see https://dataclient.io/rest/api/Entity#fromJS - * @param [props] Plain Object of properties to assign. - */ - static fromJS: (this: T, props?: Partial>) => AbstractInstanceType; - /** - * A unique identifier for each Entity - * - * @see https://dataclient.io/rest/api/Entity#pk - * @param [value] POJO of the entity or subset used - * @param [parent] When normalizing, the object which included the entity - * @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; - /** Do any transformations when first receiving input - * - * @see https://dataclient.io/rest/api/Entity#process - */ - static process(input: any, parent: any, key: string | undefined, args: any[]): any; - static denormalize: (this: T, input: any, args: readonly any[], unvisit: (schema: any, input: any) => any) => AbstractInstanceType; -} -export {}; diff --git a/packages/endpoint/src-4.0-types/schemas/EntityTypes.d.ts b/packages/endpoint/src-4.0-types/schemas/EntityTypes.d.ts deleted file mode 100644 index f5e6cb83d627..000000000000 --- a/packages/endpoint/src-4.0-types/schemas/EntityTypes.d.ts +++ /dev/null @@ -1,164 +0,0 @@ -// we just removed instances of 'new' -import type { Schema, IQueryDelegate } from '../interface.js'; -import { AbstractInstanceType } from '../normal.js'; -/** - * Entity defines a single (globally) unique object. - * @see https://dataclient.io/rest/api/EntityMixin - */ -export interface IEntityClass { - toJSON(): { - name: string; - schema: { - [k: string]: Schema; - }; - key: string; - }; - /** Defines nested entities - * - * @see https://dataclient.io/rest/api/Entity#schema - */ - schema: { - [k: string]: Schema; - }; - /** Returns the globally unique identifier for the static Entity - * - * @see https://dataclient.io/rest/api/Entity#key - */ - key: string; - /** Defines indexes to enable lookup by - * - * @see https://dataclient.io/rest/api/Entity#indexes - */ - indexes?: readonly string[] | undefined; - maxEntityDepth?: number | undefined; - /** - * A unique identifier for each Entity - * - * @see https://dataclient.io/rest/api/Entity#pk - * @param [value] POJO of the entity or subset used - * @param [parent] When normalizing, the object which included the entity - * @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; - /** Return true to merge incoming data; false keeps existing entity - * - * @see https://dataclient.io/docs/api/Entity#shouldUpdate - */ - shouldUpdate(existingMeta: { - date: number; - fetchedAt: number; - }, incomingMeta: { - date: number; - fetchedAt: number; - }, existing: any, incoming: any): boolean; - /** Determines the order of incoming entity vs entity already in store - * - * @see https://dataclient.io/docs/api/Entity#shouldReorder - * @returns true if incoming entity should be first argument of merge() - */ - shouldReorder(existingMeta: { - date: number; - fetchedAt: number; - }, incomingMeta: { - date: number; - fetchedAt: number; - }, existing: any, incoming: any): boolean; - /** Creates new instance copying over defined values of arguments - * - * @see https://dataclient.io/docs/api/Entity#merge - */ - merge(existing: any, incoming: any): any; - /** Run when an existing entity is found in the store - * - * @see https://dataclient.io/docs/api/Entity#mergeWithStore - */ - mergeWithStore(existingMeta: { - date: number; - fetchedAt: number; - }, incomingMeta: { - date: number; - fetchedAt: number; - }, existing: any, incoming: any): any; - /** Run when an existing entity is found in the store - * - * @see https://dataclient.io/docs/api/Entity#mergeMetaWithStore - */ - mergeMetaWithStore(existingMeta: { - expiresAt: number; - date: number; - fetchedAt: number; - }, incomingMeta: { - expiresAt: number; - date: number; - fetchedAt: number; - }, existing: any, incoming: any): { - expiresAt: number; - date: number; - fetchedAt: number; - }; - /** Factory method to convert from Plain JS Objects. - * - * @param [props] Plain Object of properties to assign. - */ - fromJS IEntityInstance & InstanceType) & 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; - /** 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; - /** Do any transformations when first receiving input - * - * @see https://dataclient.io/rest/api/Entity#validate - */ - validate(processedEntity: any): string | undefined; - /** Builds a key access the entity without endpoint results - * - * @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, args: readonly any[], unvisit: (schema: any, input: any) => any): AbstractInstanceType; - /** All instance defaults set */ - readonly defaults: any; -} -export interface IEntityInstance { - /** - * A unique identifier for each Entity - * - * @param [parent] When normalizing, the object which included the entity - * @param [key] When normalizing, the key where this entity was found - * @param [args] ...args sent to Endpoint - */ - pk(parent?: any, key?: string, args?: readonly any[]): string | number | undefined; -} -export type Constructor = new (...args: any[]) => {}; -export type IDClass = new (...args: any[]) => { - id: string | number | undefined; -}; -export type PKClass = new (...args: any[]) => { - pk(parent?: any, key?: string, args?: readonly any[]): string | number | undefined; -}; -type ValidSchemas = { - [k in keyof TInstance]?: Schema; -}; -export type EntityOptions = { - readonly schema?: ValidSchemas; - readonly pk?: ((value: TInstance, parent?: any, key?: string) => string | number | undefined) | keyof TInstance; - readonly key?: string; -} & { - readonly [K in Extract]?: IEntityClass TInstance>[K]; -}; -export interface RequiredPKOptions extends EntityOptions { - readonly pk: ((value: TInstance, parent?: any, key?: string) => string | number | undefined) | keyof TInstance; -} -export {}; diff --git a/packages/endpoint/src-4.0-types/utility.d.ts b/packages/endpoint/src-4.0-types/utility.d.ts new file mode 100644 index 000000000000..3104c5ccb622 --- /dev/null +++ b/packages/endpoint/src-4.0-types/utility.d.ts @@ -0,0 +1,10 @@ +// PartialArray is recursive, which needs TypeScript 4.1; this approximates it with Partial +/** Get the Params type for a given Shape */ +export 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 */ +export type ResolveType any> = ReturnType extends Promise ? R : never; +export type PartialArray = A extends any[] ? Partial : never; +export type PartialParameters any> = T extends (...args: infer P) => any ? Partial

: never; +export type EndpointToFunction Promise> = (this: E, ...args: Parameters) => ReturnType; diff --git a/packages/endpoint/src/schema.d.ts b/packages/endpoint/src/schema.d.ts index 65cbc15ca970..0ea1c195ff23 100644 --- a/packages/endpoint/src/schema.d.ts +++ b/packages/endpoint/src/schema.d.ts @@ -228,8 +228,8 @@ export interface UnionConstructor { */ export 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; diff --git a/packages/endpoint/src/schemas/EntityMixin.ts b/packages/endpoint/src/schemas/EntityMixin.ts index b28fd9d3bdc1..6944f9ed89b9 100644 --- a/packages/endpoint/src/schemas/EntityMixin.ts +++ b/packages/endpoint/src/schemas/EntityMixin.ts @@ -13,6 +13,7 @@ import type { RequiredPKOptions, IDClass, Constructor, + ConstructorInstance, PKClass, } from './EntityTypes.js'; @@ -22,24 +23,24 @@ import type { */ export default function EntityMixin( Base: TBase, - opt?: EntityOptions>, + opt?: EntityOptions>, ): IEntityClass & TBase; // id is in Instance, so we default to that as pk export default function EntityMixin( Base: TBase, - opt?: EntityOptions>, + opt?: EntityOptions>, ): IEntityClass & TBase & (new (...args: any[]) => IEntityInstance); // pk was specified in options, so we don't need to redefine export default function EntityMixin( Base: TBase, - opt: RequiredPKOptions>, + opt: RequiredPKOptions>, ): IEntityClass & TBase & (new (...args: any[]) => IEntityInstance); export default function EntityMixin( Base: TBase, - options: EntityOptions> = {}, + options: EntityOptions> = {}, ) { /** * Entity defines a single (globally) unique object. diff --git a/packages/endpoint/src/schemas/EntityRecord.ts b/packages/endpoint/src/schemas/EntityRecord.ts index 43bb81f79eaa..fb646451a62b 100644 --- a/packages/endpoint/src/schemas/EntityRecord.ts +++ b/packages/endpoint/src/schemas/EntityRecord.ts @@ -1,6 +1,10 @@ -import { Constructor, EntityOptions } from './EntityTypes.js'; +import { + Constructor, + ConstructorInstance, + EntityOptions, +} from './EntityTypes.js'; export default function EntityRecord( Base: TBase, - options: EntityOptions> = {}, + options: EntityOptions> = {}, ) {} diff --git a/packages/endpoint/src/schemas/EntityTypes.ts b/packages/endpoint/src/schemas/EntityTypes.ts index 592a71a7fc91..48e01f21fb88 100644 --- a/packages/endpoint/src/schemas/EntityTypes.ts +++ b/packages/endpoint/src/schemas/EntityTypes.ts @@ -53,7 +53,7 @@ export interface IEntityClass { pk< T extends (abstract new ( ...args: any[] - ) => IEntityInstance & InstanceType) & + ) => IEntityInstance & ConstructorInstance) & IEntityClass & TBase, >( @@ -136,7 +136,7 @@ export interface IEntityClass { fromJS< T extends (abstract new ( ...args: any[] - ) => IEntityInstance & InstanceType) & + ) => IEntityInstance & ConstructorInstance) & IEntityClass & TBase, >( @@ -151,7 +151,7 @@ export interface IEntityClass { createIfValid< T extends (abstract new ( ...args: any[] - ) => IEntityInstance & InstanceType) & + ) => IEntityInstance & ConstructorInstance) & IEntityClass & TBase, >( @@ -182,7 +182,7 @@ export interface IEntityClass { denormalize< T extends (abstract new ( ...args: any[] - ) => IEntityInstance & InstanceType) & + ) => IEntityInstance & ConstructorInstance) & IEntityClass & TBase, >( @@ -209,6 +209,9 @@ export interface IEntityInstance { } export type Constructor = abstract new (...args: any[]) => {}; +/** InstanceType<> that accepts abstract constructors on TypeScript 4.2 (whose InstanceType needs `new`) */ +export type ConstructorInstance any> = + T extends abstract new (...args: any) => infer R ? R : any; export type IDClass = abstract new (...args: any[]) => { id: string | number | undefined; }; diff --git a/packages/rest/src/RestEndpointTypes.ts b/packages/rest/src/RestEndpointTypes.ts index 7e0e163a1503..e1efe25e1ff9 100644 --- a/packages/rest/src/RestEndpointTypes.ts +++ b/packages/rest/src/RestEndpointTypes.ts @@ -166,7 +166,8 @@ export interface RestInstance< O['paginationField'] extends string ? PaginationFieldEndpoint< F & { schema: S; sideEffect: M } & O, - O['paginationField'] + // TypeScript <4.6 doesn't narrow O['paginationField'] here + Extract > : undefined : undefined; diff --git a/scripts/build-legacy-types.sh b/scripts/build-legacy-types.sh index b54f85420c51..dd447856702d 100755 --- a/scripts/build-legacy-types.sh +++ b/scripts/build-legacy-types.sh @@ -20,6 +20,11 @@ copy_types() { done } +# True when version $1 is older than version $2. +version_lt() { + [ "$1" != "$2" ] && [ "$(printf '%s\n' "$1" "$2" | sort -V | head -1)" = "$1" ] +} + # Versions are listed newest first, and custom types for a version also apply # to every version listed after it. So each output dir gets the downleveled lib, # then earlier versions' custom types, then its own. Each output dir only @@ -28,6 +33,10 @@ build_version() { local version="$1" shift "$downlevel_dts" lib "ts$version" --to="$version" + # downlevel-dts keeps `abstract new` constructor types, which need TS 4.2 + if version_lt "$version" 4.2; then + find "ts$version" -name '*.d.ts' -exec perl -pi -e 's/abstract new \(/new (/g' {} + + fi for earlier in "$@" do copy_types "./src-$earlier-types" "./ts$version" @@ -37,8 +46,7 @@ build_version() { # LEGACY_MIN_TS skips outputs no consumer reads (CI's oldest tested TS). below_min() { - [ -n "$LEGACY_MIN_TS" ] && [ "$1" != "$LEGACY_MIN_TS" ] \ - && [ "$(printf '%s\n' "$1" "$LEGACY_MIN_TS" | sort -V | head -1)" = "$1" ] + [ -n "$LEGACY_MIN_TS" ] && version_lt "$1" "$LEGACY_MIN_TS" } pids=() 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 d369ce495270..e918f5465345 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -17,6 +17,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)) - Fix `Cannot find name 'NoInfer'` and `export type` errors on TypeScript 4.x with `skipLibCheck` off ([#4138](https://github.com/reactive/data-client/pull/4138)) +- Fix `Entity`, `Endpoint`, `Union` and `RestEndpoint` type errors on TypeScript 4.0–4.5 with `skipLibCheck` off ([#PRNUM](https://github.com/reactive/data-client/pull/PRNUM)) {/* truncate */} From ee358aec2f4214184d4581e23b85687be5de1e71 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 03:00:39 +0000 Subject: [PATCH 4/7] docs(blog): Link #4140 in v0.19 notes Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01AMEHZGboXdMFRkF4D2ktki --- website/blog/2026-10-03-v0.19-batch-set.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e918f5465345..2baea5e94e09 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -17,7 +17,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)) - Fix `Cannot find name 'NoInfer'` and `export type` errors on TypeScript 4.x with `skipLibCheck` off ([#4138](https://github.com/reactive/data-client/pull/4138)) -- Fix `Entity`, `Endpoint`, `Union` and `RestEndpoint` type errors on TypeScript 4.0–4.5 with `skipLibCheck` off ([#PRNUM](https://github.com/reactive/data-client/pull/PRNUM)) +- Fix `Entity`, `Endpoint`, `Union` and `RestEndpoint` type errors on TypeScript 4.0–4.5 with `skipLibCheck` off ([#4140](https://github.com/reactive/data-client/pull/4140)) {/* truncate */} From 4c22d7358a799ecf2f81ed7ee56d2b52256498b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 03:20:47 +0000 Subject: [PATCH 5/7] refactor(types): Isolate recursive tuple types so the 4.0 overlay is one small file - RemoveArray and PartialArray move to tupleTypes.ts; the src-4.0-types overlay replaces only that module instead of copying endpointTypes/utility - Legacy build only rewrites files that contain `abstract new (` - libcheck typetest skips TypeScript's own lib checks - Split the legacy types bullet in ci-config.mdc Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01AMEHZGboXdMFRkF4D2ktki --- .cursor/rules/ci-config.mdc | 7 +- .../todo-app/tsconfig.typetest-libcheck.json | 1 + .../endpoint/src-4.0-types/endpointTypes.d.ts | 170 ------------------ .../endpoint/src-4.0-types/tupleTypes.d.ts | 14 ++ packages/endpoint/src-4.0-types/utility.d.ts | 10 -- .../endpoint/src-4.2-types/endpointTypes.d.ts | 7 +- packages/endpoint/src/endpointTypes.ts | 8 +- packages/endpoint/src/schemas/EntityTypes.ts | 5 +- packages/endpoint/src/tupleTypes.ts | 16 ++ packages/endpoint/src/utility.ts | 7 +- scripts/build-legacy-types.sh | 5 +- 11 files changed, 48 insertions(+), 202 deletions(-) delete mode 100644 packages/endpoint/src-4.0-types/endpointTypes.d.ts create mode 100644 packages/endpoint/src-4.0-types/tupleTypes.d.ts delete mode 100644 packages/endpoint/src-4.0-types/utility.d.ts create mode 100644 packages/endpoint/src/tupleTypes.ts diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index abe098fd0ea6..2e315573a044 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -12,7 +12,12 @@ alwaysApply: false - Jobs halt via the `halt-unless-relevant-change` command based on flags computed once in `setup` (`.ci-esmodule-relevant`, `.ci-tests-relevant`) and transported via `save_cache`/`restore_cache` (keyed on `CIRCLE_SHA1`) so jobs can halt before paying `attach_workspace`. Missing/unreadable flags fail open (jobs run). On the default branch both flags are always true (a push may carry several commits). The diff uses `--no-renames` so moving a file out of a relevant dir still counts. - `esmodule` (validate-esmodule-browser-build, esmodule-types*): a denylist, so new paths fail open. False only when every changed path is provably outside the esmodule jobs' inputs: the shared `DOCS_ONLY` paths (also the `tests` denylist) plus `.vscode/`, `plans/`, root `__tests__/` (excluded by every `tsconfig.compile.json`), `eslint.config.mjs`, `jest.config.js`, `examples/*.md`, and examples the jobs never build (`benchmark`, `benchmark-react`, `coin-app`, `nextjs`, `normalizr-github`, `normalizr-redux`, `test-bundlesize`, `vue-todo-app`). Only add a path if no esmodule job (or the `setup` builds feeding them) reads it. - `tests` (lint, typecheck, unit_tests, node_matrix): false only when every changed path is docs/website/tooling (`website/`, `docs/`, `.changeset/`, `.cursor/`, `.agents/`, `.claude/`, `.github/`, root `*.md`), except `website/src/components/Playground/` (has unit tests). When both flags are false, `setup` halts before install. -- Legacy TS types (`ci:build:legacy-types`, consumed by `esmodule-types`) build inside `setup` (`ci:build:setup:esmodule`) only when the esmodule flag is set; there is no separate job, to keep a job hop off the critical path. In CI it builds only endpoint/normalizr/rest outputs for TS >= 4.0 (`LEGACY_MIN_TS=4.0`), since the oldest TS in the `esmodule-types` matrix is 4.0; release builds (`build:types`) still emit every version. `scripts/build-legacy-types.sh` builds each TS version concurrently; each `ts/` gets the downleveled `lib` (with `abstract new` rewritten to `new` below 4.2, which `downlevel-dts` misses), then every newer version's `src-*-types` overlay, then its own. `esmodule-types` also runs `examples/todo-app/tsconfig.typetest-libcheck.json` (`skipLibCheck: false`, `types: []`) so errors inside the legacy outputs fail CI; the other typetests use `skipLibCheck: true`. Any change to legacy types building must leave `ts*/` output byte-identical to master (diff it) unless it intentionally changes published types (then add a changeset). +- Legacy TS types (`ci:build:legacy-types`, consumed by `esmodule-types`): + - Built inside `setup` (`ci:build:setup:esmodule`) only when the esmodule flag is set; there is no separate job, to keep a job hop off the critical path. + - In CI only endpoint/normalizr/rest outputs for TS >= 4.0 are built (`LEGACY_MIN_TS=4.0`, the oldest TS in the `esmodule-types` matrix); release builds (`build:types`) still emit every version. + - `scripts/build-legacy-types.sh` builds each TS version concurrently; each `ts/` gets the downleveled `lib` (with `abstract new` rewritten to `new` below 4.2, which `downlevel-dts` misses), then every newer version's `src-*-types` overlay, then its own. Keep overlays to small single-purpose modules (like `NoInfer.ts`, `tupleTypes.ts`) so whole-file copies can't go stale. + - `esmodule-types` also runs `examples/todo-app/tsconfig.typetest-libcheck.json` (`skipLibCheck: false`, `types: []`) so errors inside the legacy outputs fail CI; the other typetests use `skipLibCheck: true`. + - Any change to legacy types building must leave `ts*/` output byte-identical to master (diff it) unless it intentionally changes published types (then add a changeset). - Never `git fetch --depth` the base branch in the relevance check: a shallow fetch severs the merge base and the three-dot diff fails. - Changing root `package.json` `workspaces` requires updating the `setup` job's workspace trimming step. diff --git a/examples/todo-app/tsconfig.typetest-libcheck.json b/examples/todo-app/tsconfig.typetest-libcheck.json index 47dd263fe943..3104ec65ec61 100644 --- a/examples/todo-app/tsconfig.typetest-libcheck.json +++ b/examples/todo-app/tsconfig.typetest-libcheck.json @@ -8,6 +8,7 @@ "moduleResolution": "node", "strict": true, "skipLibCheck": false, + "skipDefaultLibCheck": true, "types": [], "noEmit": true }, diff --git a/packages/endpoint/src-4.0-types/endpointTypes.d.ts b/packages/endpoint/src-4.0-types/endpointTypes.d.ts deleted file mode 100644 index 0703e0c611e1..000000000000 --- a/packages/endpoint/src-4.0-types/endpointTypes.d.ts +++ /dev/null @@ -1,170 +0,0 @@ -// relaxed constraints on call,apply,bind *this* -// RemoveArray is unrolled (recursive conditional types need TypeScript 4.1) - -/* eslint-disable @typescript-eslint/ban-types */ -import type { EndpointInterface, Schema } from './interface.js'; -import type { - EndpointExtraOptions, - FetchFunction, - PartialParameters, -} from './types.js'; -export interface EndpointOptions< - F extends FetchFunction = FetchFunction, - S extends Schema | undefined = undefined, - M extends boolean | undefined = false, -> extends EndpointExtraOptions { - key?: (...args: Parameters) => string; - sideEffect?: M; - schema?: S; - [k: string]: any; -} -export interface EndpointExtendOptions< - F extends FetchFunction = FetchFunction, - S extends Schema | undefined = Schema | undefined, - M extends boolean | undefined = boolean | undefined, -> extends EndpointOptions { - fetch?: FetchFunction; -} -export type ParamFromFetch = - F extends (params: infer P, body?: any) => Promise ? P : never; -export type KeyofEndpointInstance = keyof EndpointInstance; -export type ExtendedEndpoint< - O extends EndpointExtendOptions, - E extends EndpointInstance< - FetchFunction, - Schema | undefined, - boolean | undefined - >, - F extends FetchFunction, -> = EndpointInstance< - 'fetch' extends keyof O ? Exclude : E['fetch'], - 'schema' extends keyof O ? O['schema'] : E['schema'], - 'sideEffect' extends keyof O ? O['sideEffect'] : E['sideEffect'] -> & - Omit & - Omit; -/** - * Defines an async data source. - * @see https://dataclient.io/docs/api/Endpoint - */ -export interface EndpointInstance< - F extends (...args: any) => Promise = FetchFunction, - S extends Schema | undefined = Schema | undefined, - M extends boolean | undefined = boolean | undefined, -> extends EndpointInstanceInterface { - extend< - E extends EndpointInstance< - (...args: any) => Promise, - Schema | undefined, - boolean | undefined - >, - O extends EndpointExtendOptions & - Partial>> & - Record, - >( - this: E, - options: Readonly, - ): ExtendedEndpoint; -} -/** - * Defines an async data source. - * @see https://dataclient.io/docs/api/Endpoint - */ -export interface EndpointInstanceInterface< - F extends FetchFunction = FetchFunction, - S extends Schema | undefined = Schema | undefined, - M extends boolean | undefined = boolean | undefined, -> extends EndpointInterface { - constructor: EndpointConstructor; - /** - * Calls the function, substituting the specified object for the this value of the function, and the specified array for the arguments of the function. - * @param thisArg The object to be used as the this object. - * @param argArray A set of arguments to be passed to the function. - */ - apply( - this: E, - thisArg: any, - argArray?: Parameters, - ): ReturnType; - /** - * Calls a method of an object, substituting another object for the current object. - * @param thisArg The object to be used as the current object. - * @param argArray A list of arguments to be passed to the method. - */ - call( - this: E, - thisArg: any, - ...argArray: Parameters - ): ReturnType; - /** - * For a given function, creates a bound function that has the same body as the original function. - * The this object of the bound function is associated with the specified object, and has the specified initial parameters. - * @param thisArg An object to which the this keyword can refer inside the new function. - * @param argArray A list of arguments to be passed to the new function. - */ - bind>( - this: E, - thisArg: any, - ...args: readonly [...P] - ): EndpointInstance< - (...args: readonly [...RemoveArray, P>]) => ReturnType, - S, - M - > & - Omit>; - /** Returns a string representation of a function. */ - toString(): string; - prototype: any; - readonly length: number; - arguments: any; - caller: F; - key(...args: Parameters): string; - readonly sideEffect: M; - readonly schema: S; - fetch: F; - /** @see https://dataclient.io/rest/api/Endpoint#testKey */ - testKey(key: string): boolean; -} -export interface EndpointConstructor { - new < - F extends ( - this: EndpointInstance & E, - params?: any, - body?: any, - ) => Promise, - S extends Schema | undefined = undefined, - M extends boolean | undefined = false, - E extends Record = {}, - >( - fetchFunction: F, - options?: EndpointOptions & E, - ): EndpointInstance & E; - readonly prototype: Function; -} -export interface ExtendableEndpointConstructor { - new < - F extends ( - this: EndpointInstanceInterface & 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 [] ? Orig - : Rem extends [any] ? - Orig extends [any, ...infer R] ? R : never - : Rem extends [any, any] ? - Orig extends [any, any, ...infer R] ? R : never - : Rem extends [any, any, any] ? - Orig extends [any, any, any, ...infer R] ? R : never - : any[]; -export {}; -//# sourceMappingURL=endpointTypes.d.ts.map diff --git a/packages/endpoint/src-4.0-types/tupleTypes.d.ts b/packages/endpoint/src-4.0-types/tupleTypes.d.ts new file mode 100644 index 000000000000..0f74f0cdd61d --- /dev/null +++ b/packages/endpoint/src-4.0-types/tupleTypes.d.ts @@ -0,0 +1,14 @@ +// Recursive conditional types need TypeScript 4.1, so these are approximated + +export type PartialArray = A extends any[] ? Partial : never; + +/** Removes Rem's length worth of leading elements from Orig (up to 3) */ +export type RemoveArray = + Rem extends [] ? Orig + : Rem extends [any] ? + Orig extends [any, ...infer R] ? R : never + : Rem extends [any, any] ? + Orig extends [any, any, ...infer R] ? R : never + : Rem extends [any, any, any] ? + Orig extends [any, any, any, ...infer R] ? R : never + : any[]; diff --git a/packages/endpoint/src-4.0-types/utility.d.ts b/packages/endpoint/src-4.0-types/utility.d.ts deleted file mode 100644 index 3104c5ccb622..000000000000 --- a/packages/endpoint/src-4.0-types/utility.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// PartialArray is recursive, which needs TypeScript 4.1; this approximates it with Partial -/** Get the Params type for a given Shape */ -export 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 */ -export type ResolveType any> = ReturnType extends Promise ? R : never; -export type PartialArray = A extends any[] ? Partial : never; -export type PartialParameters any> = T extends (...args: infer P) => any ? Partial

: never; -export type EndpointToFunction Promise> = (this: E, ...args: Parameters) => ReturnType; diff --git a/packages/endpoint/src-4.2-types/endpointTypes.d.ts b/packages/endpoint/src-4.2-types/endpointTypes.d.ts index a2ed418c4898..d697ee2a4c26 100644 --- a/packages/endpoint/src-4.2-types/endpointTypes.d.ts +++ b/packages/endpoint/src-4.2-types/endpointTypes.d.ts @@ -7,6 +7,7 @@ import type { FetchFunction, PartialParameters, } from './types.js'; +import type { RemoveArray } from './tupleTypes.js'; export interface EndpointOptions< F extends FetchFunction = FetchFunction, S extends Schema | undefined = undefined, @@ -156,11 +157,5 @@ export interface ExtendableEndpointConstructor { ): EndpointInstanceInterface & E; readonly prototype: Function; } -type RemoveArray = - Rem extends [any, ...infer RestRem] ? - Orig extends [any, ...infer RestOrig] ? - RemoveArray - : never - : Orig; export {}; //# sourceMappingURL=endpointTypes.d.ts.map diff --git a/packages/endpoint/src/endpointTypes.ts b/packages/endpoint/src/endpointTypes.ts index ee90dcd47b1f..1c7fe5a67a64 100644 --- a/packages/endpoint/src/endpointTypes.ts +++ b/packages/endpoint/src/endpointTypes.ts @@ -5,6 +5,7 @@ import type { FetchFunction, PartialParameters, } from './types.js'; +import type { RemoveArray } from './tupleTypes.js'; export interface EndpointOptions< F extends FetchFunction = FetchFunction, @@ -191,10 +192,3 @@ type IfAny = 0 extends 1 & T ? Y : N; type IfTypeScriptLooseNull = 1 | undefined extends 1 ? Y : N; type OnlyFirst = A extends [] ? [] : [A[0]]; - -type RemoveArray = - Rem extends [any, ...infer RestRem] ? - Orig extends [any, ...infer RestOrig] ? - RemoveArray - : never - : Orig; diff --git a/packages/endpoint/src/schemas/EntityTypes.ts b/packages/endpoint/src/schemas/EntityTypes.ts index 48e01f21fb88..89187468b3a3 100644 --- a/packages/endpoint/src/schemas/EntityTypes.ts +++ b/packages/endpoint/src/schemas/EntityTypes.ts @@ -209,7 +209,10 @@ export interface IEntityInstance { } export type Constructor = abstract new (...args: any[]) => {}; -/** InstanceType<> that accepts abstract constructors on TypeScript 4.2 (whose InstanceType needs `new`) */ +/** InstanceType<> for abstract constructor types, which TypeScript 4.2's InstanceType rejects + * + * Unlike AbstractInstanceType<>, this resolves constructor type aliases (not just classes) + */ export type ConstructorInstance any> = T extends abstract new (...args: any) => infer R ? R : any; export type IDClass = abstract new (...args: any[]) => { diff --git a/packages/endpoint/src/tupleTypes.ts b/packages/endpoint/src/tupleTypes.ts new file mode 100644 index 000000000000..fe387f31dff9 --- /dev/null +++ b/packages/endpoint/src/tupleTypes.ts @@ -0,0 +1,16 @@ +// Recursive conditional types need TypeScript 4.1; src-4.0-types overlays this file + +export type PartialArray = + A extends [] ? [] + : A extends [infer F] ? [F] | [] + : A extends [infer F, ...infer Rest] ? [F] | [F, ...PartialArray] + : A extends (infer T)[] ? T[] + : never; + +/** Removes Rem's length worth of leading elements from Orig */ +export type RemoveArray = + Rem extends [any, ...infer RestRem] ? + Orig extends [any, ...infer RestOrig] ? + RemoveArray + : never + : Orig; diff --git a/packages/endpoint/src/utility.ts b/packages/endpoint/src/utility.ts index 3dc9a49b10c5..9f73d47e6849 100644 --- a/packages/endpoint/src/utility.ts +++ b/packages/endpoint/src/utility.ts @@ -8,12 +8,7 @@ export type EndpointParam = export type ResolveType any> = ReturnType extends Promise ? R : never; -export type PartialArray = - A extends [] ? [] - : A extends [infer F] ? [F] | [] - : A extends [infer F, ...infer Rest] ? [F] | [F, ...PartialArray] - : A extends (infer T)[] ? T[] - : never; +export type { PartialArray } from './tupleTypes.js'; // workaround for https://github.com/microsoft/TypeScript/issues/29919 export type PartialParameters any> = diff --git a/scripts/build-legacy-types.sh b/scripts/build-legacy-types.sh index dd447856702d..b20fd0596dd2 100755 --- a/scripts/build-legacy-types.sh +++ b/scripts/build-legacy-types.sh @@ -35,7 +35,10 @@ build_version() { "$downlevel_dts" lib "ts$version" --to="$version" # downlevel-dts keeps `abstract new` constructor types, which need TS 4.2 if version_lt "$version" 4.2; then - find "ts$version" -name '*.d.ts' -exec perl -pi -e 's/abstract new \(/new (/g' {} + + grep -rl --include='*.d.ts' 'abstract new (' "ts$version" | while IFS= read -r file + do + perl -pi -e 's/abstract new \(/new (/g' "$file" + done fi for earlier in "$@" do From c79765ed36828fbdcaf28c9ff5e38a75e4041d65 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 03:21:34 +0000 Subject: [PATCH 6/7] internal: Resolve leftover merge conflict in ci-config rule Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01AMEHZGboXdMFRkF4D2ktki --- .cursor/rules/ci-config.mdc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 2b1d86b62457..2e315573a044 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -12,16 +12,12 @@ alwaysApply: false - Jobs halt via the `halt-unless-relevant-change` command based on flags computed once in `setup` (`.ci-esmodule-relevant`, `.ci-tests-relevant`) and transported via `save_cache`/`restore_cache` (keyed on `CIRCLE_SHA1`) so jobs can halt before paying `attach_workspace`. Missing/unreadable flags fail open (jobs run). On the default branch both flags are always true (a push may carry several commits). The diff uses `--no-renames` so moving a file out of a relevant dir still counts. - `esmodule` (validate-esmodule-browser-build, esmodule-types*): a denylist, so new paths fail open. False only when every changed path is provably outside the esmodule jobs' inputs: the shared `DOCS_ONLY` paths (also the `tests` denylist) plus `.vscode/`, `plans/`, root `__tests__/` (excluded by every `tsconfig.compile.json`), `eslint.config.mjs`, `jest.config.js`, `examples/*.md`, and examples the jobs never build (`benchmark`, `benchmark-react`, `coin-app`, `nextjs`, `normalizr-github`, `normalizr-redux`, `test-bundlesize`, `vue-todo-app`). Only add a path if no esmodule job (or the `setup` builds feeding them) reads it. - `tests` (lint, typecheck, unit_tests, node_matrix): false only when every changed path is docs/website/tooling (`website/`, `docs/`, `.changeset/`, `.cursor/`, `.agents/`, `.claude/`, `.github/`, root `*.md`), except `website/src/components/Playground/` (has unit tests). When both flags are false, `setup` halts before install. -<<<<<<< HEAD - Legacy TS types (`ci:build:legacy-types`, consumed by `esmodule-types`): - Built inside `setup` (`ci:build:setup:esmodule`) only when the esmodule flag is set; there is no separate job, to keep a job hop off the critical path. - In CI only endpoint/normalizr/rest outputs for TS >= 4.0 are built (`LEGACY_MIN_TS=4.0`, the oldest TS in the `esmodule-types` matrix); release builds (`build:types`) still emit every version. - `scripts/build-legacy-types.sh` builds each TS version concurrently; each `ts/` gets the downleveled `lib` (with `abstract new` rewritten to `new` below 4.2, which `downlevel-dts` misses), then every newer version's `src-*-types` overlay, then its own. Keep overlays to small single-purpose modules (like `NoInfer.ts`, `tupleTypes.ts`) so whole-file copies can't go stale. - `esmodule-types` also runs `examples/todo-app/tsconfig.typetest-libcheck.json` (`skipLibCheck: false`, `types: []`) so errors inside the legacy outputs fail CI; the other typetests use `skipLibCheck: true`. - Any change to legacy types building must leave `ts*/` output byte-identical to master (diff it) unless it intentionally changes published types (then add a changeset). -======= -- Legacy TS types (`ci:build:legacy-types`, consumed by `esmodule-types`) build inside `setup` (`ci:build:setup:esmodule`) only when the esmodule flag is set; there is no separate job, to keep a job hop off the critical path. In CI it builds only endpoint/normalizr/rest outputs for TS >= 4.0 (`LEGACY_MIN_TS=4.0`), since the oldest TS in the `esmodule-types` matrix is 4.0; release builds (`build:types`) still emit every version. `scripts/build-legacy-types.sh` builds each TS version concurrently; each `ts/` gets the downleveled `lib`, then every newer version's `src-*-types` overlay, then its own. Any change to legacy types building must leave `ts*/` output byte-identical to master (diff it) unless it intentionally changes published types (then add a changeset). ->>>>>>> origin/claude/project-thread-bw2hx0 - Never `git fetch --depth` the base branch in the relevance check: a shallow fetch severs the merge base and the three-dot diff fails. - Changing root `package.json` `workspaces` requires updating the `setup` job's workspace trimming step. From ac19e204c9bf9c076d0da3890e0af1334f7c56a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 03:22:27 +0000 Subject: [PATCH 7/7] internal: Resolve leftover merge conflict in build-legacy-types.sh Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01AMEHZGboXdMFRkF4D2ktki --- scripts/build-legacy-types.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/scripts/build-legacy-types.sh b/scripts/build-legacy-types.sh index 8d789a123a7d..b20fd0596dd2 100755 --- a/scripts/build-legacy-types.sh +++ b/scripts/build-legacy-types.sh @@ -20,14 +20,11 @@ copy_types() { done } -<<<<<<< HEAD # True when version $1 is older than version $2. version_lt() { [ "$1" != "$2" ] && [ "$(printf '%s\n' "$1" "$2" | sort -V | head -1)" = "$1" ] } -======= ->>>>>>> origin/claude/project-thread-bw2hx0 # Versions are listed newest first, and custom types for a version also apply # to every version listed after it. So each output dir gets the downleveled lib, # then earlier versions' custom types, then its own. Each output dir only @@ -36,7 +33,6 @@ build_version() { local version="$1" shift "$downlevel_dts" lib "ts$version" --to="$version" -<<<<<<< HEAD # downlevel-dts keeps `abstract new` constructor types, which need TS 4.2 if version_lt "$version" 4.2; then grep -rl --include='*.d.ts' 'abstract new (' "ts$version" | while IFS= read -r file @@ -44,8 +40,6 @@ build_version() { perl -pi -e 's/abstract new \(/new (/g' "$file" done fi -======= ->>>>>>> origin/claude/project-thread-bw2hx0 for earlier in "$@" do copy_types "./src-$earlier-types" "./ts$version"