Skip to content

Commit

Permalink
redis: add adapter level type generic (#1252)
Browse files Browse the repository at this point in the history
* feat: add adapter level type generic

* test: add type tests

---------

Co-authored-by: Jared Wray <me@jaredwray.com>
  • Loading branch information
mahdavipanah and jaredwray authored Dec 12, 2024
1 parent fb9a5c5 commit 5edb8c0
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 9 deletions.
29 changes: 29 additions & 0 deletions packages/redis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Redis storage adapter for [Keyv](https://github.com/jaredwray/keyv).
# Table of Contents
* [Usage](#usage)
* [Namespaces](#namespaces)
* [Typescript](#typescript)
* [Performance Considerations](#performance-considerations)
* [High Memory Usage on Redis Server](#high-memory-usage-on-redis-server)
* [Using Cacheable with Redis](#using-cacheable-with-redis)
Expand Down Expand Up @@ -110,6 +111,34 @@ keyv.namespace = 'my-namespace';

NOTE: If you plan to do many clears or deletes, it is recommended to read the [Performance Considerations](#performance-considerations) section.

## Typescript

When initializing `KeyvRedis`, you can specify the type of the values you are storing and you can also specify types when calling methods:

```typescript
import Keyv from 'keyv';
import KeyvRedis, { createClient } from '@keyv/redis';


interface User {
id: number
name: string
}

const redis = createClient('redis://user:pass@localhost:6379');

const keyvRedis = new KeyvRedis<User>(redis);
const keyv = new Keyv({ store: keyvRedis });

await keyv.set("user:1", { id: 1, name: "Alice" })
const user = await keyv.get("user:1")
console.log(user.name) // 'Alice'

// specify types when calling methods
const user = await keyv.get<User>("user:1")
console.log(user.name) // 'Alice'
```

# Performance Considerations

With namespaces being prefix based it is critical to understand some of the performance considerations we have made:
Expand Down
4 changes: 2 additions & 2 deletions packages/redis/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"scripts": {
"build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean",
"prepublishOnly": "pnpm build",
"test": "xo --fix && vitest run --coverage",
"test:ci": "xo && vitest --run --sequence.setupFiles=list",
"test": "xo --fix && vitest run --coverage --typecheck",
"test:ci": "xo && vitest --run --sequence.setupFiles=list --typecheck",
"clean": "rimraf ./node_modules ./coverage ./dist"
},
"repository": {
Expand Down
14 changes: 7 additions & 7 deletions packages/redis/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export type KeyvRedisEntry<T> = {
export type RedisClientConnectionType = RedisClientType | RedisClusterType<RedisModules, RedisFunctions, RedisScripts>;

// eslint-disable-next-line unicorn/prefer-event-target
export default class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
export default class KeyvRedis<T> extends EventEmitter implements KeyvStoreAdapter {
private _client: RedisClientConnectionType = createClient() as RedisClientType;
private _namespace: string | undefined;
private _keyPrefixSeparator = '::';
Expand Down Expand Up @@ -299,29 +299,29 @@ export default class KeyvRedis extends EventEmitter implements KeyvStoreAdapter
* @param {string} key - the key to get
* @returns {Promise<string | undefined>} - the value or undefined if the key does not exist
*/
public async get<T>(key: string): Promise<T | undefined> {
public async get<U = T>(key: string): Promise<U | undefined> {
const client = await this.getClient();
key = this.createKeyPrefix(key, this._namespace);
const value = await client.get(key);
if (value === null) {
return undefined;
}

return value as T;
return value as U;
}

/**
* Get many values from the store. If a key does not exist, it will return undefined.
* @param {Array<string>} keys - the keys to get
* @returns {Promise<Array<string | undefined>>} - array of values or undefined if the key does not exist
*/
public async getMany<T>(keys: string[]): Promise<Array<T | undefined>> {
public async getMany<U = T>(keys: string[]): Promise<Array<U | undefined>> {
if (keys.length === 0) {
return [];
}

keys = keys.map(key => this.createKeyPrefix(key, this._namespace));
const values = await this.mget<T>(keys);
const values = await this.mget<U>(keys);

return values;
}
Expand Down Expand Up @@ -434,7 +434,7 @@ export default class KeyvRedis extends EventEmitter implements KeyvStoreAdapter
* @param {string} [namespace] - the namespace to iterate over
* @returns {AsyncGenerator<[string, T | undefined], void, unknown>} - async iterator with key value pairs
*/
public async * iterator<Value>(namespace?: string): AsyncGenerator<[string, Value | undefined], void, unknown> {
public async * iterator<U = T>(namespace?: string): AsyncGenerator<[string, U | undefined], void, unknown> {
// When instance is not a cluster, it will only have one client
const clients = await this.getMasterNodes();

Expand All @@ -453,7 +453,7 @@ export default class KeyvRedis extends EventEmitter implements KeyvStoreAdapter

if (keys.length > 0) {
// eslint-disable-next-line no-await-in-loop
const values = await this.mget<Value>(keys);
const values = await this.mget<U>(keys);
for (const i of keys.keys()) {
const key = this.getKeyWithoutPrefix(keys[i], namespace);
const value = values[i];
Expand Down
42 changes: 42 additions & 0 deletions packages/redis/test/types.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {describe, test, expectTypeOf} from 'vitest';
import KeyvRedis from '../src/index.js';

describe('KeyvRedis Types', () => {
test('should be able to set adapter-level generic value type', async () => {
type Value = {foo: string};

const keyvRedis = new KeyvRedis<Value>();

expectTypeOf(keyvRedis.get('foo')).toEqualTypeOf<
Promise<Value | undefined>
>();

expectTypeOf(keyvRedis.getMany(['foo', 'bar'])).toEqualTypeOf<
Promise<Array<Value | undefined>>
>();

expectTypeOf(keyvRedis.iterator()).toEqualTypeOf<
AsyncGenerator<[string, Value | undefined], void, unknown>
>();
});

test('should be able to set method-level generic value type', async () => {
type ValueFoo = {foo: string};

type ValueBar = {bar: string};

const keyvRedis = new KeyvRedis<ValueFoo>();

expectTypeOf(keyvRedis.get<ValueBar>('foo')).toEqualTypeOf<
Promise<ValueBar | undefined>
>();

expectTypeOf(keyvRedis.getMany<ValueBar>(['foo', 'bar'])).toEqualTypeOf<
Promise<Array<ValueBar | undefined>>
>();

expectTypeOf(keyvRedis.iterator<ValueBar>()).toEqualTypeOf<
AsyncGenerator<[string, ValueBar | undefined], void, unknown>
>();
});
});

0 comments on commit 5edb8c0

Please sign in to comment.