Skip to content

Commit

Permalink
type(storage): enhance ts type for get/set/has/del
Browse files Browse the repository at this point in the history
  • Loading branch information
wzc520pyfm committed Nov 14, 2024
1 parent e42c01d commit 14f4530
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 24 deletions.
16 changes: 15 additions & 1 deletion docs/1.guide/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ icon: ph:book-open-duotone
## Introduction

We usually choose one or more storage backends based on our use cases, such as the filesystem, a database, or LocalStorage for browsers. It soon starts to create troubles when supporting and combining multiple options or switching between them. For JavaScript library authors, this usually means that they have to decide how many platforms they are going to support and implement storage for each of them.
We usually choose one or more storage backends based on our use cases, such as the filesystem, a database, or LocalStorage for browsers. It soon starts to create troubles when supporting and combining multiple options or switching between them. For JavaScript library authors, this usually means that they have to decide how many platforms they are going to support and implement storage for each of them.

## Installation

Expand Down Expand Up @@ -310,6 +310,20 @@ await storage.getItem("k"); // => <string>

storage.setItem("k", "val"); // Check ok
storage.setItem("k", 123); // TS error

type TestObjType = {
a: number;
b: boolean;
};
type MyStorage = {
foo: string;
baz: TestObjType;
};
const storage = createStorage<MyStorage>();
await storage.getItem("foo"); // => <string>
await storage.getItem("baz"); // => <TestObjType>
storage.setItem("foo", "val"); // Check ok
storage.setItem("foo", 123); // TS error
```

::note
Expand Down
29 changes: 19 additions & 10 deletions src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,19 +161,22 @@ export function createStorage<T extends StorageValue>(

const storage: Storage = {
// Item
hasItem(key, opts = {}) {
hasItem(key: string, opts = {}) {
key = normalizeKey(key);
const { relativeKey, driver } = getMount(key);
return asyncCall(driver.hasItem, relativeKey, opts);
},
getItem(key, opts = {}) {
getItem(key: string, opts = {}) {
key = normalizeKey(key);
const { relativeKey, driver } = getMount(key);
return asyncCall(driver.getItem, relativeKey, opts).then((value) =>
destr(value)
);
},
getItems(items, commonOptions) {
getItems(
items: (string | { key: string; options?: TransactionOptions })[],
commonOptions = {}
) {
return runBatch(items, commonOptions, (batch) => {
if (batch.driver.getItems) {
return asyncCall(
Expand Down Expand Up @@ -214,7 +217,7 @@ export function createStorage<T extends StorageValue>(
deserializeRaw(value)
);
},
async setItem(key, value, opts = {}) {
async setItem(key: string, value: T, opts = {}) {
if (value === undefined) {
return storage.removeItem(key);
}
Expand Down Expand Up @@ -273,7 +276,12 @@ export function createStorage<T extends StorageValue>(
onChange("update", key);
}
},
async removeItem(key, opts = {}) {
async removeItem(
key: string,
opts:
| (TransactionOptions & { removeMeta?: boolean })
| boolean /* legacy: removeMeta */ = {}
) {
// TODO: Remove in next major version
if (typeof opts === "boolean") {
opts = { removeMeta: opts };
Expand Down Expand Up @@ -453,11 +461,12 @@ export function createStorage<T extends StorageValue>(
},
// Aliases
keys: (base, opts = {}) => storage.getKeys(base, opts),
get: (key, opts = {}) => storage.getItem(key, opts),
set: (key, value, opts = {}) => storage.setItem(key, value, opts),
has: (key, opts = {}) => storage.hasItem(key, opts),
del: (key, opts = {}) => storage.removeItem(key, opts),
remove: (key, opts = {}) => storage.removeItem(key, opts),
get: (key: string, opts = {}) => storage.getItem(key, opts),
set: (key: string, value: T, opts = {}) =>
storage.setItem(key, value, opts),
has: (key: string, opts = {}) => storage.hasItem(key, opts),
del: (key: string, opts = {}) => storage.removeItem(key, opts),
remove: (key: string, opts = {}) => storage.removeItem(key, opts),
};

return storage;
Expand Down
49 changes: 37 additions & 12 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,21 @@ export interface Driver<OptionsT = any, InstanceT = any> {

export interface Storage<T extends StorageValue = StorageValue> {
// Item
hasItem: (key: string, opts?: TransactionOptions) => Promise<boolean>;
getItem: <U extends T>(
hasItem<U extends Extract<T, object>, K extends keyof U>(
key: K,
opts?: TransactionOptions
): Promise<boolean>;
hasItem(key: string, opts?: TransactionOptions): Promise<boolean>;

getItem<U extends Extract<T, object>, K extends keyof U>(
key: K,
ops?: TransactionOptions
): Promise<U[K] | null>;
getItem<U extends T>(
key: string,
opts?: TransactionOptions
) => Promise<U | null>;
): Promise<U | null>;

/** @experimental */
getItems: <U extends T>(
items: (string | { key: string; options?: TransactionOptions })[],
Expand All @@ -78,11 +88,18 @@ export interface Storage<T extends StorageValue = StorageValue> {
key: string,
opts?: TransactionOptions
) => Promise<MaybeDefined<T> | null>;
setItem: <U extends T>(

setItem<U extends Extract<T, object>, K extends keyof U>(
key: K,
value: U[K],
opts?: TransactionOptions
): Promise<void>;
setItem<U extends T>(
key: string,
value: U,
opts?: TransactionOptions
) => Promise<void>;
): Promise<void>;

/** @experimental */
setItems: <U extends T>(
items: { key: string; value: U; options?: TransactionOptions }[],
Expand All @@ -94,12 +111,20 @@ export interface Storage<T extends StorageValue = StorageValue> {
value: MaybeDefined<T>,
opts?: TransactionOptions
) => Promise<void>;
removeItem: (

removeItem<U extends Extract<T, object>, K extends keyof U>(
key: K,
opts?:
| (TransactionOptions & { removeMeta?: boolean })
| boolean /* legacy: removeMeta */
): Promise<void>;
removeItem(
key: string,
opts?:
| (TransactionOptions & { removeMeta?: boolean })
| boolean /* legacy: removeMeta */
) => Promise<void>;
): Promise<void>;

// Meta
getMeta: (
key: string,
Expand Down Expand Up @@ -130,9 +155,9 @@ export interface Storage<T extends StorageValue = StorageValue> {
) => { base: string; driver: Driver }[];
// Aliases
keys: Storage["getKeys"];
get: Storage["getItem"];
set: Storage["setItem"];
has: Storage["hasItem"];
del: Storage["removeItem"];
remove: Storage["removeItem"];
get: Storage<T>["getItem"];
set: Storage<T>["setItem"];
has: Storage<T>["hasItem"];
del: Storage<T>["removeItem"];
remove: Storage<T>["removeItem"];
}
34 changes: 33 additions & 1 deletion test/storage.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi } from "vitest";
import { describe, it, expect, vi, expectTypeOf } from "vitest";
import {
createStorage,
snapshot,
Expand Down Expand Up @@ -173,6 +173,38 @@ describe("utils", () => {
await storage.remove("bar");
expect(await storage.has("bar")).toBe(false);
});

it("check types", async () => {
type TestObjType = {
a: number;
b: boolean;
};
type MyStorage = {
foo: string;
bar: number;
baz: TestObjType;
};
const storage = createStorage<MyStorage>();

expectTypeOf(await storage.getItem("foo")).toMatchTypeOf<string | null>();
expectTypeOf(await storage.getItem("bar")).toMatchTypeOf<number | null>();
expectTypeOf(await storage.get("baz")).toMatchTypeOf<TestObjType | null>();
expectTypeOf(
await storage.getItem("aaaaa")
).toMatchTypeOf<MyStorage | null>();

// @ts-expect-error
await storage.setItem("foo", 1); // ts err: Argument of type 'number' is not assignable to parameter of type 'string'
await storage.setItem("foo", "str");
// @ts-expect-error
await storage.set("bar", "str"); // ts err: Argument of type 'string' is not assignable to parameter of type 'number'.
await storage.set("bar", 1);

// should be able to get ts prompts: 'foo' | 'bar' | 'baz'
await storage.removeItem("foo");
await storage.remove("bar");
await storage.del("baz");
});
});

describe("Regression", () => {
Expand Down

0 comments on commit 14f4530

Please sign in to comment.