Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .changeset/legacy-ts-endpoint-libcheck.md
Original file line number Diff line number Diff line change
@@ -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<any>'.
// Before (TypeScript 4.2):
// error TS2344: Type 'TBase' does not satisfy the constraint 'new (...args: any) => any'.
// After: no errors
```
2 changes: 2 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion .cursor/rules/ci-config.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -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<version>/` 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<version>/` 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.

Expand Down
16 changes: 16 additions & 0 deletions examples/todo-app/tsconfig.typetest-libcheck.json
Original file line number Diff line number Diff line change
@@ -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"]
}
44 changes: 44 additions & 0 deletions examples/todo-app/typetest-libcheck.ts
Original file line number Diff line number Diff line change
@@ -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 };
45 changes: 0 additions & 45 deletions packages/endpoint/src-4.0-types/schemas/Entity.d.ts

This file was deleted.

164 changes: 0 additions & 164 deletions packages/endpoint/src-4.0-types/schemas/EntityTypes.d.ts

This file was deleted.

14 changes: 14 additions & 0 deletions packages/endpoint/src-4.0-types/tupleTypes.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Recursive conditional types need TypeScript 4.1, so these are approximated

export type PartialArray<A> = A extends any[] ? Partial<A> : never;

/** Removes Rem's length worth of leading elements from Orig (up to 3) */
export type RemoveArray<Orig extends any[], Rem extends any[]> =
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[];
7 changes: 1 addition & 6 deletions packages/endpoint/src-4.2-types/endpointTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -156,11 +157,5 @@ export interface ExtendableEndpointConstructor {
): EndpointInstanceInterface<F, S, M> & E;
readonly prototype: Function;
}
type RemoveArray<Orig extends any[], Rem extends any[]> =
Rem extends [any, ...infer RestRem] ?
Orig extends [any, ...infer RestOrig] ?
RemoveArray<RestOrig, RestRem>
: never
: Orig;
export {};
//# sourceMappingURL=endpointTypes.d.ts.map
8 changes: 1 addition & 7 deletions packages/endpoint/src/endpointTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
FetchFunction,
PartialParameters,
} from './types.js';
import type { RemoveArray } from './tupleTypes.js';

export interface EndpointOptions<
F extends FetchFunction = FetchFunction,
Expand Down Expand Up @@ -191,10 +192,3 @@ type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
type IfTypeScriptLooseNull<Y, N> = 1 | undefined extends 1 ? Y : N;

type OnlyFirst<A extends unknown[]> = A extends [] ? [] : [A[0]];

type RemoveArray<Orig extends any[], Rem extends any[]> =
Rem extends [any, ...infer RestRem] ?
Orig extends [any, ...infer RestOrig] ?
RemoveArray<RestOrig, RestRem>
: never
: Orig;
4 changes: 2 additions & 2 deletions packages/endpoint/src/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ export interface UnionConstructor {
*/
export interface UnionInstance<
Choices extends EntityMap = any,
Args extends EntityFields<AbstractInstanceType<Choices[keyof Choices]>> =
EntityFields<AbstractInstanceType<Choices[keyof Choices]>>,
// unconstrained: TypeScript 4.2-4.4 can't prove UnionConstructor's Args satisfy EntityFields
Args = EntityFields<AbstractInstanceType<Choices[keyof Choices]>>,
> {
readonly _hoistable: true;
define(definition: Schema): void;
Expand Down
Loading