Skip to content

feat: typed storages support #826

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

ipakhomov
Copy link

@ipakhomov ipakhomov commented Mar 31, 2025

Changes for #815

Changes

  • Added ability to pass an optional generic type when declaring MMKV instance to describe storage keys and values for better type-safety
  • Updated hooks to support typed storage instances
  • Added tests for types using tsd
  • Updated Contributing guide a bit

Usage

  1. Define Your Storage Schema
type StorageValues = {
  userToken: string;
  sessionTTL: number;
  isPremiumUser: boolean;
  cachedData: ArrayBuffer;
};
  1. Create a Typed MMKV Instance:
const storage = new MMKV<StorageValues>();
  1. Store & Retrieve Values with Type Safety
// ✅ Correct Usage
storage.set('userToken', 'abc123');
const token = storage.getString('userToken'); // string | undefined

storage.set('sessionTTL', 3600);
const ttl = storage.getNumber('sessionTTL'); // number | undefined

storage.set('isPremiumUser', true);
const isPremium = storage.getBoolean('isPremiumUser'); // boolean | undefined
  1. Type Errors Prevent Mistakes:
// ❌ TypeScript will prevent incorrect operations:
storage.set('userToken', 123); // Error: Expected string value
storage.getBoolean('sessionTTL'); // Error: Expected boolean key
  1. Hooks were also updated:
const [userToken, setUserToken] = useMMKVString('userToken', storage);
setUserToken('newToken123'); // ✅ Type-safe
setUserToken(false) //  ❌ TypeScript will report this error

More examples of correct and incorrect use-cases can be found in types tests file package/src/__types_tests__/mmkv.test-d.ts

@ipakhomov
Copy link
Author

@mrousavy I look forward to your feedback! Please let me know if I can update the Readme.

@mrousavy
Copy link
Owner

mrousavy commented Apr 1, 2025

I appreciate the PR, but you added a package-lock.json which clearly can't be right. MMKV uses Bun or Yarn.

Also, I don't think this should be patched into MMKV.ts, this should be an added interface ontop.

@ipakhomov
Copy link
Author

@mrousavy good catch about package-lock.json - removed, thanks! I accidentally added it because of mention of npm in Contributing guide (fixed in this PR as well).

As for this:

I don't think this should be patched into MMKV.ts, this should be an added interface ontop.

I'm not quite sure how the same functionality can be achieved without touching package/src/MMKV.ts file. Could you please elaborate?

@mrousavy
Copy link
Owner

mrousavy commented Apr 1, 2025

You just provide a thin wrapper that only does types. Your implementation doesn't do anything else, no JS logic as far as I can see.

So users would call new TypedMMKV<...> instead of new MMKV then

@ipakhomov
Copy link
Author

@mrousavy yes, my implementation does not change JS logic. But it looked to me that introducing a new class is a bit redundant comparing to adding an optional generic type to the existing one (MMKV). Usage isn't changed for the existing users, and the class became more type-safe.
Thoughts?

PS: Also, I moved types tests to a separate directory and added them to the code-checks pipeline. Could you please check?

@MarkusWendorf
Copy link

Looks reasonable to me, existing users do not need to change anything new MMKV()

Using more specific types is opt-in via generics new MMKV<T>()

@ipakhomov
Copy link
Author

@mrousavy just a friendly ping here 🙂

@devuco
Copy link

devuco commented Apr 29, 2025

@mrousavy Looks good to me too.
Please merge ASAP, need support for TS.
Thanks

@ipakhomov
Copy link
Author

@mrousavy just a kindly reminder about this PR :)

@@ -127,7 +136,9 @@ export class MMKV implements MMKVInterface {
};
}

addOnValueChangedListener(onValueChanged: (key: string) => void): Listener {
addOnValueChangedListener(
onValueChanged: (key: keyof TStorage) => void
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This right here is problematic since it is technically not true. The user can create a new MMKV instance that is not typed but has the same key, set a value that is not a value of keyof TStorage, and then the onValueChanged listener will be called with a value that is within the typescript types.

I guess it's an edge case, but should still be considered. Maybe we keep this string? People will probably complain if it's string.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mrousavy I'm not sure this changes anything technically, since this method belongs to a storage instance. For a typed storage instance, it will suggest one of its keys, while for an untyped instance, the keys will have the string type (i.e., it will work as before).

I've added a test that demonstrates this—let me know if I misunderstood anything.

@mrousavy
Copy link
Owner

mrousavy commented Jun 5, 2025

I gotta be honest I'm still pretty torn between either merging this or having the user be responsible for this. There's a few cases where the type safety just isn't guaranteed. After all, it's a storage - it can be anything if written to from elsewhere (e.g. from an untyped MMKV storage, from a previous version where the user specified different types, or from native).

I feel like this just increases the maintenance layer for me - people will find bugs in the type system here and complain in issues.

@NathanP-7West
Copy link

NathanP-7West commented Jun 6, 2025

If it helps, I implemented something pretty similar for storage in the apps I've been working on, but it uses zod validation to enforce the storage values at runtime + a primitive migrations system, to address the whole type 'assumption' problem.

  • There is the performance tradeoff of doing runtime validation of course
  • The migration system is a direct result of the issue that mrousavy was talking about where the stored value shape changed across different app versions

Probably not able to share what we're using as it's a bit tightly coupled with our stuff, but I'll maybe look into throwing together a thin wrapping library (since this seems to be beyond the scope of this project) using https://github.com/standard-schema/standard-schema given the interest is there.

@ipakhomov
Copy link
Author

@mrousavy Thank you for taking a look!
Indeed, the changes in this PR make supporting the library a bit more complex due to the tightened types. However, I believe it's worth it, as the library becomes more type-safe.
We could consider releasing this as a beta, and I will be happy to help you resolve any TypeScript-related issues.

Thoughts?

@NathanP-7West Thank you for your comment!
The approach you suggested can work, but I'm trying to avoid runtime validation and instead provide built-in type checks within the library itself. Please let me know if I misunderstood your idea.

@ipakhomov
Copy link
Author

@mrousavy In the last two commits, I slightly tightened some type checks for default (non-typed) storage and added a few more tests to demonstrate that non-typed storage types aren't affected and continue to work correctly. So all comments seems to be resolved.

Looking forward to your feedback!

@ipakhomov
Copy link
Author

@mrousavy just a kind reminder that this PR waits for your attention

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants