Skip to content

Instantly share code, notes, and snippets.

@lafiosca
Last active February 13, 2024 03:42
Show Gist options
  • Save lafiosca/b7bbb569ae3fe5c1ce110bf71d7ee153 to your computer and use it in GitHub Desktop.
Save lafiosca/b7bbb569ae3fe5c1ce110bf71d7ee153 to your computer and use it in GitHub Desktop.
redux-persist migrations in TypeScript with a little help from patch-package
import { createMigrate } from 'redux-persist';
import { RootState } from './modules/reducer';
/*
* Latest version (V3) is simply the currently used redux RootState.
*/
type PersistedRootStateV3 = RootState;
/*
* Previous versions can be built from advanced types, working backward from current;
* in this example a field was added at `slice.newField` when going from V2 to V3,
* so to get V2's shape we take everything except that field from V3.
*/
type PersistedRootStateV2 = Omit<PersistedRootStateV3, 'slice'> & {
slice: Omit<PersistedRootStateV2['slice'], 'newField'>;
};
/*
* The entire `slice` reducer was added when going from V1 to V2, so omit it from the
* V2 definition. This approach of building incrementally backwards also means that we
* don't have to update all of the old state types each time we add a new migration.
*/
type PersistedRootStateV1 = Omit<PersistedRootStateV2, 'slice'>;
/*
* Each migration step will take one version as input and return the next version as output.
* (The key `2` means that it is the step which migrates from V1 to V2.)
*/
const persistMigrations = {
2: (state: PersistedRootStateV1): PersistedRootStateV2 => ({
...state,
slice: { // Introduce new `slice`.
someField: 0,
someOtherField: [],
},
}),
3: async (state: PersistedRootStateV2): Promise<PersistedRootStateV3> => {
return {
...state,
slice: {
...state.slice,
newField: {}, // Add `newField` to `slice`.
},
};
},
};
/*
* A union type is created specifically to use below.
*/
type MigrationState = PersistedRootStateV1 | PersistedRootStateV2 | PersistedRootStateV3
/*
* The union type is used as the generic for `createMigrate`. The type must be explicitly
* provided. Relying on type inference here would fail because it would assume {}.
*/
export const persistMigrate = createMigrate<MigrationState>(persistMigrations);
/*
* This is the current version and should match the latest version above (V3).
*/
export const persistVersion = 3;
diff --git a/node_modules/redux-persist/types/createMigrate.d.ts b/node_modules/redux-persist/types/createMigrate.d.ts
index 2c1329d..2e4cd57 100644
--- a/node_modules/redux-persist/types/createMigrate.d.ts
+++ b/node_modules/redux-persist/types/createMigrate.d.ts
@@ -11,7 +11,7 @@ declare module "redux-persist/es/createMigrate" {
* @param config migration configuration
*/
// tslint:disable-next-line: strict-export-declare-modifiers
- export default function createMigrate(migrations: MigrationManifest, config?: MigrationConfig): PersistMigrate;
+ export default function createMigrate<T extends {}>(migrations: MigrationManifest<T>, config?: MigrationConfig): PersistMigrate;
}
declare module "redux-persist/lib/createMigrate" {
diff --git a/node_modules/redux-persist/types/types.d.ts b/node_modules/redux-persist/types/types.d.ts
index b3733bc..f8106cb 100644
--- a/node_modules/redux-persist/types/types.d.ts
+++ b/node_modules/redux-persist/types/types.d.ts
@@ -72,9 +72,9 @@ declare module "redux-persist/es/types" {
removeItem(key: string): Promise<void>;
}
- interface MigrationManifest {
- [key: string]: (state: PersistedState) => PersistedState;
- }
+ type MigrationFunction<T extends {}, U extends {} = T> = T extends infer V ? U extends infer W ? (state: V) => W | Promise<W> : never : never;
+
+ type MigrationManifest<T extends {}> = Record<string, MigrationFunction<T>>;
/**
* @desc
/* ... */
import { persistReducer } from 'redux-persist';
import AsyncStorage from '@react-native-community/async-storage';
import { persistVersion, persistMigrate } from './persistMigrations';
/* ... */
const persistConfig = {
key: 'root',
storage: AsyncStorage,
whitelist: [
'slice',
/* ... */
],
version: persistVersion,
migrate: persistMigrate,
};
const persistedRootReducer = persistReducer(persistConfig, rootReducer);
/* ... and so on, creating the store as usual */
@jakiestfu
Copy link

jakiestfu commented Dec 3, 2021

This was helpful to me. Any idea why this isn't in redux-persist already?

Edit

I see you have been discussing things here! rt2zz/redux-persist#1065 (comment)

@lafiosca
Copy link
Author

lafiosca commented Dec 3, 2021

Over time I've actually found it easier to just wipe the entire store when I have breaking migrations. Perhaps not the smoothest user experience, but less bookkeeping:

import { createMigrate } from 'redux-persist';

import { RootState } from './modules/reducer';
import { resetStore } from './modules/actions';
import { dispatchLogMessage } from '../services/log';
import { somethingReducer } from './modules/something/reducer';
import { somethingElseReducer } from './modules/somethingElse/reducer';

export const persistVersion = 13;
export const persistWhitelist = [
	'something',
	'somethingElse',
] as const;

type Whitelist = typeof persistWhitelist;
type Whitelisted = Whitelist extends ReadonlyArray<infer T> ? T : never;
type PersistedRootState = Pick<RootState, Whitelisted>;

const persistMigrations = {
	[persistVersion]: (): PersistedRootState => {
		dispatchLogMessage('Resetting redux-persist store');
		const action = resetStore();
		return {
			something: somethingReducer(undefined, action),
			somethingElse: somethingElseReducer(undefined, action),
		};
	},
};

export const persistMigrate = createMigrate<PersistedRootState>(persistMigrations);

All I do is bump the version number.

@elcharitas
Copy link

Over time I've actually found it easier to just wipe the entire store when I have breaking migrations. Perhaps not the smoothest user experience, but less bookkeeping

I'm at a point where I'm forced to agree 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment