Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1c9ba74
enhance(rest): Faster type checking for RestEndpoint, resource() and …
claude Oct 4, 2026
698b471
docs(blog): Note faster TypeScript checking in v0.19
claude Oct 4, 2026
d66078d
test(rest): Commit extend/paginate/path type probes; simplify cleanups
claude Oct 4, 2026
e426394
docs: Update type-check numbers against latest master
claude Oct 4, 2026
565035e
Merge master
claude Oct 5, 2026
10d7312
docs(blog): Headline faster TypeScript in v0.19 post with charts and …
claude Oct 5, 2026
063647c
docs(website): Show draft pages on Vercel preview deploys
claude Oct 5, 2026
267a450
Merge remote-tracking branch 'origin/master' into claude/type-perf-q9…
claude Oct 5, 2026
03b15ea
docs(blog): Use PerfChart for faster TypeScript results
claude Oct 5, 2026
3fc327d
docs(website): Mark 1x in PerfChart and add visual PerfTable
claude Oct 5, 2026
a4be579
docs(website): Show PerfTable exact numbers on hover
claude Oct 5, 2026
c0cd88d
docs(website): Move PerfChart exact numbers into hover tooltips
claude Oct 5, 2026
c5fca9d
docs(website): Scroll PerfTable in place on narrow screens
claude Oct 5, 2026
3970ebb
docs(website): Align neighboring PerfChart 1x lines and add tap tooltips
claude Oct 5, 2026
4fc0411
docs(website): Decide PerfChart log scale from its own rows, not scal…
claude Oct 5, 2026
53127e0
docs(blog): Compare faster TypeScript numbers with published v0.18.1
claude Oct 5, 2026
a1672f2
docs(website): Keep regressed rows visible on log-scale PerfCharts
claude Oct 5, 2026
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
23 changes: 23 additions & 0 deletions .changeset/faster-rest-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@data-client/rest': patch
'@data-client/endpoint': patch
'@data-client/graphql': patch
---

Speed up TypeScript checking of `RestEndpoint`, `resource()` and `.extend()`

Editors and `tsc` check code that defines or calls endpoints faster and with less memory. In our stress tests, a file of 150 RestEndpoints with long paths, `.extend()` and `.paginated()` checked in about half the time (3.1s → 1.6s) and memory (345MB → 207MB). Files that use `resource()` with React or Vue hooks did 22-25% less type work. TypeScript reports the same errors as before.

On TypeScript 5.x and earlier, a `process(value, params)` method passed to `.extend()` no longer fails with "implicitly has an 'any' type" under `strict`, matching TypeScript 6+. TypeScript 4.0 also accepts a chained `.extend().extend()`.

```ts
const getUser = new RestEndpoint({ path: '/users/:id', schema: User });

// Before (TypeScript 5.x and earlier): error TS7006: Parameter 'value' implicitly has an 'any' type.
// After: no error
const getUserName = getUser.extend({
process(value, params) {
return value.name;
},
});
```
11 changes: 10 additions & 1 deletion examples/todo-app/typetest-libcheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,18 @@ const PostResource = resource({
});
PostResource.getList.getPage({ cursor: 'a' });
const search = new RestEndpoint({ path: '/search' });
// extend() methods' parameters aren't implicitly any on any TypeScript version,
// including on a chained extend()
const getPostTitle = new RestEndpoint({ path: '/posts/:id', schema: Post })
.extend({ dataExpiryLength: 5 })
.extend({
process(value, params) {
return `${params.id}: ${value.title}`;
},
});

const memo = new MemoCache();
const { result, entities } = normalize(Post, { id: '1', title: 'hi' });
denormalize(Post, result, entities);

export { getUser, feed, search, memo };
export { getUser, feed, search, memo, getPostTitle };
4 changes: 2 additions & 2 deletions packages/endpoint/src-4.2-types/endpointTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ export interface EndpointInstance<
Record<string, unknown>,
>(
this: E,
options: Readonly<O>,
): ExtendedEndpoint<typeof options, E, F>;
options: Readonly<O> & O,
): ExtendedEndpoint<Readonly<O>, E, F>;
}
/**
* Defines an async data source.
Expand Down
6 changes: 3 additions & 3 deletions packages/endpoint/src/endpointTypes.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
import type { EndpointInterface, Schema } from './interface.js';
import type { RemoveArray } from './tupleTypes.js';
import type {
EndpointExtraOptions,
FetchFunction,
PartialParameters,
} from './types.js';
import type { RemoveArray } from './tupleTypes.js';

export interface EndpointOptions<
F extends FetchFunction = FetchFunction,
Expand Down Expand Up @@ -69,8 +69,8 @@ export interface EndpointInstance<
Record<string, unknown>,
>(
this: E,
options: Readonly<O>,
): ExtendedEndpoint<typeof options, E, F>;
options: Readonly<O> & O,
): ExtendedEndpoint<Readonly<O>, E, F>;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/rest/src-4.1-types/resourceExtendable.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ export interface Extendable<O extends ResourceGenerics = {
}> {
extend<R extends {
[K in ExtendKey]: RestInstanceBase;
}, ExtendKey extends Exclude<Extract<keyof R, string>, 'extend'>, ExtendOptions extends PartialRestGenerics | {}>(this: R, key: ExtendKey, options: Readonly<RestEndpointExtendOptions<ExtendOptions, R[ExtendKey], EndpointToFunction<R[ExtendKey]>> & ExtendOptions>): ResourceExtension<R, ExtendKey, ExtendOptions>;
}, ExtendKey extends Exclude<Extract<keyof R, string>, 'extend'>, ExtendOptions extends PartialRestGenerics | {}>(this: R, key: ExtendKey, options: Readonly<RestEndpointExtendOptions<ExtendOptions, R[ExtendKey], EndpointToFunction<R[ExtendKey]>> & ExtendOptions> & ExtendOptions): ResourceExtension<R, ExtendKey, ExtendOptions>;
extend<R extends {
get: RestInstanceBase;
}, ExtendKey extends string, ExtendOptions extends PartialRestGenerics | {}>(this: R, key: ExtendKey, options: Readonly<RestEndpointExtendOptions<ExtendOptions, R['get'], EndpointToFunction<R['get']>> & ExtendOptions>): R & {
}, ExtendKey extends string, ExtendOptions extends PartialRestGenerics | {}>(this: R, key: ExtendKey, options: Readonly<RestEndpointExtendOptions<ExtendOptions, R['get'], EndpointToFunction<R['get']>> & ExtendOptions> & ExtendOptions): R & {
[key in ExtendKey]: RestExtendedEndpoint<ExtendOptions, R['get']>;
};
extend<R extends ResourceInterface, Get extends PartialRestGenerics = {}, GetList extends PartialRestGenerics = {}, Update extends PartialRestGenerics = {}, PartialUpdate extends PartialRestGenerics = {}, Delete extends PartialRestGenerics = {}>(this: R, options: ResourceEndpointExtensions<R, Get, GetList, Update, PartialUpdate, Delete>): CustomResource<R, O, Get, GetList, Update, PartialUpdate, Delete>;
Expand Down
127 changes: 81 additions & 46 deletions packages/rest/src/RestEndpointTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,56 @@ type ContentSchemaGuard<O> =
{ schema?: undefined }
: {};

/* Generic `this: E` methods (extend, paginated) live in non-generic mixin
* interfaces. TypeScript re-instantiates every member of a generic interface
* (with fresh signature type parameters) for each distinct instantiation;
* here the signatures are shared across all endpoints, and compare as
* identical when relating one RestInstanceBase to another. extend() reads
* `E['fetch']` instead of `F` so it doesn't depend on the outer generics.
*/
interface RestInstanceExtenders {
// TODO: `ExtendOptions extends PartialRestGenerics | {}` is a hack for options with no
// PartialRestGenerics members. Overloads (like paginated) can't tell the cases apart
// since every member is optional.
/** Creates a child endpoint that inherits from this while overriding provided `options`.
* @see https://dataclient.io/rest/api/RestEndpoint#extend
*/
extend<
E extends RestInstanceBase,
ExtendOptions extends PartialRestGenerics | {},
>(
this: E,
options: Readonly<
RestEndpointExtendOptions<ExtendOptions, E, E['fetch']> & ExtendOptions
> &
// naked ExtendOptions wins inference (plain object type instead of a
// reverse-mapped type); Readonly<> still provides literal-preserving
// contextual types for path/method/etc.
ExtendOptions &
Comment thread
ntucker marked this conversation as resolved.
ContentSchemaGuard<ExtendOptions>,
): RestExtendedEndpoint<ExtendOptions, E>;
}

interface RestInstancePaginators {
/** Creates an Endpoint to append the next page extending a list for pagination
* @see https://dataclient.io/rest/api/RestEndpoint#paginated
*/
paginated<
E extends RestInstanceBase<FetchFunction, any, undefined>,
A extends any[],
>(
this: E,
removeCursor: (...args: A) => readonly [...Parameters<E>],
): PaginationEndpoint<E, A>;
paginated<
E extends RestInstanceBase<FetchFunction, any, undefined>,
C extends string,
>(
this: E,
cursorField: C,
): PaginationFieldEndpoint<E, C>;
}

export interface RestInstanceBase<
F extends FetchFunction = FetchFunction,
S extends Schema | undefined = any,
Expand All @@ -42,7 +92,8 @@ export interface RestInstanceBase<
searchParams?: any;
method?: string;
} = { path: string },
> extends EndpointInstanceInterface<F, S, M> {
>
extends EndpointInstanceInterface<F, S, M>, RestInstanceExtenders {
/** @see https://dataclient.io/rest/api/RestEndpoint#body */
readonly body?: 'body' extends keyof O ? O['body'] : any;
/** @see https://dataclient.io/rest/api/RestEndpoint#searchParams */
Expand Down Expand Up @@ -109,25 +160,6 @@ export interface RestInstanceBase<
* @see https://dataclient.io/rest/api/RestEndpoint#testKey
*/
testKey(key: string): boolean;

/* extenders */
// TODO: figure out better way than wrapping whole options in Readonly<> + making O extend from {}
// this is just a hack to handle when no members of PartialRestGenerics are present
// Note: Using overloading (like paginated did) struggles because typescript does not have a clear way of distinguishing one
// should be used from the other (due to same problem with every member being partial)
/** Creates a child endpoint that inherits from this while overriding provided `options`.
* @see https://dataclient.io/rest/api/RestEndpoint#extend
*/
extend<
E extends RestInstanceBase,
ExtendOptions extends PartialRestGenerics | {},
>(
this: E,
options: Readonly<
RestEndpointExtendOptions<ExtendOptions, E, F> & ExtendOptions
> &
ContentSchemaGuard<ExtendOptions>,
): RestExtendedEndpoint<ExtendOptions, E>;
}

export interface RestInstance<
Expand All @@ -141,31 +173,21 @@ export interface RestInstance<
method?: string;
paginationField?: string;
} = { path: string },
> extends RestInstanceBase<F, S, M, O> {
/** Creates an Endpoint to append the next page extending a list for pagination
* @see https://dataclient.io/rest/api/RestEndpoint#paginated
*/
paginated<
E extends RestInstanceBase<FetchFunction, any, undefined>,
A extends any[],
>(
this: E,
removeCursor: (...args: A) => readonly [...Parameters<E>],
): PaginationEndpoint<E, A>;
paginated<
E extends RestInstanceBase<FetchFunction, any, undefined>,
C extends string,
>(
this: E,
cursorField: C,
): PaginationFieldEndpoint<E, C>;
>
extends RestInstanceBase<F, S, M, O>, RestInstancePaginators {
/** Concatinate the next page of results (GET)
* @see https://dataclient.io/rest/api/RestEndpoint#getPage
*/
getPage: 'paginationField' extends keyof O ?
O['paginationField'] extends string ?
PaginationFieldEndpoint<
F & { schema: S; sideEffect: M } & O,
// A plain fetch function (no members) only contributes ResolveType<>;
// collapsing it to one signature avoids distributing this intersection
// over each member when F is a union of fetch signatures.
([keyof F] extends [never] ? (...args: any) => ReturnType<F> : F) & {
schema: S;
sideEffect: M;
} & O,
// TypeScript <4.6 doesn't narrow O['paginationField'] here
Extract<O['paginationField'], string>
>
Expand Down Expand Up @@ -251,13 +273,21 @@ export type RestEndpointExtendOptions<
'schema' extends keyof O ? Extract<O['schema'], Schema | undefined>
: E['schema']
> &
Partial<
Omit<
E,
KeyofRestEndpoint | keyof PartialRestGenerics | keyof RestEndpointOptions
>
// Same as Partial<Omit<E, ExtendOmitKeys>>, but skips the per-key Exclude<> work
// (and the double mapped type) when E has no members beyond the standard ones.
// Keep the guard inside the mapped type's keys: a `? unknown : ...` conditional
// here would be deferred and break ExtendOptions inference on chained extend().
PartialPick<
E,
keyof E extends ExtendOmitKeys ? never : Exclude<keyof E, ExtendOmitKeys>
>;

type ExtendOmitKeys =
KeyofRestEndpoint | keyof PartialRestGenerics | keyof RestEndpointOptions;

/** Partial<Pick<T, K>> as a single homomorphic mapped type */
type PartialPick<T, K extends keyof T> = { [P in K]?: T[P] };

type OptionsToRestEndpoint<
O extends PartialRestGenerics,
E extends RestInstanceBase & { body?: any; paginationField?: string },
Expand Down Expand Up @@ -394,8 +424,11 @@ export type RestExtendedEndpoint<
: E['sideEffect']
>
> &
Omit<O, KeyofRestEndpoint> &
Omit<E, KeyofRestEndpoint | keyof O>;
// Equivalent to Omit<O, KeyofRestEndpoint> & Omit<E, KeyofRestEndpoint | keyof O>;
// the guards avoid per-key Exclude<> work when there are no extra members
(keyof O extends KeyofRestEndpoint ? unknown : Omit<O, KeyofRestEndpoint>) &
(keyof E extends KeyofRestEndpoint ? unknown
: Omit<E, KeyofRestEndpoint | keyof O>);

export interface PartialRestGenerics {
/** @see https://dataclient.io/rest/api/RestEndpoint#path */
Expand Down Expand Up @@ -678,6 +711,8 @@ export interface RestEndpointConstructor {
...options
}: RestEndpointConstructorOptions<O> &
Readonly<O> &
// naked O wins inference (plain object type instead of a reverse-mapped type)
O &
ContentSchemaGuard<O>): RestEndpoint<O>;
readonly prototype: RestInstanceBase;
}
Expand Down
Loading
Loading