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/.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/.circleci/config.yml b/.circleci/config.yml index 06221312b2f3..3054a37e5956 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -85,14 +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 - 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 + # 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 "^(${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 @@ -326,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 @@ -350,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 490a6482dd4e..4b743b6e8a04 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -10,9 +10,14 @@ 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 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. +- 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, 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/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/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 2d66e7ee8eb6..5faf4e204233 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 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/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 9e5be7dab35c..6a4b1c560f10 100755 --- a/scripts/build-legacy-types.sh +++ b/scripts/build-legacy-types.sh @@ -20,26 +20,37 @@ 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. Each output dir only depends on -# lib and src-*-types, so versions build concurrently. +# True when major.minor version $1 is older than $2 (pure bash, no `sort -V`). +version_lt() { + 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 +# 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 - mkdir -p "./ts$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" || true; } | 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" done - "$downlevel_dts" lib "ts$version" --to="$version" copy_types "./src-$version-types" "./ts$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 77d20541c40c..484ea6cb623d 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,8 @@ 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)) +- 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 */}