From bcd4e36acd720ce147defa5bd45db14cc5deff47 Mon Sep 17 00:00:00 2001 From: Francois Best <github@francoisbest.com> Date: Wed, 26 Jun 2024 09:41:12 +0200 Subject: [PATCH 1/3] feat: Add parser type inference helpers Closes #571. --- README.md | 35 +++++++++++++++++ packages/docs/content/docs/utilities.mdx | 35 +++++++++++++++++ packages/nuqs/src/parsers.ts | 49 ++++++++++++++++++++++++ 3 files changed, 119 insertions(+) diff --git a/README.md b/README.md index ba3abd89a..5a6bbe1ba 100644 --- a/README.md +++ b/README.md @@ -684,6 +684,41 @@ serialize(url, { foo: 'bar' }) // https://example.com/path?baz=qux&foo=bar serialize('?remove=me', { foo: 'bar', remove: null }) // ?foo=bar ``` +## Parser type inference + +To access the underlying type returned by a parser, you can use the +`inferParserType` type helper: + +```ts +import { parseAsInteger, type inferParserType } from 'nuqs' // or 'nuqs/server' + +const intNullable = parseAsInteger +const intNonNull = parseAsInteger.withDefault(0) + +inferParserType<typeof intNullable> // number | null +inferParserType<typeof intNonNull> // number +``` + +For an object describing parsers (that you'd pass to `createSearchParamsCache` +or to `useQueryStates`, you can use the +`inferParserRecordType` helper: + +```ts +import { + parseAsBoolean, + parseAsInteger, + type inferParserRecordType +} from 'nuqs' // or 'nuqs/server' + +const parsers = { + a: parseAsInteger, + b: parseAsBoolean.withDefault(false) +} + +inferParserRecordType<typeof parsers> +// { a: number | null, b: boolean } +``` + ## Testing Currently, the best way to test the behaviour of your components using diff --git a/packages/docs/content/docs/utilities.mdx b/packages/docs/content/docs/utilities.mdx index a5be7beaf..ad9421ed9 100644 --- a/packages/docs/content/docs/utilities.mdx +++ b/packages/docs/content/docs/utilities.mdx @@ -61,3 +61,38 @@ serialize(url, { foo: 'bar' }) // https://example.com/path?baz=qux&foo=bar // Passing null removes existing values serialize('?remove=me', { foo: 'bar', remove: null }) // ?foo=bar ``` + +## Parser type inference + +To access the underlying type returned by a parser, you can use the +`inferParserType` type helper: + +```ts +import { parseAsInteger, type inferParserType } from 'nuqs' // or 'nuqs/server' + +const intNullable = parseAsInteger +const intNonNull = parseAsInteger.withDefault(0) + +inferParserType<typeof intNullable> // number | null +inferParserType<typeof intNonNull> // number +``` + +For an object describing parsers (that you'd pass to [`createSearchParamsCache`](./server-side) +or to [`useQueryStates`](./batching#usequerystates)), you can use the +`inferParserRecordType` helper: + +```ts +import { + parseAsBoolean, + parseAsInteger, + type inferParserRecordType +} from 'nuqs' // or 'nuqs/server' + +const parsers = { + a: parseAsInteger, + b: parseAsBoolean.withDefault(false) +} + +inferParserRecordType<typeof parsers> +// { a: number | null, b: boolean } +``` diff --git a/packages/nuqs/src/parsers.ts b/packages/nuqs/src/parsers.ts index 67e51af14..e22aeccc8 100644 --- a/packages/nuqs/src/parsers.ts +++ b/packages/nuqs/src/parsers.ts @@ -404,3 +404,52 @@ export function parseAsArrayOf<ItemType>( } }) } + +/** + * Type helper to extract the underlying returned data type of a parser. + * + * Usage: + * + * ```ts + * import { type inferParserType } from 'nuqs' // or 'nuqs/server' + * + * const intNullable = parseAsInteger + * const intNonNull = parseAsInteger.withDefault(0) + * + * inferParserType<typeof intNullable> // number | null + * inferParserType<typeof intNonNull> // number + * ``` + */ +export type inferParserType<Parser> = Parser extends ParserBuilder< + infer Type +> & { + defaultValue: infer Type +} + ? Type + : Parser extends ParserBuilder<infer Type> + ? Type | null + : never + +/** + * Type helper to extract the underlying returned data type of an object + * describing multiple parsers and their associated keys. + * + * Usage: + * + * ```ts + * import { type inferParserRecordType } from 'nuqs' // or 'nuqs/server' + * + * const parsers = { + * a: parseAsInteger, + * b: parseAsBoolean.withDefault(false) + * } + * + * inferParserRecordType<typeof parsers> + * // { a: number | null, b: boolean } + * ``` + */ +export type inferParserRecordType< + Map extends Record<string, ParserBuilder<any>> +> = { + [Key in keyof Map]: inferParserType<Map[Key]> +} From 95b8271e4b69cebd55a3262eacbc1802521795ec Mon Sep 17 00:00:00 2001 From: Francois Best <github@francoisbest.com> Date: Fri, 30 Aug 2024 21:45:20 +0200 Subject: [PATCH 2/3] test: Add type testing for inference helpers --- packages/nuqs/package.json | 1 + packages/nuqs/src/tests/parsers.test-d.ts | 23 ++++++++++++++++++++++- pnpm-lock.yaml | 8 ++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/nuqs/package.json b/packages/nuqs/package.json index bf3012efc..741d20661 100644 --- a/packages/nuqs/package.json +++ b/packages/nuqs/package.json @@ -79,6 +79,7 @@ "react": "rc", "react-dom": "rc", "size-limit": "^11.1.2", + "tsafe": "^1.7.2", "tsd": "^0.30.7", "tsup": "^8.0.2", "typescript": "^5.4.5", diff --git a/packages/nuqs/src/tests/parsers.test-d.ts b/packages/nuqs/src/tests/parsers.test-d.ts index 757376ef2..f8a84c562 100644 --- a/packages/nuqs/src/tests/parsers.test-d.ts +++ b/packages/nuqs/src/tests/parsers.test-d.ts @@ -1,6 +1,12 @@ import React from 'react' +import { assert, type Equals } from 'tsafe' import { expectError, expectType } from 'tsd' -import { parseAsString } from '../../dist' +import { + parseAsInteger, + parseAsString, + type inferParserRecordType, + type inferParserType +} from '../../dist' { const p = parseAsString @@ -75,3 +81,18 @@ import { parseAsString } from '../../dist' }) }) } + +// Type inference +assert<Equals<inferParserType<typeof parseAsString>, string | null>>() +const withDefault = parseAsString.withDefault('') +assert<Equals<inferParserType<typeof withDefault>, string>>() +const parsers = { + str: parseAsString, + int: parseAsInteger +} +assert< + Equals< + inferParserRecordType<typeof parsers>, + { str: string | null; int: number | null } + > +>() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44e7a898c..c7b8bbdcc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -251,6 +251,9 @@ importers: size-limit: specifier: ^11.1.2 version: 11.1.2 + tsafe: + specifier: ^1.7.2 + version: 1.7.2 tsd: specifier: ^0.30.7 version: 0.30.7 @@ -5438,6 +5441,9 @@ packages: peerDependencies: typescript: '>=2.7' + tsafe@1.7.2: + resolution: {integrity: sha512-dAPfQLhCfCRre5qs+Z5Q2a7s2CV7RxffZUmvj7puGaePYjECzWREJFd3w4XSFe/T5tbxgowfItA/JSSZ6Ma3dA==} + tsd@0.30.7: resolution: {integrity: sha512-oTiJ28D6B/KXoU3ww/Eji+xqHJojiuPVMwA12g4KYX1O72N93Nb6P3P3h2OAhhf92Xl8NIhb/xFmBZd5zw/xUw==} engines: {node: '>=14.16'} @@ -11819,6 +11825,8 @@ snapshots: typescript: 5.4.5 yn: 3.1.1 + tsafe@1.7.2: {} + tsd@0.30.7: dependencies: '@tsd/typescript': 5.3.3 From c2b490885e8b5d351e63e39761ac499a6c6b05bb Mon Sep 17 00:00:00 2001 From: Francois Best <github@francoisbest.com> Date: Fri, 30 Aug 2024 21:53:06 +0200 Subject: [PATCH 3/3] feat: Provide a single inference helper that handles both singular parser & records --- README.md | 12 ++---- packages/docs/content/docs/utilities.mdx | 8 ++-- packages/nuqs/src/parsers.ts | 46 +++++++++++------------ packages/nuqs/src/tests/parsers.test-d.ts | 9 +---- 4 files changed, 31 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 5a6bbe1ba..12c5709c2 100644 --- a/README.md +++ b/README.md @@ -700,22 +700,18 @@ inferParserType<typeof intNonNull> // number ``` For an object describing parsers (that you'd pass to `createSearchParamsCache` -or to `useQueryStates`, you can use the -`inferParserRecordType` helper: +or to `useQueryStates`, `inferParserType` will +return the type of the object with the parsers replaced by their inferred types: ```ts -import { - parseAsBoolean, - parseAsInteger, - type inferParserRecordType -} from 'nuqs' // or 'nuqs/server' +import { parseAsBoolean, parseAsInteger, type inferParserType } from 'nuqs' // or 'nuqs/server' const parsers = { a: parseAsInteger, b: parseAsBoolean.withDefault(false) } -inferParserRecordType<typeof parsers> +inferParserType<typeof parsers> // { a: number | null, b: boolean } ``` diff --git a/packages/docs/content/docs/utilities.mdx b/packages/docs/content/docs/utilities.mdx index ad9421ed9..eec7f560a 100644 --- a/packages/docs/content/docs/utilities.mdx +++ b/packages/docs/content/docs/utilities.mdx @@ -78,14 +78,14 @@ inferParserType<typeof intNonNull> // number ``` For an object describing parsers (that you'd pass to [`createSearchParamsCache`](./server-side) -or to [`useQueryStates`](./batching#usequerystates)), you can use the -`inferParserRecordType` helper: +or to [`useQueryStates`](./batching#usequerystates)), `inferParserType` will +return the type of the object with the parsers replaced by their inferred types: ```ts import { parseAsBoolean, parseAsInteger, - type inferParserRecordType + type inferParserType } from 'nuqs' // or 'nuqs/server' const parsers = { @@ -93,6 +93,6 @@ const parsers = { b: parseAsBoolean.withDefault(false) } -inferParserRecordType<typeof parsers> +inferParserType<typeof parsers> // { a: number | null, b: boolean } ``` diff --git a/packages/nuqs/src/parsers.ts b/packages/nuqs/src/parsers.ts index e22aeccc8..71176f0d5 100644 --- a/packages/nuqs/src/parsers.ts +++ b/packages/nuqs/src/parsers.ts @@ -405,22 +405,7 @@ export function parseAsArrayOf<ItemType>( }) } -/** - * Type helper to extract the underlying returned data type of a parser. - * - * Usage: - * - * ```ts - * import { type inferParserType } from 'nuqs' // or 'nuqs/server' - * - * const intNullable = parseAsInteger - * const intNonNull = parseAsInteger.withDefault(0) - * - * inferParserType<typeof intNullable> // number | null - * inferParserType<typeof intNonNull> // number - * ``` - */ -export type inferParserType<Parser> = Parser extends ParserBuilder< +type inferSingleParserType<Parser> = Parser extends ParserBuilder< infer Type > & { defaultValue: infer Type @@ -430,26 +415,37 @@ export type inferParserType<Parser> = Parser extends ParserBuilder< ? Type | null : never +type inferParserRecordType<Map extends Record<string, ParserBuilder<any>>> = { + [Key in keyof Map]: inferSingleParserType<Map[Key]> +} + /** - * Type helper to extract the underlying returned data type of an object - * describing multiple parsers and their associated keys. + * Type helper to extract the underlying returned data type of a parser + * or of an object describing multiple parsers and their associated keys. * * Usage: * * ```ts - * import { type inferParserRecordType } from 'nuqs' // or 'nuqs/server' + * import { type inferParserType } from 'nuqs' // or 'nuqs/server' + * + * const intNullable = parseAsInteger + * const intNonNull = parseAsInteger.withDefault(0) + * + * inferParserType<typeof intNullable> // number | null + * inferParserType<typeof intNonNull> // number * * const parsers = { * a: parseAsInteger, * b: parseAsBoolean.withDefault(false) * } * - * inferParserRecordType<typeof parsers> + * inferParserType<typeof parsers> * // { a: number | null, b: boolean } * ``` */ -export type inferParserRecordType< - Map extends Record<string, ParserBuilder<any>> -> = { - [Key in keyof Map]: inferParserType<Map[Key]> -} +export type inferParserType<Input> = + Input extends ParserBuilder<any> + ? inferSingleParserType<Input> + : Input extends Record<string, ParserBuilder<any>> + ? inferParserRecordType<Input> + : never diff --git a/packages/nuqs/src/tests/parsers.test-d.ts b/packages/nuqs/src/tests/parsers.test-d.ts index f8a84c562..2ba2a16fb 100644 --- a/packages/nuqs/src/tests/parsers.test-d.ts +++ b/packages/nuqs/src/tests/parsers.test-d.ts @@ -1,12 +1,7 @@ import React from 'react' import { assert, type Equals } from 'tsafe' import { expectError, expectType } from 'tsd' -import { - parseAsInteger, - parseAsString, - type inferParserRecordType, - type inferParserType -} from '../../dist' +import { parseAsInteger, parseAsString, type inferParserType } from '../../dist' { const p = parseAsString @@ -92,7 +87,7 @@ const parsers = { } assert< Equals< - inferParserRecordType<typeof parsers>, + inferParserType<typeof parsers>, { str: string | null; int: number | null } > >()