Skip to content

Commit

Permalink
enhance: Improve args type matching for hooks (#3020)
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Apr 25, 2024
1 parent b5c155c commit dcb6b2f
Show file tree
Hide file tree
Showing 35 changed files with 313 additions and 109 deletions.
20 changes: 20 additions & 0 deletions .changeset/big-donkeys-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@data-client/react": patch
---

Hooks arg-typechecking accuracy improved

For example string literals now work:

```ts
const getThing= new Endpoint(
(args: { postId: string | number; sortBy?: 'votes' | 'recent' }) =>
Promise.resolve({ a: 5, ...args }),
{ schema: MyEntity },
);

const myThing = useSuspense(getThing, {
postId: '5',
sortBy: 'votes',
});
```
7 changes: 7 additions & 0 deletions .changeset/red-ties-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@data-client/normalizr": patch
"@data-client/endpoint": patch
"@data-client/core": patch
---

Add NI<> utility type that is back-compat NoInfer<>
2 changes: 1 addition & 1 deletion docs/rest/api/RestEndpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ const getComments = new RestEndpoint({
// Hover your mouse over 'comments' to see its type
const comments = useSuspense(getComments, {
postId: '5',
sortBy: 'votes' as const,
sortBy: 'votes',
});

const ctrl = useController();
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"g:clean": "cd $INIT_CWD && rimraf lib ts3.4 legacy dist *.tsbuildinfo",
"g:downtypes": "cd $INIT_CWD && downlevel-dts",
"g:tsc": "cd $INIT_CWD && tsc",
"g:legacy-types": "cd $INIT_CWD && ../../scripts/build-legacy-types.sh",
"g:runs": "cd $INIT_CWD && run-s",
"g:copy": "cd $INIT_CWD && copyfiles",
"g:lint": "cd $INIT_CWD && eslint"
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type {
EndpointExtraOptions,
Queryable,
SchemaArgs,
NI,
} from '@data-client/normalizr';
export { ExpiryStatus } from '@data-client/normalizr';
export {
Expand Down
1 change: 1 addition & 0 deletions packages/endpoint/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
/ts3.4
/ts4.0
/ts4.2
/ts4.8
15 changes: 12 additions & 3 deletions packages/endpoint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,22 @@
"unpkg": "dist/index.umd.min.js",
"types": "lib/index.d.ts",
"typesVersions": {
">=4.8": {
">=5.4": {
"": [
"lib/index.d.ts"
],
"*": [
"lib/index.d.ts"
]
},
">=4.8": {
"": [
"ts4.8/index.d.ts"
],
"*": [
"ts4.8/index.d.ts"
]
},
">=4.2": {
"": [
"ts4.2/index.d.ts"
Expand Down Expand Up @@ -84,6 +92,7 @@
"src",
"dist",
"lib",
"ts4.8",
"ts4.2",
"ts4.0",
"ts3.4",
Expand All @@ -98,9 +107,9 @@
"build:js:node": "BROWSERSLIST_ENV=node12 yarn g:rollup && echo '{\"type\":\"commonjs\"}' > dist/package.json",
"build:js:browser": "BROWSERSLIST_ENV=legacy yarn g:rollup",
"build:bundle": "yarn g:runs build:js:\\*",
"build:clean": "yarn g:clean ts3.4 ts4.0 ts4.2",
"build:clean": "yarn g:clean ts3.4 ts4.0 ts4.2 ts4.8",
"build": "run build:lib && run build:legacy:lib && run build:bundle",
"build:legacy-types": "yarn g:downtypes lib ts3.4 && yarn g:downtypes lib ts4.0 --to=4.0 && yarn g:downtypes lib ts4.2 --to=4.2 && yarn g:copy --up 1 ./src-4.2-types/**/*.d.ts ./ts4.0/ && yarn g:copy --up 1 ./src-4.2-types/**/*.d.ts ./ts4.2 && yarn g:copy --up 1 ./src-4.0-types/**/*.d.ts ./ts3.4/ && yarn g:copy --up 1 ./src-4.0-types/**/*.d.ts ./ts4.0 && yarn g:copy --up 1 ./src-legacy-types/**/*.d.ts ./ts3.4/",
"build:legacy-types": "yarn g:legacy-types 4.8 4.2 4.0 3.4",
"dev": "run build:lib -w",
"prepare": "run build:lib",
"prepack": "run prepare && run build:bundle && run build:legacy:lib",
Expand Down
1 change: 1 addition & 0 deletions packages/endpoint/src-4.8-types/NoInfer.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type NI<T> = T;
2 changes: 2 additions & 0 deletions packages/endpoint/src/NoInfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-4.html#the-noinfer-utility-type */
export type NI<T> = NoInfer<T>;
1 change: 1 addition & 0 deletions packages/endpoint/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export type {
ErrorTypes,
EndpointToFunction,
} from './types.js';
export type { NI } from './NoInfer.js';

export { default as Endpoint, ExtendableEndpoint } from './endpoint.js';
export type { KeyofEndpointInstance } from './endpoint.js';
3 changes: 3 additions & 0 deletions packages/normalizr/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
/dist
/legacy
/index.d.ts
/ts3.4
/ts4.0
/ts4.1
15 changes: 12 additions & 3 deletions packages/normalizr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,22 @@
"module": "legacy/index.js",
"types": "lib/index.d.ts",
"typesVersions": {
">=4.1": {
">=5.4": {
"": [
"lib/index.d.ts"
],
"*": [
"lib/index.d.ts"
]
},
">=4.1": {
"": [
"ts4.1/index.d.ts"
],
"*": [
"ts4.1/index.d.ts"
]
},
">=4.0": {
"": [
"ts4.0/index.d.ts"
Expand Down Expand Up @@ -74,6 +82,7 @@
"lib",
"node.mjs",
"legacy",
"ts4.1",
"ts4.0",
"ts3.4",
"LICENSE",
Expand All @@ -87,8 +96,8 @@
"build:lib": "NODE_ENV=production BROWSERSLIST_ENV='2020' yarn g:babel --out-dir lib",
"build:legacy:lib": "NODE_ENV=production BROWSERSLIST_ENV='2018' yarn g:babel --out-dir legacy",
"build:bundle": "yarn g:runs build:js:\\*",
"build:clean": "yarn g:clean ts3.4 ts4.0",
"build:legacy-types": "yarn g:downtypes lib ts3.4 && yarn g:downtypes lib ts4.0 --to=4.0 && yarn g:copy --up 1 ./src-4.0-types/**/*.d.ts ./ts3.4/ && yarn g:copy --up 1 ./src-4.0-types/**/*.d.ts ./ts4.0",
"build:clean": "yarn g:clean ts3.4 ts4.0 ts4.1",
"build:legacy-types": "yarn g:legacy-types 4.1 4.0 3.4",
"prepublishOnly": "run build"
},
"author": "Nathaniel Tucker <me@ntucker.me> (https://github.com/ntucker)",
Expand Down
1 change: 1 addition & 0 deletions packages/normalizr/src-4.1-types/NoInfer.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type NI<T> = T;
2 changes: 2 additions & 0 deletions packages/normalizr/src/NoInfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-4.html#the-noinfer-utility-type */
export type NI<T> = NoInfer<T>;
1 change: 1 addition & 0 deletions packages/normalizr/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type {
NormalizeNullable,
SchemaArgs,
} from './types.js';
export type { NI } from './NoInfer.js';
export * from './endpoint/types.js';
export * from './interface.js';
export * from './Expiry.js';
Expand Down
3 changes: 0 additions & 3 deletions packages/react/src/hooks/__tests__/useSuspense.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -671,9 +671,6 @@ describe('useSuspense()', () => {
sortBy: 'votes',
});

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO: if you put a string that doesn't match it will complain
// but it somehow grabs the args as what it extends, making it include null
comments.map(comment => comment.title);
}
};
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/hooks/useCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
Schema,
FetchFunction,
ResolveType,
NI,
} from '@data-client/core';
import { ExpiryStatus } from '@data-client/core';
import { useMemo } from 'react';
Expand All @@ -23,10 +24,9 @@ export default function useCache<
EndpointInterface<FetchFunction, Schema | undefined, undefined | boolean>,
'key' | 'schema' | 'invalidIfStale'
>,
Args extends readonly [...Parameters<E['key']>] | readonly [null],
>(
endpoint: E,
...args: Args
...args: NI<readonly [...Parameters<E['key']>] | readonly [null]>
): E['schema'] extends undefined | null ?
E extends (...args: any) => any ?
ResolveType<E> | undefined
Expand Down
59 changes: 39 additions & 20 deletions packages/react/src/hooks/useDLE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,31 @@ import type {
FetchFunction,
Schema,
ResolveType,
NI,
} from '@data-client/core';
import { ExpiryStatus } from '@data-client/core';
import { useMemo } from 'react';

import useCacheState from './useCacheState.js';
import useController from '../hooks/useController.js';

type CondNull<P, A, B> = P extends null ? A : B;

type StatefulReturn<S extends Schema | undefined, P> = CondNull<
P,
{
data: DenormalizeNullable<S>;
loading: false;
error: undefined;
},
type SchemaReturn<S extends Schema | undefined> =
| {
data: Denormalize<S>;
loading: false;
error: undefined;
}
| { data: DenormalizeNullable<S>; loading: true; error: undefined }
| { data: DenormalizeNullable<S>; loading: false; error: ErrorTypes }
>;
| { data: DenormalizeNullable<S>; loading: false; error: ErrorTypes };

type AsyncReturn<E> =
| {
data: E extends (...args: any) => any ? ResolveType<E> : any;
loading: false;
error: undefined;
}
| { data: undefined; loading: true; error: undefined }
| { data: undefined; loading: false; error: ErrorTypes };

/**
* Use async date with { data, loading, error } (DLE)
Expand All @@ -42,17 +43,35 @@ export default function useDLE<
Schema | undefined,
undefined | false
>,
Args extends readonly [...Parameters<E>] | readonly [null],
>(
endpoint: E,
...args: Args
): E['schema'] extends undefined | null ?
{
data: E extends (...args: any) => any ? ResolveType<E> | undefined : any;
loading: boolean;
error: ErrorTypes | undefined;
}
: StatefulReturn<E['schema'], Args[0]> {
...args: readonly [...Parameters<NI<E>>]
): E['schema'] extends undefined | null ? AsyncReturn<E>
: SchemaReturn<E['schema']>;

export default function useDLE<
E extends EndpointInterface<
FetchFunction,
Schema | undefined,
undefined | false
>,
>(
endpoint: E,
...args: readonly [...Parameters<NI<E>>] | readonly [null]
): {
data: E['schema'] extends undefined | null ? undefined
: DenormalizeNullable<E['schema']>;
loading: false;
error: undefined;
};

export default function useDLE<
E extends EndpointInterface<
FetchFunction,
Schema | undefined,
undefined | false
>,
>(endpoint: E, ...args: readonly [...Parameters<E>] | readonly [null]): any {
const state = useCacheState();
const controller = useController();

Expand Down
12 changes: 5 additions & 7 deletions packages/react/src/hooks/useError.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import type { NetworkError, UnknownError } from '@data-client/core';
import type { NI, NetworkError, UnknownError } from '@data-client/core';
import { EndpointInterface } from '@data-client/core';

import useCacheState from './useCacheState.js';
import useController from '../hooks/useController.js';

export type ErrorTypes = NetworkError | UnknownError;

type UseErrorReturn<P> = P extends [null] ? undefined : ErrorTypes | undefined;

/**
* Get any errors for a given request
* @see https://dataclient.io/docs/api/useError
*/
export default function useError<
E extends Pick<EndpointInterface, 'key'>,
Args extends readonly [...Parameters<E['key']>] | readonly [null],
>(endpoint: E, ...args: Args): UseErrorReturn<Args> {
export default function useError<E extends Pick<EndpointInterface, 'key'>>(
endpoint: E,
...args: NI<readonly [...Parameters<E['key']>] | readonly [null]>
): ErrorTypes | undefined {
const state = useCacheState();

const controller = useController();
Expand Down
5 changes: 2 additions & 3 deletions packages/react/src/hooks/useFetch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { ExpiryStatus } from '@data-client/core';
import { ExpiryStatus, NI } from '@data-client/core';
import {
EndpointInterface,
Denormalize,
Expand All @@ -21,8 +21,7 @@ export default function useFetch<
Schema | undefined,
undefined | false
>,
Args extends readonly [...Parameters<E>] | readonly [null],
>(endpoint: E, ...args: Args) {
>(endpoint: E, ...args: NI<readonly [...Parameters<E>] | readonly [null]>) {
const state = useCacheState();
const controller = useController();

Expand Down
31 changes: 28 additions & 3 deletions packages/react/src/hooks/useLive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {
Denormalize,
Schema,
FetchFunction,
ResolveType,
DenormalizeNullable,
NI,
} from '@data-client/core';

import { SuspenseReturn } from './types.js';
import useSubscription from './useSubscription.js';
import useSuspense from './useSuspense.js';

Expand All @@ -24,8 +26,31 @@ export default function useLive<
Schema | undefined,
undefined | false
>,
Args extends readonly [...Parameters<E>] | readonly [null],
>(endpoint: E, ...args: Args): SuspenseReturn<E, Args> {
>(
endpoint: E,
...args: readonly [...Parameters<NI<E>>]
): E['schema'] extends undefined | null ? ResolveType<E>
: Denormalize<E['schema']>;

export default function useLive<
E extends EndpointInterface<
FetchFunction,
Schema | undefined,
undefined | false
>,
>(
endpoint: E,
...args: readonly [...Parameters<NI<E>>] | readonly [null]
): E['schema'] extends undefined | null ? ResolveType<E> | undefined
: DenormalizeNullable<E['schema']>;

export default function useLive<
E extends EndpointInterface<
FetchFunction,
Schema | undefined,
undefined | false
>,
>(endpoint: E, ...args: readonly [...Parameters<E>] | readonly [null]): any {
useSubscription(endpoint, ...args);
return useSuspense(endpoint, ...args);
}
Loading

0 comments on commit dcb6b2f

Please sign in to comment.