diff --git a/packages/client/lib/RESP/types.spec.ts b/packages/client/lib/RESP/types.spec.ts new file mode 100644 index 00000000000..ce9693afc46 --- /dev/null +++ b/packages/client/lib/RESP/types.spec.ts @@ -0,0 +1,99 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { RESP_TYPES } from './decoder'; + +describe('RESP Type Mapping', () => { + testUtils.testWithClient('type mappings', async client => { + // Scalar Primitives + // INTEGER: EXISTS returns number (0|1) + const existsRes = await client.withTypeMapping({}).exists('some_key'); + assert.strictEqual(typeof existsRes, 'number'); + + // BIG_NUMBER: maps to bigint when configured + const bigNumRes = await client + .withTypeMapping({ + [RESP_TYPES.BIG_NUMBER]: BigInt + }) + .hello(); + assert.ok( + typeof bigNumRes === 'bigint' || + typeof bigNumRes === 'number' || + typeof bigNumRes === 'object' + ); + + // DOUBLE: maps to number when configured + // Use ZINCRBY which returns a DoubleReply + const doubleRes = await client + .withTypeMapping({ + [RESP_TYPES.DOUBLE]: Number + }) + .zIncrBy('zset-double-test', 1.5, 'member'); + assert.strictEqual(typeof doubleRes, 'number'); + assert.strictEqual(doubleRes, 1.5); + + // Complex Strings + // VERBATIM_STRING maps to string + const verbatimRes = await client + .withTypeMapping({ + [RESP_TYPES.VERBATIM_STRING]: String + }) + .get('key'); + assert.ok( + verbatimRes === null || + typeof verbatimRes === 'string' || + Buffer.isBuffer(verbatimRes) + ); + + // Recursive Collections + // ARRAY infers nested mapped types + const arrayRes = await client + .withTypeMapping({ + [RESP_TYPES.BLOB_STRING]: String + }) + .lRange('key', 0, -1); + assert.ok(Array.isArray(arrayRes)); + + // SET infers Set or array + const setRes = await client + .withTypeMapping({ + [RESP_TYPES.BLOB_STRING]: String + }) + .sMembers('key'); + assert.ok(setRes instanceof Set || Array.isArray(setRes)); + + // MAP infers Map or object + const mapRes = await client + .withTypeMapping({ + [RESP_TYPES.BLOB_STRING]: String + }) + .hGetAll('key'); + assert.ok(mapRes instanceof Map || typeof mapRes === 'object'); + + // Edge Cases + // SIMPLE_ERROR remains Error + try { + await client + .withTypeMapping({ + [RESP_TYPES.SIMPLE_ERROR]: Error + }) + .hello(); + assert.fail('Expected error'); + } catch (e) { + assert.ok(e instanceof Error); + } + + // NULL always remains null + const nullRes = await client + .withTypeMapping({}) + .get('missing-key-random-12345'); + assert.strictEqual(nullRes, null); + + // hGet infers string | null + const hGetRes = await client + .withTypeMapping({ + [RESP_TYPES.BLOB_STRING]: String + }) + .hGet('foo', 'bar'); + assert.ok(hGetRes === null || typeof hGetRes === 'string'); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/RESP/types.ts b/packages/client/lib/RESP/types.ts index 8749bbdc7b0..d1d26e6ccbf 100644 --- a/packages/client/lib/RESP/types.ts +++ b/packages/client/lib/RESP/types.ts @@ -27,14 +27,14 @@ export interface RespType< export interface NullReply extends RespType< RESP_TYPES['NULL'], null -> {} +> { } export interface BooleanReply< T extends boolean = boolean > extends RespType< RESP_TYPES['BOOLEAN'], T -> {} +> { } export interface NumberReply< T extends number = number @@ -43,7 +43,7 @@ export interface NumberReply< T, `${T}`, number | string -> {} +> { } export interface BigNumberReply< T extends bigint = bigint @@ -52,7 +52,7 @@ export interface BigNumberReply< T, number | `${T}`, bigint | number | string -> {} +> { } export interface DoubleReply< T extends number = number @@ -61,7 +61,7 @@ export interface DoubleReply< T, `${T}`, number | string -> {} +> { } export interface SimpleStringReply< T extends string = string @@ -70,7 +70,7 @@ export interface SimpleStringReply< T, Buffer, string | Buffer -> {} +> { } export interface BlobStringReply< T extends string = string @@ -90,65 +90,65 @@ export interface VerbatimStringReply< T, Buffer | VerbatimString, string | Buffer | VerbatimString -> {} +> { } export interface SimpleErrorReply extends RespType< RESP_TYPES['SIMPLE_ERROR'], SimpleError, Buffer -> {} +> { } export interface BlobErrorReply extends RespType< RESP_TYPES['BLOB_ERROR'], BlobError, Buffer -> {} +> { } export interface ArrayReply extends RespType< RESP_TYPES['ARRAY'], Array, never, Array -> {} +> { } export interface TuplesReply]> extends RespType< RESP_TYPES['ARRAY'], T, never, Array -> {} +> { } export interface SetReply extends RespType< RESP_TYPES['SET'], Array, Set, Array | Set -> {} +> { } export interface MapReply extends RespType< RESP_TYPES['MAP'], { [key: string]: V }, Map | Array, Map | Array -> {} +> { } type MapKeyValue = [key: BlobStringReply | SimpleStringReply, value: unknown]; type MapTuples = Array; type ExtractMapKey = ( - T extends BlobStringReply ? S : - T extends SimpleStringReply ? S : - never + T extends BlobStringReply ? S : + T extends SimpleStringReply ? S : + never ); export interface TuplesToMapReply extends RespType< RESP_TYPES['MAP'], { - [P in T[number] as ExtractMapKey]: P[1]; + [P in T[number]as ExtractMapKey]: P[1]; }, Map, T[number][1]> | FlattenTuples -> {} +> { } type FlattenTuples = ( T extends [] ? [] : @@ -193,32 +193,38 @@ type MapKey< [RESP_TYPES.BLOB_STRING]: StringConstructor; }>; +type UnwrapConstructor = + T extends StringConstructor ? string : + T extends NumberConstructor ? number : + T extends BooleanConstructor ? boolean : + T extends BigIntConstructor ? bigint : + T; export type UnwrapReply> = REPLY['DEFAULT' | 'TYPES']; export type ReplyWithTypeMapping< REPLY, TYPE_MAPPING extends TypeMapping > = ( - // if REPLY is a type, extract the coresponding type from TYPE_MAPPING or use the default type - REPLY extends RespType ? + // if REPLY is a type, extract the coresponding type from TYPE_MAPPING or use the default type + REPLY extends RespType ? TYPE_MAPPING[RESP_TYPE] extends MappedType ? - ReplyWithTypeMapping, TYPE_MAPPING> : - ReplyWithTypeMapping - : ( - // if REPLY is a known generic type, convert its generic arguments - // TODO: tuples? - REPLY extends Array ? Array> : - REPLY extends Set ? Set> : - REPLY extends Map ? Map, ReplyWithTypeMapping> : - // `Date | Buffer | Error` are supersets of `Record`, so they need to be checked first - REPLY extends Date | Buffer | Error ? REPLY : - REPLY extends Record ? { - [P in keyof REPLY]: ReplyWithTypeMapping; - } : - // otherwise, just return the REPLY as is - REPLY - ) -); + ReplyWithTypeMapping>, TYPE_MAPPING> : + ReplyWithTypeMapping + : ( + // if REPLY is a known generic type, convert its generic arguments + // TODO: tuples? + REPLY extends Array ? Array> : + REPLY extends Set ? Set> : + REPLY extends Map ? Map, ReplyWithTypeMapping> : + // `Date | Buffer | Error` are supersets of `Record`, so they need to be checked first + REPLY extends Date | Buffer | Error ? REPLY : + REPLY extends Record ? { + [P in keyof REPLY]: ReplyWithTypeMapping; + } : + // otherwise, just return the REPLY as is + REPLY + ) + ); export type TransformReply = (this: void, reply: any, preserve?: any, typeMapping?: TypeMapping) => any; // TODO; @@ -342,17 +348,17 @@ type Resp2Array = ( export type Resp2Reply = ( RESP3REPLY extends RespType ? - // TODO: RESP3 only scalar types - RESP_TYPE extends RESP_TYPES['DOUBLE'] ? BlobStringReply : - RESP_TYPE extends RESP_TYPES['ARRAY'] | RESP_TYPES['SET'] ? RespType< - RESP_TYPE, - Resp2Array - > : - RESP_TYPE extends RESP_TYPES['MAP'] ? RespType< - RESP_TYPES['ARRAY'], - Resp2Array>> - > : - RESP3REPLY : + // TODO: RESP3 only scalar types + RESP_TYPE extends RESP_TYPES['DOUBLE'] ? BlobStringReply : + RESP_TYPE extends RESP_TYPES['ARRAY'] | RESP_TYPES['SET'] ? RespType< + RESP_TYPE, + Resp2Array + > : + RESP_TYPE extends RESP_TYPES['MAP'] ? RespType< + RESP_TYPES['ARRAY'], + Resp2Array>> + > : + RESP3REPLY : RESP3REPLY ); @@ -362,13 +368,13 @@ export type CommandReply< COMMAND extends Command, RESP extends RespVersions > = ( - // if transformReply is a function, use its return type - COMMAND['transformReply'] extends (...args: any) => infer T ? T : - // if transformReply[RESP] is a function, use its return type - COMMAND['transformReply'] extends Record infer T> ? T : - // otherwise use the generic reply type - ReplyUnion -); + // if transformReply is a function, use its return type + COMMAND['transformReply'] extends (...args: any) => infer T ? T : + // if transformReply[RESP] is a function, use its return type + COMMAND['transformReply'] extends Record infer T> ? T : + // otherwise use the generic reply type + ReplyUnion + ); export type CommandSignature< COMMAND extends Command,