From 446a986fabd1329d6bbfcb5b4f8d7c30d2372e35 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 23:43:29 +0000 Subject: [PATCH 1/5] internal(ci): Fail open on esmodule relevance and speed up rest legacy types The esmodule relevance flag now uses a denylist of provably irrelevant paths, so new inputs run the esmodule jobs by default. rest's legacy types build uses scripts/build-legacy-types.sh (direct downlevel-dts, d.ts-only copies) with --newer-overlays-last to keep its overlay order; ts4.0/ts4.1 output is byte-identical (8.6s -> 4.5s locally). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01EEcQdob4nGVuMq91oUHC2V --- .circleci/config.yml | 5 +++-- .cursor/rules/ci-config.mdc | 4 ++-- packages/rest/package.json | 4 +--- scripts/build-legacy-types.sh | 22 ++++++++++++++++++---- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 06221312b2f3..374a1422ea6d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -85,8 +85,9 @@ jobs: elif git -c core.quotePath=off diff --no-renames --name-only "${DIFF_RANGE}" > /tmp/ci-changed-files; then echo "Changed files:" cat /tmp/ci-changed-files - if ! grep -Eq \ - '^(\.circleci/config\.yml|package\.json$|tsconfig[^/]*\.json$|babel\.config\.js$|\.yarnrc\.yml$|scripts/|yarn\.lock|examples/todo-app/|examples/github-app/|examples/normalizr-relationships/|packages/)' /tmp/ci-changed-files; then + # Denylist (fail open): only paths provably outside every esmodule + # job's inputs can skip them, so a new input dir runs by default. + if ! grep -Evq '^(website/|docs/|\.changeset/|\.cursor/|\.agents/|\.claude/|\.github/|\.vscode/|plans/|__tests__/|[^/]+\.md$|eslint\.config\.mjs$|jest\.config\.js$|examples/[^/]+\.md$|examples/(benchmark|benchmark-react|coin-app|nextjs|normalizr-github|normalizr-redux|test-bundlesize|vue-todo-app)/)' /tmp/ci-changed-files; then echo "No relevant esmodule-related changes; downstream esmodule jobs will halt." ESMODULE_RELEVANT=false fi diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 490a6482dd4e..342924efefe1 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -10,9 +10,9 @@ alwaysApply: false - Jest `--maxWorkers` is pinned per job to the `resource_class` vCPU count (large = 4, medium = 2) because docker containers report the host's CPUs via `os.cpus()`. Exception: the ReactNative `unit_tests` run is deliberately uncapped — its suites are fake-timer-wait dominated and capping workers flakes 5s test timeouts. - 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`: the path regex must cover every path the esmodule jobs build (currently `examples/todo-app`, `examples/github-app`, `examples/normalizr-relationships`, `packages/`, root `package.json`, root `tsconfig*.json`, `babel.config.js`, `.yarnrc.yml`, `scripts/`, `yarn.lock`, the config itself). + - `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 `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. +- 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 `--newer-overlays-last` because its newer `src-*-types` must replace the downleveled lib files (endpoint/normalizr copy them before downleveling). Any change to legacy types building must leave `ts*/` output byte-identical to master (diff it). - 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/rest/package.json b/packages/rest/package.json index 2d66e7ee8eb6..3009c284831d 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -123,9 +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:runp build:legacy-types:4.0 build:legacy-types:4.1", - "build:legacy-types:4.0": "yarn g:downtypes lib ts4.0 --to=4.0 && yarn g:copy --up 1 ./src-4.1-types/**/*.d.ts ./ts4.0/ && yarn g:copy --up 1 ./src-4.0-types/**/*.d.ts ./ts4.0", - "build:legacy-types:4.1": "yarn g:downtypes lib ts4.1 --to=4.1 && yarn g:copy --up 1 ./src-4.1-types/**/*.d.ts ./ts4.1", + "build:legacy-types": "yarn g:legacy-types --newer-overlays-last 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 9e5be7dab35c..97c292185b74 100755 --- a/scripts/build-legacy-types.sh +++ b/scripts/build-legacy-types.sh @@ -1,9 +1,17 @@ #!/bin/bash set -e +# --newer-overlays-last: copy earlier versions' custom types after the +# downleveled lib instead of before, so they replace colliding lib files. +newer_last=false +if [ "$1" = "--newer-overlays-last" ]; then + newer_last=true + shift +fi + # Check if at least one directory is provided if [ $# -eq 0 ]; then - echo "Usage: $0 [version2] ..." + echo "Usage: $0 [--newer-overlays-last] [version2] ..." exit 1 fi @@ -22,17 +30,23 @@ copy_types() { # Custom types for a version also apply to every version listed after it, # so each output dir gets (in order): earlier versions' custom types, the -# downleveled lib, then its own custom types. Each output dir only depends on -# lib and src-*-types, so versions build concurrently. +# downleveled lib, then its own custom types (with --newer-overlays-last, the +# downleveled lib comes first). Each output dir only depends on lib and +# src-*-types, so versions build concurrently. build_version() { local version="$1" shift mkdir -p "./ts$version" + if [ "$newer_last" = true ]; then + "$downlevel_dts" lib "ts$version" --to="$version" + fi for earlier in "$@" do copy_types "./src-$earlier-types" "./ts$version" done - "$downlevel_dts" lib "ts$version" --to="$version" + if [ "$newer_last" = false ]; then + "$downlevel_dts" lib "ts$version" --to="$version" + fi copy_types "./src-$version-types" "./ts$version" } From 60e36284957a338c402d2d0bd3a04d261c2bcc3a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 02:37:03 +0000 Subject: [PATCH 2/5] internal(ci): Simplify legacy types overlay order and share docs-only paths Downlevel always runs first; --inherit-newer-overlays (rest) then applies earlier versions' custom types. Without the flag those copies were always overwritten by downlevel-dts, so endpoint/normalizr output is unchanged (ts*/ byte-identical). The esmodule and tests relevance checks now share one DOCS_ONLY path list. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01EEcQdob4nGVuMq91oUHC2V --- .circleci/config.yml | 6 ++++-- .cursor/rules/ci-config.mdc | 4 ++-- packages/rest/package.json | 2 +- scripts/build-legacy-types.sh | 36 +++++++++++++++-------------------- 4 files changed, 22 insertions(+), 26 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 374a1422ea6d..86d6393a13d2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -85,15 +85,17 @@ jobs: elif git -c core.quotePath=off diff --no-renames --name-only "${DIFF_RANGE}" > /tmp/ci-changed-files; then echo "Changed files:" cat /tmp/ci-changed-files + # Docs/website/tooling paths; both flags below are denylists over it. + DOCS_ONLY='website/|docs/|\.changeset/|\.cursor/|\.agents/|\.claude/|\.github/|[^/]+\.md$' # Denylist (fail open): only paths provably outside every esmodule # job's inputs can skip them, so a new input dir runs by default. - if ! grep -Evq '^(website/|docs/|\.changeset/|\.cursor/|\.agents/|\.claude/|\.github/|\.vscode/|plans/|__tests__/|[^/]+\.md$|eslint\.config\.mjs$|jest\.config\.js$|examples/[^/]+\.md$|examples/(benchmark|benchmark-react|coin-app|nextjs|normalizr-github|normalizr-redux|test-bundlesize|vue-todo-app)/)' /tmp/ci-changed-files; then + if ! grep -Evq "^(${DOCS_ONLY}|\.vscode/|plans/|__tests__/|eslint\.config\.mjs$|jest\.config\.js$|examples/[^/]+\.md$|examples/(benchmark|benchmark-react|coin-app|nextjs|normalizr-github|normalizr-redux|test-bundlesize|vue-todo-app)/)" /tmp/ci-changed-files; then echo "No relevant esmodule-related changes; downstream esmodule jobs will halt." ESMODULE_RELEVANT=false fi # Docs/website/tooling-only changes can't affect lint, typecheck or # unit tests (Playground has unit tests, so it stays relevant). - if ! grep -Evq '^(website/|docs/|\.changeset/|\.cursor/|\.agents/|\.claude/|\.github/|[^/]+\.md$)' /tmp/ci-changed-files \ + if ! grep -Evq "^(${DOCS_ONLY})" /tmp/ci-changed-files \ && ! grep -q '^website/src/components/Playground/' /tmp/ci-changed-files; then echo "Only docs/website/tooling paths changed; test jobs will halt." TESTS_RELEVANT=false diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 342924efefe1..5db8838975a5 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -10,9 +10,9 @@ alwaysApply: false - Jest `--maxWorkers` is pinned per job to the `resource_class` vCPU count (large = 4, medium = 2) because docker containers report the host's CPUs via `os.cpus()`. Exception: the ReactNative `unit_tests` run is deliberately uncapped — its suites are fake-timer-wait dominated and capping workers flakes 5s test timeouts. - 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 `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. + - `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 `--newer-overlays-last` because its newer `src-*-types` must replace the downleveled lib files (endpoint/normalizr copy them before downleveling). 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; 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). - 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/rest/package.json b/packages/rest/package.json index 3009c284831d..63b837a0a850 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 --newer-overlays-last 4.1 4.0", + "build:legacy-types": "yarn g:legacy-types --inherit-newer-overlays 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 97c292185b74..89e972517a49 100755 --- a/scripts/build-legacy-types.sh +++ b/scripts/build-legacy-types.sh @@ -1,17 +1,17 @@ #!/bin/bash set -e -# --newer-overlays-last: copy earlier versions' custom types after the -# downleveled lib instead of before, so they replace colliding lib files. -newer_last=false -if [ "$1" = "--newer-overlays-last" ]; then - newer_last=true +# --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 [--newer-overlays-last] [version2] ..." + echo "Usage: $0 [--inherit-newer-overlays] [version2] ..." exit 1 fi @@ -28,24 +28,18 @@ copy_types() { done } -# Custom types for a version also apply to every version listed after it, -# so each output dir gets (in order): earlier versions' custom types, the -# downleveled lib, then its own custom types (with --newer-overlays-last, the -# downleveled lib comes first). Each output dir only depends on lib and -# src-*-types, so versions build concurrently. +# 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. build_version() { local version="$1" shift - mkdir -p "./ts$version" - if [ "$newer_last" = true ]; then - "$downlevel_dts" lib "ts$version" --to="$version" - fi - for earlier in "$@" - do - copy_types "./src-$earlier-types" "./ts$version" - done - if [ "$newer_last" = false ]; then - "$downlevel_dts" lib "ts$version" --to="$version" + "$downlevel_dts" lib "ts$version" --to="$version" + if [ "$inherit" = true ]; then + for earlier in "$@" + do + copy_types "./src-$earlier-types" "./ts$version" + done fi copy_types "./src-$version-types" "./ts$version" } From 202acbddd39dfe99937731293617c75b0172a8cd Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Sat, 3 Oct 2026 23:17:40 -0400 Subject: [PATCH 3/5] fix(types): Apply newer legacy type overlays to older TS outputs (#4138) * 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 * docs(blog): Note legacy TypeScript types fix Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MpCgcZt8G8R5KARZ7z5CKE --------- Co-authored-by: Claude --- .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 ++++++++-------------- website/blog/2026-10-03-v0.19-batch-set.md | 1 + 6 files changed, 40 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 5db8838975a5..9da0cac5e476 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" } 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 77d20541c40c..86180b75057b 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -19,6 +19,7 @@ import StackBlitz from '@site/src/components/StackBlitz'; - 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)) +- 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 182344b63c50709b4d23b6bf65c41ee8ba3b30b3 Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Sat, 3 Oct 2026 23:57:12 -0400 Subject: [PATCH 4/5] fix(types): Fix endpoint/rest legacy type errors with skipLibCheck off (#4140) * 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 * docs(blog): Note legacy TypeScript types fix Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MpCgcZt8G8R5KARZ7z5CKE * 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 * 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 * 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 * 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 * 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 --------- Co-authored-by: Claude --- .changeset/legacy-ts-endpoint-libcheck.md | 30 ++++ .circleci/config.yml | 2 + .cursor/rules/ci-config.mdc | 7 +- .../todo-app/tsconfig.typetest-libcheck.json | 16 ++ examples/todo-app/typetest-libcheck.ts | 44 +++++ .../src-4.0-types/schemas/Entity.d.ts | 45 ----- .../src-4.0-types/schemas/EntityTypes.d.ts | 164 ------------------ .../endpoint/src-4.0-types/tupleTypes.d.ts | 14 ++ .../endpoint/src-4.2-types/endpointTypes.d.ts | 7 +- packages/endpoint/src/endpointTypes.ts | 8 +- 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 | 14 +- packages/endpoint/src/tupleTypes.ts | 16 ++ packages/endpoint/src/utility.ts | 7 +- packages/rest/src/RestEndpointTypes.ts | 3 +- scripts/build-legacy-types.sh | 15 +- website/blog/2026-10-03-v0.19-batch-set.md | 1 + 19 files changed, 170 insertions(+), 244 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 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/tupleTypes.d.ts create mode 100644 packages/endpoint/src/tupleTypes.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 9da0cac5e476..3283a70bfe87 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`, 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`): + - 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 new file mode 100644 index 000000000000..3104ec65ec61 --- /dev/null +++ b/examples/todo-app/tsconfig.typetest-libcheck.json @@ -0,0 +1,16 @@ +{ + // 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, + "skipDefaultLibCheck": true, + "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/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/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.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/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..89187468b3a3 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,12 @@ export interface IEntityInstance { } export 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) + */ +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/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/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..b20fd0596dd2 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,13 @@ 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 + 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 copy_types "./src-$earlier-types" "./ts$version" @@ -37,8 +49,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 86180b75057b..484ea6cb623d 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -20,6 +20,7 @@ import StackBlitz from '@site/src/components/StackBlitz'; - 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)) - 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 ([#4140](https://github.com/reactive/data-client/pull/4140)) {/* truncate */} From 479badb7017ba884cc7dbdbcdfd08d58bdb72325 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 15:14:53 +0000 Subject: [PATCH 5/5] internal(ci): Harden legacy types script and libcheck lib/ on latest TS version_lt compares major.minor in bash instead of relying on sort -V (a missing -V silently skipped the abstract new rewrite), and the abstract new grep no longer fails the build under pipefail when nothing matches. esmodule-types-latest also runs the skipLibCheck: false typetest so lib/ is checked on the newest compiler. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01EEcQdob4nGVuMq91oUHC2V --- .circleci/config.yml | 2 ++ .cursor/rules/ci-config.mdc | 2 +- scripts/build-legacy-types.sh | 7 ++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b42b06379396..3054a37e5956 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -355,6 +355,8 @@ jobs: yarn run tsc --project examples/todo-app/tsconfig.typetest.json --exactOptionalPropertyTypes true yarn run tsc --project examples/github-app/tsconfig.typetest.json --exactOptionalPropertyTypes true yarn run tsc --project examples/todo-app/tsconfig.typetest41.json --exactOptionalPropertyTypes true + # skipLibCheck: false on lib/ too (TS 7 removed moduleResolution node) + yarn run tsc --project examples/todo-app/tsconfig.typetest-libcheck.json --moduleResolution bundler validate-esmodule-browser-build: executor: node diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 3283a70bfe87..4b743b6e8a04 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -16,7 +16,7 @@ alwaysApply: false - 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`. + - `esmodule-types` also runs `examples/todo-app/tsconfig.typetest-libcheck.json` (`skipLibCheck: false`, `types: []`) so errors inside the legacy outputs fail CI, and `esmodule-types-latest` runs it with `--moduleResolution bundler` (TS 7 removed `node`) to cover `lib/`; 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/scripts/build-legacy-types.sh b/scripts/build-legacy-types.sh index b20fd0596dd2..6a4b1c560f10 100755 --- a/scripts/build-legacy-types.sh +++ b/scripts/build-legacy-types.sh @@ -20,9 +20,10 @@ copy_types() { done } -# True when version $1 is older than version $2. +# True when major.minor version $1 is older than $2 (pure bash, no `sort -V`). version_lt() { - [ "$1" != "$2" ] && [ "$(printf '%s\n' "$1" "$2" | sort -V | head -1)" = "$1" ] + local a_major="${1%%.*}" a_minor="${1#*.}" b_major="${2%%.*}" b_minor="${2#*.}" + (( a_major < b_major || (a_major == b_major && a_minor < b_minor) )) } # Versions are listed newest first, and custom types for a version also apply @@ -35,7 +36,7 @@ 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 - grep -rl --include='*.d.ts' 'abstract new (' "ts$version" | while IFS= read -r file + { grep -rl --include='*.d.ts' 'abstract new (' "ts$version" || true; } | while IFS= read -r file do perl -pi -e 's/abstract new \(/new (/g' "$file" done