Skip to content

Commit 27e60dd

Browse files
committed
fix: DataStoreData allow interfaces without index signature
1 parent d6843f9 commit 27e60dd

File tree

5 files changed

+54
-15
lines changed

5 files changed

+54
-15
lines changed

.changeset/wide-seals-worry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sv443-network/coreutils": patch
3+
---
4+
5+
Made the type `DataStoreData` much less constrained. This way it won't require an index signature anymore.

docs.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -854,13 +854,12 @@ The old data is a copy of the cached object, so you can mutate it directly, use
854854

855855
### `type DataStoreData`
856856
```ts
857-
type DataStoreData<TData extends SerializableVal = SerializableVal> = Record<string, SerializableVal | TData>;
857+
type DataStoreData = object;
858858
```
859859

860860
A type that represents the data stored in a DataStore instance.
861-
It is a record of string keys to values that can be serialized to JSON via `JSON.stringify()`.
862-
This means that the values can be primitive types (string, number, boolean, null), arrays or objects that only contain serializable values.
863-
Refer to the [`SerializableVal` type](#type-serializableval) for more information.
861+
It uses `object` instead of an index signature so that interfaces without an explicit index signature can also be used as `TData`.
862+
Make sure to only use JSON-serializable types here, otherwise unexpected behavior may occur.
864863

865864
<br><br>
866865

lib/DataStore.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export type DataStoreOptions<TData extends DataStoreData> = Prettify<
5050
*
5151
* - ⚠️ Don't reuse the same engine instance for multiple DataStores, unless it explicitly supports it!
5252
*/
53-
engine: (() => DataStoreEngine<TData>) | DataStoreEngine<TData>;
53+
engine: (() => DataStoreEngine) | DataStoreEngine;
5454
/**
5555
* A dictionary of functions that can be used to migrate data from older versions to newer ones.
5656
* The keys of the dictionary should be the format version that the functions can migrate to, from the previous whole integer value.
@@ -111,8 +111,12 @@ export type DataStoreOptions<TData extends DataStoreData> = Prettify<
111111
)
112112
>;
113113

114-
/** Generic type that represents the serializable data structure saved in a {@linkcode DataStore} instance. */
115-
export type DataStoreData<TData extends SerializableVal = SerializableVal> = Record<string, SerializableVal | TData>;
114+
/**
115+
* Generic type that represents the serializable data structure saved in a {@linkcode DataStore} instance.
116+
* - ⚠️ Uses `object` instead of an index signature so that interfaces without an explicit index signature can be used as `TData`.
117+
* Make sure to only use JSON-serializable types here, otherwise unexpected behavior may occur!
118+
*/
119+
export type DataStoreData = object;
116120

117121
//#region class
118122

@@ -141,7 +145,7 @@ export class DataStore<TData extends DataStoreData> {
141145
public readonly decodeData: DataStoreOptions<TData>["decodeData"];
142146
public readonly compressionFormat: Exclude<DataStoreOptions<TData>["compressionFormat"], undefined> = "deflate-raw";
143147
public readonly memoryCache: boolean = true;
144-
public readonly engine: DataStoreEngine<TData>;
148+
public readonly engine: DataStoreEngine;
145149
public options: DataStoreOptions<TData>;
146150

147151
/**
@@ -284,7 +288,7 @@ export class DataStore<TData extends DataStoreData> {
284288
}
285289

286290
// deserialize the data if needed
287-
let parsed = await this.engine.deserializeData(storedData, isEncoded);
291+
let parsed = await this.engine.deserializeData(storedData, isEncoded) as TData;
288292

289293
// run migrations if needed
290294
if(storedFmtVer < this.formatVersion && this.migrations)
@@ -444,7 +448,7 @@ export class DataStore<TData extends DataStoreData> {
444448
if(data === undefined || isNaN(fmtVer))
445449
return;
446450

447-
const parsed = await this.engine.deserializeData(data, isEncoded);
451+
const parsed = await this.engine.deserializeData(data, isEncoded) as TData;
448452
await Promise.allSettled([
449453
this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(parsed)),
450454
this.engine.setValue(`__ds-${this.id}-ver`, fmtVer),

lib/DataStoreEngine.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import type { Prettify, SerializableVal } from "./types.ts";
1111
//#region >> DataStoreEngine
1212

1313
/** Contains the only properties of {@linkcode DataStoreOptions} that are relevant to the {@linkcode DataStoreEngine} class. */
14-
export type DataStoreEngineDSOptions<TData extends DataStoreData> = Prettify<Pick<DataStoreOptions<TData>, "decodeData" | "encodeData" | "id">>;
14+
export type DataStoreEngineDSOptions<TData extends DataStoreData = DataStoreData> = Prettify<Pick<DataStoreOptions<TData>, "decodeData" | "encodeData" | "id">>;
1515

16-
export interface DataStoreEngine<TData extends DataStoreData> { // eslint-disable-line @typescript-eslint/no-unused-vars
16+
export interface DataStoreEngine<TData extends DataStoreData = DataStoreData> { // eslint-disable-line @typescript-eslint/no-unused-vars
1717
/** Deletes all data in persistent storage, including the data container itself (e.g. a file or a database) */
1818
deleteStorage?(): Promise<void>;
1919
}
@@ -22,7 +22,7 @@ export interface DataStoreEngine<TData extends DataStoreData> { // eslint-disabl
2222
* Base class for creating {@linkcode DataStore} storage engines.
2323
* This acts as an interchangeable API for writing and reading persistent JSON-serializable data in various environments.
2424
*/
25-
export abstract class DataStoreEngine<TData extends DataStoreData> {
25+
export abstract class DataStoreEngine<TData extends DataStoreData = DataStoreData> {
2626
protected dataStoreOptions!: DataStoreEngineDSOptions<TData>; // setDataStoreOptions() is called from inside the DataStore constructor to set this value
2727

2828
constructor(options?: DataStoreEngineDSOptions<TData>) {
@@ -118,7 +118,7 @@ export type BrowserStorageEngineOptions = {
118118
* - ⚠️ Requires a DOM environment
119119
* - ⚠️ Don't reuse engine instances, always create a new one for each {@linkcode DataStore} instance
120120
*/
121-
export class BrowserStorageEngine<TData extends DataStoreData> extends DataStoreEngine<TData> {
121+
export class BrowserStorageEngine<TData extends DataStoreData = DataStoreData> extends DataStoreEngine<TData> {
122122
protected options: BrowserStorageEngineOptions & Required<Pick<BrowserStorageEngineOptions, "type">>;
123123

124124
/**
@@ -188,7 +188,7 @@ export type FileStorageEngineOptions = {
188188
* - ⚠️ Requires Node.js or Deno with Node compatibility (v1.31+)
189189
* - ⚠️ Don't reuse engine instances, always create a new one for each {@linkcode DataStore} instance
190190
*/
191-
export class FileStorageEngine<TData extends DataStoreData> extends DataStoreEngine<TData> {
191+
export class FileStorageEngine<TData extends DataStoreData = DataStoreData> extends DataStoreEngine<TData> {
192192
protected options: FileStorageEngineOptions & Required<Pick<FileStorageEngineOptions, "filePath">>;
193193
private fileAccessQueue: Promise<void> = Promise.resolve();
194194

lib/test/DataStore.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,4 +349,35 @@ describe("DataStore", () => {
349349
store.getData();
350350
}).toThrow();
351351
});
352+
353+
it("No type error when using interface without index signature", async () => {
354+
interface MyData {
355+
a: number;
356+
b: number;
357+
}
358+
359+
const defaultData: MyData = { a: 1, b: 2 };
360+
361+
const store = new DataStore({
362+
id: "test-interface-without-index-signature",
363+
defaultData,
364+
formatVersion: 1,
365+
engine: new BrowserStorageEngine({ type: "localStorage" }),
366+
});
367+
368+
await store.loadData();
369+
370+
expect(store.getData().a).toBe(1);
371+
expect(store.getData().b).toBe(2);
372+
373+
await store.setData({ ...store.getData(), a: 42 });
374+
375+
await store.loadData();
376+
377+
expect(store.getData().a).toBe(42);
378+
expect(store.getData().b).toBe(2);
379+
380+
// restore initial state:
381+
await store.deleteData();
382+
});
352383
});

0 commit comments

Comments
 (0)