feat: typed storages support
Changes for https://github.com/mrousavy/react-native-mmkv/issues/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
- Define Your Storage Schema
type StorageValues = {
userToken: string;
sessionTTL: number;
isPremiumUser: boolean;
cachedData: ArrayBuffer;
};
- Create a Typed MMKV Instance:
const storage = new MMKV<StorageValues>();
- 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
- Type Errors Prevent Mistakes:
// ❌ TypeScript will prevent incorrect operations:
storage.set('userToken', 123); // Error: Expected string value
storage.getBoolean('sessionTTL'); // Error: Expected boolean key
- 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
@mrousavy I look forward to your feedback! Please let me know if I can update the Readme.
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.
@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?
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
@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?
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>()
@mrousavy just a friendly ping here 🙂
@mrousavy Looks good to me too. Please merge ASAP, need support for TS. Thanks
@mrousavy just a kindly reminder about this PR :)
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.
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.
@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.
@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!
@mrousavy just a kind reminder that this PR waits for your attention
Like everyone here, I value a well-designed type system. But for this one, I have to agree with @mrousavy that the TypeScript interface proposed here, while interesting and well thought out, isn't a reliable guarantee of type safety:
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). -- https://github.com/mrousavy/react-native-mmkv/pull/826#issuecomment-2943157435 (emphasis added)
Due to situations like these, even if this PR was merged, I would still need to maintain my own validation layer around MMKV, at which point I might as well continue typing my validation layer instead of MMKV.
I agree, yea. I don't think this should be MMKV's duty. Type validation is a whole other project.
I really appreciate everyone's thoughts and time that went into this, and especially from you @ipakhomov for creating the PR, but I'm going to close this as this is out of scope for MMKV itself. Maybe we can create a typed storage wrapper ontop of this as a separate project?
@mrousavy Thank you for the feedback. I’ll consider creating a separate wrapper package based on this.