Skip to content

Commit

Permalink
Add generic type support for Keyv class and improve type safety (#1139)
Browse files Browse the repository at this point in the history
* Add generic type support for Keyv class and improve type safety

* Refactor `set` method and update type definitions

* Add type-safe usage examples to README
  • Loading branch information
chrisllontop authored Oct 1, 2024
1 parent 207a1c8 commit 10013c1
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 20 deletions.
32 changes: 31 additions & 1 deletion packages/keyv/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,36 @@ First, create a new Keyv instance.
import Keyv from 'keyv';
```

### Type-safe Usage

You can create a `Keyv` instance with a generic type to enforce type safety for the values stored. Additionally, both the `get` and `set` methods support specifying custom types for specific use cases.

#### Example with Instance-level Generic Type:

```ts
const keyv = new Keyv<number>(); // Instance handles only numbers
await keyv.set('key1', 123);
const value = await keyv.get('key1'); // value is inferred as number
```

#### Example with Method-level Generic Type:

You can also specify a type directly in the `get` or `set` methods, allowing flexibility for different types of values within the same instance.

```ts
const keyv = new Keyv(); // Generic type not specified at instance level

await keyv.set<string>('key2', 'some string'); // Method-level type for this value
const strValue = await keyv.get<string>('key2'); // Explicitly typed as string

await keyv.set<number>('key3', 456); // Storing a number in the same instance
const numValue = await keyv.get<number>('key3'); // Explicitly typed as number
```

This makes `Keyv` highly adaptable to different data types while maintaining type safety.

### Using Storage Adapters

Once you have created your Keyv instance you can use it as a simple key-value store with `in-memory` by default. To use a storage adapter, create an instance of the adapter and pass it to the Keyv constructor. Here are some examples:

```js
Expand Down Expand Up @@ -434,4 +464,4 @@ We welcome contributions to Keyv! 🎉 Here are some guides to get you started w

# License

[MIT © Jared Wray](LICENSE)
[MIT © Jared Wray](LICENSE)
25 changes: 13 additions & 12 deletions packages/keyv/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import StatsManager from './stats-manager.js';

export type DeserializedData<Value> = {
value?: Value;
expires?: number;
// eslint-disable-next-line @typescript-eslint/ban-types
expires?: number | null;
};

export interface CompressionAdapter {
Expand Down Expand Up @@ -84,7 +85,7 @@ const iterableAdapters = [
'tiered',
];

export class Keyv extends EventManager {
export class Keyv<GenericValue = any> extends EventManager {
opts: KeyvOptions_;
iterator?: IteratorFunction;
hooks = new HooksManager();
Expand Down Expand Up @@ -200,11 +201,11 @@ export class Keyv extends EventManager {
);
}

async get<Value>(key: string, options?: {raw: false}): Promise<StoredDataNoRaw<Value>>;
async get<Value>(key: string, options?: {raw: true}): Promise<StoredDataRaw<Value>>;
async get<Value>(key: string[], options?: {raw: false}): Promise<Array<StoredDataNoRaw<Value>>>;
async get<Value>(key: string[], options?: {raw: true}): Promise<Array<StoredDataRaw<Value>>>;
async get<Value>(key: string | string[], options?: {raw: boolean}): Promise<StoredDataNoRaw<Value> | Array<StoredDataNoRaw<Value>> | StoredDataRaw<Value> | Array<StoredDataRaw<Value>>> {
async get<Value = GenericValue>(key: string, options?: {raw: false}): Promise<StoredDataNoRaw<Value>>;
async get<Value = GenericValue>(key: string, options?: {raw: true}): Promise<StoredDataRaw<Value>>;
async get<Value = GenericValue>(key: string[], options?: {raw: false}): Promise<Array<StoredDataNoRaw<Value>>>;
async get<Value = GenericValue>(key: string[], options?: {raw: true}): Promise<Array<StoredDataRaw<Value>>>;
async get<Value = GenericValue>(key: string | string[], options?: {raw: boolean}): Promise<StoredDataNoRaw<Value> | Array<StoredDataNoRaw<Value>> | StoredDataRaw<Value> | Array<StoredDataRaw<Value>>> {
const {store} = this.opts;
const isArray = Array.isArray(key);
const keyPrefixed = isArray ? this._getKeyPrefixArray(key) : this._getKeyPrefix(key);
Expand Down Expand Up @@ -293,7 +294,7 @@ export class Keyv extends EventManager {
return (options?.raw) ? deserializedData : (deserializedData as DeserializedData<Value>).value;
}

async set(key: string, value: any, ttl?: number): Promise<boolean> {
async set<Value = GenericValue>(key: string, value: Value, ttl?: number): Promise<boolean> {
this.hooks.trigger(KeyvHooks.PRE_SET, {key, value, ttl});
const keyPrefixed = this._getKeyPrefix(key);
if (typeof ttl === 'undefined') {
Expand All @@ -312,11 +313,11 @@ export class Keyv extends EventManager {
this.emit('error', 'symbol cannot be serialized');
}

value = {value, expires};
const formattedValue = {value, expires};
const serializedValue = await this.opts.serialize!(formattedValue);

value = await this.opts.serialize!(value);
await store.set(keyPrefixed, value, ttl);
this.hooks.trigger(KeyvHooks.POST_SET, {key: keyPrefixed, value, ttl});
await store.set(keyPrefixed, serializedValue, ttl);
this.hooks.trigger(KeyvHooks.POST_SET, {key: keyPrefixed, value: serializedValue, ttl});
this.stats.set();
return true;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/keyv/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ test.it('Keyv is a class', t => {

test.it('Keyv accepts storage adapters', async t => {
const store = new Map();
const keyv = new Keyv({store});
const keyv = new Keyv<string>({store});
t.expect(store.size).toBe(0);
await keyv.set('foo', 'bar');
t.expect(await keyv.get('foo')).toBe('bar');
Expand All @@ -44,7 +44,7 @@ test.it('Keyv accepts storage adapters and options', async t => {

test.it('Keyv accepts storage adapters instead of options', async t => {
const store = new Map();
const keyv = new Keyv(store);
const keyv = new Keyv<string>(store);
t.expect(store.size).toBe(0);
await keyv.set('foo', 'bar');
t.expect(await keyv.get('foo')).toBe('bar');
Expand Down
10 changes: 5 additions & 5 deletions packages/memcache/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ test.beforeEach(async () => {
});

test.it('keyv get / no expired', async t => {
const keyv = new Keyv({store: keyvMemcache});
const keyv = new Keyv<string>({store: keyvMemcache});

await keyv.set('foo', 'bar');

Expand Down Expand Up @@ -76,7 +76,7 @@ test.it('keyv get with namespace', async t => {
});

test.it('keyv get / should still exist', async t => {
const keyv = new Keyv({store: keyvMemcache});
const keyv = new Keyv<string>({store: keyvMemcache});

await keyv.set('foo-expired', 'bar-expired', 10_000);

Expand All @@ -88,7 +88,7 @@ test.it('keyv get / should still exist', async t => {
});

test.it('keyv get / expired existing', async t => {
const keyv = new Keyv({store: keyvMemcache});
const keyv = new Keyv<string>({store: keyvMemcache});

await keyv.set('foo-expired', 'bar-expired', 1000);

Expand All @@ -100,7 +100,7 @@ test.it('keyv get / expired existing', async t => {
});

test.it('keyv get / expired existing with bad number', async t => {
const keyv = new Keyv({store: keyvMemcache});
const keyv = new Keyv<string>({store: keyvMemcache});

await keyv.set('foo-expired', 'bar-expired', 1);

Expand All @@ -112,7 +112,7 @@ test.it('keyv get / expired existing with bad number', async t => {
});

test.it('keyv get / expired', async t => {
const keyv = new Keyv({store: keyvMemcache});
const keyv = new Keyv<string>({store: keyvMemcache});

await keyv.set('foo-expired', 'bar-expired', 1000);

Expand Down

0 comments on commit 10013c1

Please sign in to comment.