Skip to content

Commit f3ae52f

Browse files
committed
refactor: improve typing by adding generics to setItem methods
- Add generic type parameter `<T>` to setItem methods in all interfaces allowing type-safe content storage: `setItem<T>(key, content: T | undefined)` - Update AbstractBaseStrategy and ExpirationStrategy with generic setItem - Update all storage implementations with generic setItem methods - Narrow intermediate deserialization types in Redis storages from `unknown` to more specific union types (e.g., `string | T` during JSON parsing) - Fix test to use explicit union type for mixed content arrays This improves type inference and allows consumers to get compile-time type checking when storing typed data in the cache.
1 parent b43778e commit f3ae52f

11 files changed

Lines changed: 73 additions & 48 deletions

File tree

storages/lru-redis/src/LRUWithRedisStorage.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ export class LRUWithRedisStorage implements IAsynchronousCacheType {
4545
}
4646

4747
/** ttl in seconds! */
48-
public async setItem(key: string, content: unknown, options?: { ttl?: number }): Promise<void> {
48+
public async setItem<T = unknown>(
49+
key: string,
50+
content: T | undefined,
51+
options?: { ttl?: number }
52+
): Promise<void> {
4953
this.myCache.set(key, content);
5054
if (this.options?.maxAge) {
5155
await this.redis().setex(key, options?.ttl || this.options.maxAge, JSON.stringify(content));

storages/lru/src/LRUStorage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class LRUStorage implements ISynchronousCacheType, IMultiSynchronousCache
1818
};
1919
}
2020

21-
setItems(values: { key: string; content: unknown }[]): void {
21+
setItems<T = unknown>(values: { key: string; content: T | undefined }[]): void {
2222
values.forEach(val => {
2323
this.myCache.set(val.key, val.content);
2424
});
@@ -28,7 +28,7 @@ export class LRUStorage implements ISynchronousCacheType, IMultiSynchronousCache
2828
return this.myCache.get(key) as T | undefined;
2929
}
3030

31-
public setItem(key: string, content: unknown): void {
31+
public setItem<T = unknown>(key: string, content: T | undefined): void {
3232
this.myCache.set(key, content);
3333
}
3434

storages/node-cache/src/node-cache.storage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ export class NodeCacheStorage implements ISynchronousCacheType, IMultiSynchronou
1313
return this.myCache.mget(keys);
1414
}
1515

16-
setItems(values: { key: string; content: unknown }[]): void {
16+
setItems<T = unknown>(values: { key: string; content: T | undefined }[]): void {
1717
this.myCache.mset(values.map(v => ({ key: v.key, val: v.content })));
1818
}
1919

2020
public getItem<T>(key: string): T | undefined {
2121
return this.myCache.get(key) || undefined;
2222
}
2323

24-
public setItem(key: string, content: unknown): void {
24+
public setItem<T = unknown>(key: string, content: T | undefined): void {
2525
this.myCache.set(key, content);
2626
}
2727

storages/redis/src/redis.storage.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,23 @@ export class RedisStorage implements IAsynchronousCacheType {
2020
if (entry === null) {
2121
return undefined;
2222
}
23-
let finalItem: unknown = entry;
23+
// Try to parse as JSON, fallback to raw string
24+
let parsedItem: T | string = entry;
2425
try {
25-
finalItem = JSON.parse(entry);
26+
parsedItem = JSON.parse(entry) as T;
2627
} catch (error) {
27-
/** ignore */
28+
/** Not JSON, keep as string */
2829
}
29-
return finalItem as T | undefined;
30+
return parsedItem as T | undefined;
3031
}
3132

32-
public async setItem(key: string, content: unknown): Promise<void> {
33-
let stringContent: string;
33+
public async setItem<T = unknown>(key: string, content: T | undefined): Promise<void> {
3434
if (content === undefined) {
3535
await this.client.delAsync(key);
3636
return;
3737
}
38-
if (typeof content === 'object') {
39-
stringContent = JSON.stringify(content);
40-
} else {
41-
stringContent = String(content);
42-
}
38+
const stringContent: string =
39+
typeof content === 'object' ? JSON.stringify(content) : String(content);
4340
await this.client.setAsync(key, stringContent);
4441
}
4542

storages/redisio/src/redisio.storage.ts

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,23 @@ export class RedisIOStorage implements IAsynchronousCacheType, IMultiIAsynchrono
3232
return [keys[i], null]; // value does exist, but is empty
3333
}
3434

35-
let finalItem: unknown =
35+
// Decompress if needed, result is a string
36+
const stringValue: string =
3637
entry && this.options.compress
3738
? await this.uncompress(entry as Buffer)
3839
: (entry as string);
3940

41+
// Try to parse as JSON
42+
let parsedItem: T | string = stringValue;
4043
try {
41-
finalItem = finalItem && JSON.parse(finalItem as string);
44+
if (stringValue) {
45+
parsedItem = JSON.parse(stringValue) as T;
46+
}
4247
} catch (error) {
43-
/** ignore */
48+
/** Not JSON, keep as string */
4449
}
4550

46-
return [keys[i], finalItem];
51+
return [keys[i], parsedItem];
4752
})
4853
)
4954
);
@@ -61,8 +66,8 @@ export class RedisIOStorage implements IAsynchronousCacheType, IMultiIAsynchrono
6166
return result.toString();
6267
}
6368

64-
async setItems(
65-
values: { key: string; content: unknown }[],
69+
async setItems<T = unknown>(
70+
values: { key: string; content: T | undefined }[],
6671
options?: { ttl?: number }
6772
): Promise<void> {
6873
const redisPipeline = this.redis().pipeline();
@@ -95,7 +100,7 @@ export class RedisIOStorage implements IAsynchronousCacheType, IMultiIAsynchrono
95100
}
96101

97102
public async getItem<T>(key: string): Promise<T | undefined> {
98-
const entry = this.options.compress
103+
const entry: Buffer | string | null = this.options.compress
99104
? await this.redis().getBuffer(key)
100105
: await this.redis().get(key);
101106
if (entry === null) {
@@ -104,36 +109,44 @@ export class RedisIOStorage implements IAsynchronousCacheType, IMultiIAsynchrono
104109
if (entry === '') {
105110
return null as T; // value exists but is empty
106111
}
107-
let finalItem: unknown =
112+
113+
// Decompress if needed, result is a string
114+
const stringValue: string =
108115
entry && this.options.compress ? await this.uncompress(entry as Buffer) : (entry as string);
109116

117+
// Try to parse as JSON
118+
let parsedItem: T | string = stringValue;
110119
try {
111-
finalItem = JSON.parse(finalItem as string);
120+
parsedItem = JSON.parse(stringValue) as T;
112121
} catch (error) {
113-
/** ignore */
122+
/** Not JSON, keep as string */
114123
}
115-
return finalItem as T;
124+
return parsedItem as T;
116125
}
117126

118-
public async setItem(key: string, content: unknown, options?: { ttl?: number }): Promise<void> {
119-
if (typeof content === 'object') {
120-
content = JSON.stringify(content);
121-
} else if (content === undefined) {
127+
public async setItem<T = unknown>(
128+
key: string,
129+
content: T | undefined,
130+
options?: { ttl?: number }
131+
): Promise<void> {
132+
if (content === undefined) {
122133
await this.redis().del(key);
123134
return;
124135
}
125136

137+
// Serialize to string, then optionally compress
138+
let serialized: string | Buffer =
139+
typeof content === 'object' ? JSON.stringify(content) : String(content);
140+
126141
if (this.options.compress) {
127-
content = await this.compress(content as string);
142+
serialized = await this.compress(serialized);
128143
}
129144

130145
const ttl = options?.ttl ?? this.options.maxAge;
131-
let savePromise: Promise<'OK' | null>;
132-
if (ttl) {
133-
savePromise = this.redis().setex(key, ttl, content as Buffer | string);
134-
} else {
135-
savePromise = this.redis().set(key, content as Buffer | string);
136-
}
146+
const savePromise: Promise<'OK' | null> = ttl
147+
? this.redis().setex(key, ttl, serialized)
148+
: this.redis().set(key, serialized);
149+
137150
if (this.errorHandler) {
138151
// if we have an error handler, we do not need to await the result
139152
savePromise.catch(err => this.errorHandler && this.errorHandler(err));

storages/redisio/test/redis.storage.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ describe('RedisIOStorage', () => {
8383
});
8484

8585
it('Mutli Should set and retrieve item correclty', async () => {
86-
await storage.setItems([
86+
await storage.setItems<{ asdf: number } | string>([
8787
{ key: 'test', content: { asdf: 2 } },
8888
{ key: 'test2', content: '2' }
8989
]);

ts-cache/src/storage/fs/fs.json.storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class FsJsonStorage implements IAsynchronousCacheType {
1414
return (await this.getCacheObject())[key] as T | undefined;
1515
}
1616

17-
public async setItem(key: string, content: unknown): Promise<void> {
17+
public async setItem<T = unknown>(key: string, content: T | undefined): Promise<void> {
1818
const cache = await this.getCacheObject();
1919
cache[key] = content;
2020
await this.setCache(cache);

ts-cache/src/storage/memory/memory.storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export class MemoryStorage implements ISynchronousCacheType {
77
return this.memCache[key] as T | undefined;
88
}
99

10-
public setItem(key: string, content: unknown): void {
10+
public setItem<T = unknown>(key: string, content: T | undefined): void {
1111
this.memCache[key] = content;
1212
}
1313

ts-cache/src/strategy/caching/abstract.base.strategy.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ export abstract class AbstractBaseStrategy implements IAsynchronousCacheType {
99

1010
public abstract getItem<T>(key: string): Promise<T | undefined>;
1111

12-
public abstract setItem(key: string, content: unknown, options?: ICacheOptions): Promise<void>;
12+
public abstract setItem<T = unknown>(
13+
key: string,
14+
content: T | undefined,
15+
options?: ICacheOptions
16+
): Promise<void>;
1317

1418
public abstract clear(): Promise<void>;
1519
}

ts-cache/src/strategy/caching/expiration.strategy.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ export class ExpirationStrategy extends AbstractBaseStrategy {
3535
return item.meta !== false && typeof item.meta.ttl === 'number';
3636
}
3737

38-
public async setItem(key: string, content: unknown, options?: ICacheOptions): Promise<void> {
38+
public async setItem<T = unknown>(
39+
key: string,
40+
content: T | undefined,
41+
options?: ICacheOptions
42+
): Promise<void> {
3943
const mergedOptions: IExpirationOptions = {
4044
ttl: 60,
4145
isLazy: true,

0 commit comments

Comments
 (0)