diff --git a/.changeset/legacy-ts-endpoint-libcheck.md b/.changeset/legacy-ts-endpoint-libcheck.md new file mode 100644 index 000000000000..6fde9e1a81a8 --- /dev/null +++ b/.changeset/legacy-ts-endpoint-libcheck.md @@ -0,0 +1,30 @@ +--- +'@data-client/endpoint': patch +'@data-client/rest': patch +'@data-client/graphql': patch +--- + +Fix more TypeScript 4.x errors when `skipLibCheck` is off + +[Entity](https://dataclient.io/rest/api/Entity), [Endpoint](https://dataclient.io/rest/api/Endpoint), [Union](https://dataclient.io/rest/api/Union) and [RestEndpoint](https://dataclient.io/rest/api/RestEndpoint) declarations no longer report errors on TypeScript 4.0 through 4.5. On TypeScript 4.0 and 4.1, an `Entity` can be an `Endpoint` schema again. + +```ts +import { Endpoint, Entity, schema } from '@data-client/endpoint'; + +class User extends Entity { + id = ''; + type = 'users'; +} +const getUser = new Endpoint( + (id: string) => fetch(`/users/${id}`).then(res => res.json()), + { schema: User }, +); +const feed = new schema.Union({ users: User }, 'type'); + +// Before (TypeScript 4.0, skipLibCheck: false): +// error TS2456: Type alias 'RemoveArray' circularly references itself. +// error TS2322: Type 'typeof User' is not assignable to type 'EntityInterface'. +// Before (TypeScript 4.2): +// error TS2344: Type 'TBase' does not satisfy the constraint 'new (...args: any) => any'. +// After: no errors +``` diff --git a/.circleci/config.yml b/.circleci/config.yml index 86d6393a13d2..b42b06379396 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -329,6 +329,8 @@ jobs: fi yarn run tsc --project examples/todo-app/tsconfig.typetest.json yarn run tsc --project examples/github-app/tsconfig.typetest.json + # skipLibCheck: false, so the legacy typesVersions outputs get checked too + yarn run tsc --project examples/todo-app/tsconfig.typetest-libcheck.json esmodule-types-latest: executor: node diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 937fdf532d2b..2e315573a044 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -12,7 +12,12 @@ alwaysApply: false - Jobs halt via the `halt-unless-relevant-change` command based on flags computed once in `setup` (`.ci-esmodule-relevant`, `.ci-tests-relevant`) and transported via `save_cache`/`restore_cache` (keyed on `CIRCLE_SHA1`) so jobs can halt before paying `attach_workspace`. Missing/unreadable flags fail open (jobs run). On the default branch both flags are always true (a push may carry several commits). The diff uses `--no-renames` so moving a file out of a relevant dir still counts. - `esmodule` (validate-esmodule-browser-build, esmodule-types*): a denylist, so new paths fail open. False only when every changed path is provably outside the esmodule jobs' inputs: the shared `DOCS_ONLY` paths (also the `tests` denylist) plus `.vscode/`, `plans/`, root `__tests__/` (excluded by every `tsconfig.compile.json`), `eslint.config.mjs`, `jest.config.js`, `examples/*.md`, and examples the jobs never build (`benchmark`, `benchmark-react`, `coin-app`, `nextjs`, `normalizr-github`, `normalizr-redux`, `test-bundlesize`, `vue-todo-app`). Only add a path if no esmodule job (or the `setup` builds feeding them) reads it. - `tests` (lint, typecheck, unit_tests, node_matrix): false only when every changed path is docs/website/tooling (`website/`, `docs/`, `.changeset/`, `.cursor/`, `.agents/`, `.claude/`, `.github/`, root `*.md`), except `website/src/components/Playground/` (has unit tests). When both flags are false, `setup` halts before install. -- Legacy TS types (`ci:build:legacy-types`, consumed by `esmodule-types`) build inside `setup` (`ci:build:setup:esmodule`) only when the esmodule flag is set; there is no separate job, to keep a job hop off the critical path. In CI it builds only endpoint/normalizr/rest outputs for TS >= 4.0 (`LEGACY_MIN_TS=4.0`), since the oldest TS in the `esmodule-types` matrix is 4.0; release builds (`build:types`) still emit every version. `scripts/build-legacy-types.sh` builds each TS version concurrently; each `ts/` gets the downleveled `lib`, 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 d369ce495270..2baea5e94e09 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -17,6 +17,7 @@ import StackBlitz from '@site/src/components/StackBlitz'; - Fix TypeScript 7 module resolution for package `exports`; imports now resolve to declaration files ([#4019](https://github.com/reactive/data-client/pull/4019)) - [renderDataHook()](/docs/api/renderDataHook) runs provider mount effects when the first render suspends ([#4099](https://github.com/reactive/data-client/pull/4099)) - Fix `Cannot find name 'NoInfer'` and `export type` errors on TypeScript 4.x with `skipLibCheck` off ([#4138](https://github.com/reactive/data-client/pull/4138)) +- Fix `Entity`, `Endpoint`, `Union` and `RestEndpoint` type errors on TypeScript 4.0–4.5 with `skipLibCheck` off ([#4140](https://github.com/reactive/data-client/pull/4140)) {/* truncate */}