简体   繁体   中英

Typescript-typed redux-persist MigrationManifest?

I have a Typescript project that uses redux and redux-persist. Is it possible to use Typescript for a redux-persist migration configuration? The main difficulty comes down to this: If you have a Typescript-typed root redux state, it typically/always represents the latest version of your state (not necessarily what was previously persisted). In the context of a migration, how do you represent the data that's being read from persistence in a typed manner, knowing it doesn't match the latest version of your state? Some details:

Here is the Typescript API of redux-persist for data migrations:

export interface MigrationManifest {
    [key: string]: (state: PersistedState) => PersistedState;
}
export interface PersistedState { _persist?: PersistState }
export interface PersistState { version: number; rehydrated: boolean; }

Makes sense, you provide a MigrationManifest which has keys that are version numbers and values that are functions that receive the persisted state and return a new persisted state. Since PersistedState is just an interface, it seems you could have one incoming type and return a different type (in cases where your data schema changed).

So let's say I have the following type as my root persistent store type. It conforms to the PersistedState interface:

type RootState = {
    name: string,
    address: Address,
    _persist?: PersistState
}

type Address = {
    street: string
    city: string
}

At some point in the future, I update my model to:

type RootState = {
    name: string,
    address: Address,
    _persist?: PersistState
}

type Address = {
    vacant: boolean
}

And I need to provide a migration which would look something like:

const manifest: MigrationManifest = {
  1: (state: PersistedState) => {
    const persistedState = state as ???
    const migratedState: RootState = migrateState(persistedState)
    return migratedState
  }
}

Where I'm struggling is with getting a type for the incoming state (where my cast to ??? is). In a simple example like this, it would be easy to maintain a record of each version of my state, import and use them as necessary:

import { RootState as RootState_v0 } from 'types_v0.ts'

const manifest: MigrationManifest = {
  1: (state: PersistedState) => {
    const persistedState = state as RootState_v0
    const migratedState: RootState = migrateState(persistedState)
    return migratedState
  }
}

In practice, it's not that simple; I have a complex state tree, not all of which is defined in a central and easy to version location.

One solution I can dream up, but I don't know is possible, would be to somehow create a version of my RootState type with all of the intermediate type aliases and interface names "dissolved". Something like:

type RootState_v0 = {
    name: string,
    address: {
        street: string
        city: string
    },
    _persist?: {
        version: number,
        rehydrated: boolean
    }
}

If I could create that in some automatic way, it would be easy and convenient to keep and use in migrations.

Any idea if that is possible, or any other suggestions about how to effectively use Typescript in react-redux migrations?

At some point you have to specify what has what type in what version. But you can use lookup types and conditional types to make it easier to build up the data structures in a decentralized way. Here's an example:

interface MigrationManifest {
    [key: string]: (state: PersistedState) => PersistedState;
}
interface PersistedState { _persist?: PersistState }
interface PersistState { version: number; rehydrated: boolean; }

//////

type VERSION_3 = 3;
type VERSION_2 = VERSION_3 | 2;
type VERSION_1 = VERSION_2 | 1;
type VERSION_0 = VERSION_1 | 0;

interface TypeBundleBase {
    root: {};
    address: {};
    phoneNumber: {};
}

interface RootV0<B extends TypeBundleBase> { 
    name: string;
    address: B["address"];
    phoneNumber: B["phoneNumber"];
    _persist?: PersistState;
}
interface ContactsV2<B extends TypeBundleBase> {
    address: B["address"];
    phoneNumber: B["phoneNumber"];
}
interface RootV2<B extends TypeBundleBase> { 
    name: string;
    contacts: ContactsV2<B>;
    _persist?: PersistState;
}

interface AddressV0<B extends TypeBundleBase> { 
    street: string;
    city: string;
}
interface AddressV1<B extends TypeBundleBase> { 
    vacant: boolean;
}

type PhoneNumberV0<B extends TypeBundleBase> = string;
interface PhoneNumberV3<B extends TypeBundleBase> { 
    countryCode: string;
    countryPhoneNumber: string;
}

interface Types<V extends VERSION_0, B extends TypeBundleBase> { 
    root: [V] extends [VERSION_2] ? RootV2<B> : RootV0<B>;
    address: [V] extends [VERSION_1] ? AddressV1<B> : AddressV0<B>;
    phoneNumber: [V] extends [VERSION_3] ? PhoneNumberV3<B> : PhoneNumberV0<B>; 
}

interface FixedTypes<V extends VERSION_0> extends Types<V, FixedTypes<V>> { }
type Root<V extends VERSION_0> = FixedTypes<V>["root"];

declare function migrateState1(state: Root<VERSION_0>): Root<VERSION_1>;
declare function migrateState2(state: Root<VERSION_1>): Root<VERSION_2>;
declare function migrateState3(state: Root<VERSION_2>): Root<VERSION_3>;

const manifest: MigrationManifest = {
    1: (state: PersistedState) => {
        const persistedState = state as Root<VERSION_0>;
        const migratedState: Root<VERSION_1> = migrateState1(persistedState);
        return migratedState;
    },
    2: (state: PersistedState) => {
        const persistedState = state as Root<VERSION_1>;
        const migratedState: Root<VERSION_2> = migrateState2(persistedState);
        return migratedState;
    },
    3: (state: PersistedState) => {
        const persistedState = state as Root<VERSION_2>;
        const migratedState: Root<VERSION_3> = migrateState3(persistedState);
        return migratedState;
    }
}

There may be glitches with this approach that I haven't anticipated, but it's worth a try. I doubt I'm the first person to think of the approach, but a few quick web searches for prior work were unsuccessful.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM