Skip to content

Commit 7f256b0

Browse files
fix: unwrap constructors to primitives in type mapping (redis#3174)
* fix(client): unwrap constructors to primitives in type mapping Resolves: redis#2987 * Addressed Timeout issue in asynchorous tests * Updated tests * test: add DOUBLE type mapping test using ZINCRBY - Uses zIncrBy which actually returns a DoubleReply - Verifies RESP_TYPES.DOUBLE maps to Number correctly - Replaces previous test that incorrectly used hello() command --------- Co-authored-by: Nikolay Karadzhov <nkaradzhov89@gmail.com>
1 parent 8d07299 commit 7f256b0

File tree

2 files changed

+160
-55
lines changed

2 files changed

+160
-55
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { strict as assert } from 'node:assert';
2+
import testUtils, { GLOBAL } from '../test-utils';
3+
import { RESP_TYPES } from './decoder';
4+
5+
describe('RESP Type Mapping', () => {
6+
testUtils.testWithClient('type mappings', async client => {
7+
// Scalar Primitives
8+
// INTEGER: EXISTS returns number (0|1)
9+
const existsRes = await client.withTypeMapping({}).exists('some_key');
10+
assert.strictEqual(typeof existsRes, 'number');
11+
12+
// BIG_NUMBER: maps to bigint when configured
13+
const bigNumRes = await client
14+
.withTypeMapping({
15+
[RESP_TYPES.BIG_NUMBER]: BigInt
16+
})
17+
.hello();
18+
assert.ok(
19+
typeof bigNumRes === 'bigint' ||
20+
typeof bigNumRes === 'number' ||
21+
typeof bigNumRes === 'object'
22+
);
23+
24+
// DOUBLE: maps to number when configured
25+
// Use ZINCRBY which returns a DoubleReply
26+
const doubleRes = await client
27+
.withTypeMapping({
28+
[RESP_TYPES.DOUBLE]: Number
29+
})
30+
.zIncrBy('zset-double-test', 1.5, 'member');
31+
assert.strictEqual(typeof doubleRes, 'number');
32+
assert.strictEqual(doubleRes, 1.5);
33+
34+
// Complex Strings
35+
// VERBATIM_STRING maps to string
36+
const verbatimRes = await client
37+
.withTypeMapping({
38+
[RESP_TYPES.VERBATIM_STRING]: String
39+
})
40+
.get('key');
41+
assert.ok(
42+
verbatimRes === null ||
43+
typeof verbatimRes === 'string' ||
44+
Buffer.isBuffer(verbatimRes)
45+
);
46+
47+
// Recursive Collections
48+
// ARRAY infers nested mapped types
49+
const arrayRes = await client
50+
.withTypeMapping({
51+
[RESP_TYPES.BLOB_STRING]: String
52+
})
53+
.lRange('key', 0, -1);
54+
assert.ok(Array.isArray(arrayRes));
55+
56+
// SET infers Set or array
57+
const setRes = await client
58+
.withTypeMapping({
59+
[RESP_TYPES.BLOB_STRING]: String
60+
})
61+
.sMembers('key');
62+
assert.ok(setRes instanceof Set || Array.isArray(setRes));
63+
64+
// MAP infers Map or object
65+
const mapRes = await client
66+
.withTypeMapping({
67+
[RESP_TYPES.BLOB_STRING]: String
68+
})
69+
.hGetAll('key');
70+
assert.ok(mapRes instanceof Map || typeof mapRes === 'object');
71+
72+
// Edge Cases
73+
// SIMPLE_ERROR remains Error
74+
try {
75+
await client
76+
.withTypeMapping({
77+
[RESP_TYPES.SIMPLE_ERROR]: Error
78+
})
79+
.hello();
80+
assert.fail('Expected error');
81+
} catch (e) {
82+
assert.ok(e instanceof Error);
83+
}
84+
85+
// NULL always remains null
86+
const nullRes = await client
87+
.withTypeMapping({})
88+
.get('missing-key-random-12345');
89+
assert.strictEqual(nullRes, null);
90+
91+
// hGet infers string | null
92+
const hGetRes = await client
93+
.withTypeMapping({
94+
[RESP_TYPES.BLOB_STRING]: String
95+
})
96+
.hGet('foo', 'bar');
97+
assert.ok(hGetRes === null || typeof hGetRes === 'string');
98+
}, GLOBAL.SERVERS.OPEN);
99+
});

packages/client/lib/RESP/types.ts

Lines changed: 61 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ export interface RespType<
2727
export interface NullReply extends RespType<
2828
RESP_TYPES['NULL'],
2929
null
30-
> {}
30+
> { }
3131

3232
export interface BooleanReply<
3333
T extends boolean = boolean
3434
> extends RespType<
3535
RESP_TYPES['BOOLEAN'],
3636
T
37-
> {}
37+
> { }
3838

3939
export interface NumberReply<
4040
T extends number = number
@@ -43,7 +43,7 @@ export interface NumberReply<
4343
T,
4444
`${T}`,
4545
number | string
46-
> {}
46+
> { }
4747

4848
export interface BigNumberReply<
4949
T extends bigint = bigint
@@ -52,7 +52,7 @@ export interface BigNumberReply<
5252
T,
5353
number | `${T}`,
5454
bigint | number | string
55-
> {}
55+
> { }
5656

5757
export interface DoubleReply<
5858
T extends number = number
@@ -61,7 +61,7 @@ export interface DoubleReply<
6161
T,
6262
`${T}`,
6363
number | string
64-
> {}
64+
> { }
6565

6666
export interface SimpleStringReply<
6767
T extends string = string
@@ -70,7 +70,7 @@ export interface SimpleStringReply<
7070
T,
7171
Buffer,
7272
string | Buffer
73-
> {}
73+
> { }
7474

7575
export interface BlobStringReply<
7676
T extends string = string
@@ -90,65 +90,65 @@ export interface VerbatimStringReply<
9090
T,
9191
Buffer | VerbatimString,
9292
string | Buffer | VerbatimString
93-
> {}
93+
> { }
9494

9595
export interface SimpleErrorReply extends RespType<
9696
RESP_TYPES['SIMPLE_ERROR'],
9797
SimpleError,
9898
Buffer
99-
> {}
99+
> { }
100100

101101
export interface BlobErrorReply extends RespType<
102102
RESP_TYPES['BLOB_ERROR'],
103103
BlobError,
104104
Buffer
105-
> {}
105+
> { }
106106

107107
export interface ArrayReply<T> extends RespType<
108108
RESP_TYPES['ARRAY'],
109109
Array<T>,
110110
never,
111111
Array<any>
112-
> {}
112+
> { }
113113

114114
export interface TuplesReply<T extends [...Array<unknown>]> extends RespType<
115115
RESP_TYPES['ARRAY'],
116116
T,
117117
never,
118118
Array<any>
119-
> {}
119+
> { }
120120

121121
export interface SetReply<T> extends RespType<
122122
RESP_TYPES['SET'],
123123
Array<T>,
124124
Set<T>,
125125
Array<any> | Set<any>
126-
> {}
126+
> { }
127127

128128
export interface MapReply<K, V> extends RespType<
129129
RESP_TYPES['MAP'],
130130
{ [key: string]: V },
131131
Map<K, V> | Array<K | V>,
132132
Map<any, any> | Array<any>
133-
> {}
133+
> { }
134134

135135
type MapKeyValue = [key: BlobStringReply | SimpleStringReply, value: unknown];
136136

137137
type MapTuples = Array<MapKeyValue>;
138138

139139
type ExtractMapKey<T> = (
140-
T extends BlobStringReply<infer S> ? S :
141-
T extends SimpleStringReply<infer S> ? S :
142-
never
140+
T extends BlobStringReply<infer S> ? S :
141+
T extends SimpleStringReply<infer S> ? S :
142+
never
143143
);
144144

145145
export interface TuplesToMapReply<T extends MapTuples> extends RespType<
146146
RESP_TYPES['MAP'],
147147
{
148-
[P in T[number] as ExtractMapKey<P[0]>]: P[1];
148+
[P in T[number]as ExtractMapKey<P[0]>]: P[1];
149149
},
150150
Map<ExtractMapKey<T[number][0]>, T[number][1]> | FlattenTuples<T>
151-
> {}
151+
> { }
152152

153153
type FlattenTuples<T> = (
154154
T extends [] ? [] :
@@ -193,32 +193,38 @@ type MapKey<
193193
[RESP_TYPES.BLOB_STRING]: StringConstructor;
194194
}>;
195195

196+
type UnwrapConstructor<T> =
197+
T extends StringConstructor ? string :
198+
T extends NumberConstructor ? number :
199+
T extends BooleanConstructor ? boolean :
200+
T extends BigIntConstructor ? bigint :
201+
T;
196202
export type UnwrapReply<REPLY extends RespType<any, any, any, any>> = REPLY['DEFAULT' | 'TYPES'];
197203

198204
export type ReplyWithTypeMapping<
199205
REPLY,
200206
TYPE_MAPPING extends TypeMapping
201207
> = (
202-
// if REPLY is a type, extract the coresponding type from TYPE_MAPPING or use the default type
203-
REPLY extends RespType<infer RESP_TYPE, infer DEFAULT, infer TYPES, unknown> ?
208+
// if REPLY is a type, extract the coresponding type from TYPE_MAPPING or use the default type
209+
REPLY extends RespType<infer RESP_TYPE, infer DEFAULT, infer TYPES, unknown> ?
204210
TYPE_MAPPING[RESP_TYPE] extends MappedType<infer T> ?
205-
ReplyWithTypeMapping<Extract<DEFAULT | TYPES, T>, TYPE_MAPPING> :
206-
ReplyWithTypeMapping<DEFAULT, TYPE_MAPPING>
207-
: (
208-
// if REPLY is a known generic type, convert its generic arguments
209-
// TODO: tuples?
210-
REPLY extends Array<infer T> ? Array<ReplyWithTypeMapping<T, TYPE_MAPPING>> :
211-
REPLY extends Set<infer T> ? Set<ReplyWithTypeMapping<T, TYPE_MAPPING>> :
212-
REPLY extends Map<infer K, infer V> ? Map<MapKey<K, TYPE_MAPPING>, ReplyWithTypeMapping<V, TYPE_MAPPING>> :
213-
// `Date | Buffer | Error` are supersets of `Record`, so they need to be checked first
214-
REPLY extends Date | Buffer | Error ? REPLY :
215-
REPLY extends Record<PropertyKey, any> ? {
216-
[P in keyof REPLY]: ReplyWithTypeMapping<REPLY[P], TYPE_MAPPING>;
217-
} :
218-
// otherwise, just return the REPLY as is
219-
REPLY
220-
)
221-
);
211+
ReplyWithTypeMapping<Extract<DEFAULT | TYPES, UnwrapConstructor<T>>, TYPE_MAPPING> :
212+
ReplyWithTypeMapping<DEFAULT, TYPE_MAPPING>
213+
: (
214+
// if REPLY is a known generic type, convert its generic arguments
215+
// TODO: tuples?
216+
REPLY extends Array<infer T> ? Array<ReplyWithTypeMapping<T, TYPE_MAPPING>> :
217+
REPLY extends Set<infer T> ? Set<ReplyWithTypeMapping<T, TYPE_MAPPING>> :
218+
REPLY extends Map<infer K, infer V> ? Map<MapKey<K, TYPE_MAPPING>, ReplyWithTypeMapping<V, TYPE_MAPPING>> :
219+
// `Date | Buffer | Error` are supersets of `Record`, so they need to be checked first
220+
REPLY extends Date | Buffer | Error ? REPLY :
221+
REPLY extends Record<PropertyKey, any> ? {
222+
[P in keyof REPLY]: ReplyWithTypeMapping<REPLY[P], TYPE_MAPPING>;
223+
} :
224+
// otherwise, just return the REPLY as is
225+
REPLY
226+
)
227+
);
222228

223229
export type TransformReply = (this: void, reply: any, preserve?: any, typeMapping?: TypeMapping) => any; // TODO;
224230

@@ -342,17 +348,17 @@ type Resp2Array<T> = (
342348

343349
export type Resp2Reply<RESP3REPLY> = (
344350
RESP3REPLY extends RespType<infer RESP_TYPE, infer DEFAULT, infer TYPES, unknown> ?
345-
// TODO: RESP3 only scalar types
346-
RESP_TYPE extends RESP_TYPES['DOUBLE'] ? BlobStringReply :
347-
RESP_TYPE extends RESP_TYPES['ARRAY'] | RESP_TYPES['SET'] ? RespType<
348-
RESP_TYPE,
349-
Resp2Array<DEFAULT>
350-
> :
351-
RESP_TYPE extends RESP_TYPES['MAP'] ? RespType<
352-
RESP_TYPES['ARRAY'],
353-
Resp2Array<Extract<TYPES, Array<any>>>
354-
> :
355-
RESP3REPLY :
351+
// TODO: RESP3 only scalar types
352+
RESP_TYPE extends RESP_TYPES['DOUBLE'] ? BlobStringReply :
353+
RESP_TYPE extends RESP_TYPES['ARRAY'] | RESP_TYPES['SET'] ? RespType<
354+
RESP_TYPE,
355+
Resp2Array<DEFAULT>
356+
> :
357+
RESP_TYPE extends RESP_TYPES['MAP'] ? RespType<
358+
RESP_TYPES['ARRAY'],
359+
Resp2Array<Extract<TYPES, Array<any>>>
360+
> :
361+
RESP3REPLY :
356362
RESP3REPLY
357363
);
358364

@@ -362,13 +368,13 @@ export type CommandReply<
362368
COMMAND extends Command,
363369
RESP extends RespVersions
364370
> = (
365-
// if transformReply is a function, use its return type
366-
COMMAND['transformReply'] extends (...args: any) => infer T ? T :
367-
// if transformReply[RESP] is a function, use its return type
368-
COMMAND['transformReply'] extends Record<RESP, (...args: any) => infer T> ? T :
369-
// otherwise use the generic reply type
370-
ReplyUnion
371-
);
371+
// if transformReply is a function, use its return type
372+
COMMAND['transformReply'] extends (...args: any) => infer T ? T :
373+
// if transformReply[RESP] is a function, use its return type
374+
COMMAND['transformReply'] extends Record<RESP, (...args: any) => infer T> ? T :
375+
// otherwise use the generic reply type
376+
ReplyUnion
377+
);
372378

373379
export type CommandSignature<
374380
COMMAND extends Command,

0 commit comments

Comments
 (0)