2

我有一个使用 redux 和 redux-persist 的 Typescript 项目。是否可以使用 Typescript 进行 redux-persist 迁移配置?主要困难归结为:如果你有一个 Typescript 类型的根 redux 状态,它通常/总是代表你的状态的最新版本(不一定是以前持久的)。在迁移的上下文中,您如何表示以类型方式从持久性中读取的数据,知道它与您的状态的最新版本不匹配?一些细节:

这是用于数据迁移的 redux-persist 的 Typescript API:

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

有意义的是,您提供的MigrationManifest键是版本号,值是接收持久状态并返回新持久状态的函数。由于PersistedState只是一个接口,因此您似乎可以拥有一种传入类型并返回另一种类型(在您的数据架构发生更改的情况下)。

所以假设我有以下类型作为我的根持久存储类型。它符合PersistedState接口:

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

type Address = {
    street: string
    city: string
}

在未来的某个时候,我将我的模型更新为:

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

type Address = {
    vacant: boolean
}

我需要提供一个看起来像这样的迁移:

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

我正在苦苦挣扎的地方是获取传入状态的类型(我的演员所在的???位置)。在像这样的简单示例中,很容易维护我的状态的每个版本的记录,并根据需要导入和使用它们:

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
  }
}

在实践中,并不是那么简单。我有一个复杂的状态树,并不是所有的状态树都定义在一个中央且易于版本化的位置。

我可以梦想的一种解决方案,但我不知道是否可行,将以某种方式创建我的RootState类型的版本,其中所有中间类型别名和接口名称“已溶解”。就像是:

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

如果我能以某种自动方式创建它,那么在迁移中保存和使用将变得容易和方便。

是否有任何想法,或者关于如何在 react-redux 迁移中有效使用 Typescript 的任何其他建议?

4

2 回答 2

0

在某些时候,您必须指定什么版本具有什么类型。但是您可以使用查找类型和条件类型来更轻松地以分散方式构建数据结构。这是一个例子:

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;
    }
}

这种方法可能会出现我没有预料到的故障,但值得一试。我怀疑我是第一个想到这种方法的人,但是一些快速的网络搜索对以前的工作没有成功。

于 2018-08-02T04:07:11.940 回答
0

遇到同样的问题。

我已经通过手动创建扩展类型来修复它PersistedState。我们可以直接扩展它,因为它是可选的并且可以是未定义的。

似乎redux-persist不需要通用来使一切都与 TS 一起工作。所以我写了解决方法。示例是关于重命名该州的字段,但它可以很容易地扩展。field1重命名并进入field1Renamed新状态的主要目标。

import { createMigrate } from 'redux-persist';
import { MigrationManifest, PersistState } from 'redux-persist/es/types';

type RootAppState = {
    field1Renamed: number;
    field2: number;
    field3: number;
    field4: number;
    field5: number;
    field6: number;
    field7: number;
}

type PersistAppStateV1 = Pick<
  RootAppState,
  | 'field1Renamed'
  | 'field2'
  | 'field3'
  | 'field4'
  | 'field5'
> & {
  _persist: PersistState;
};

type PersistAppStateV0 = Omit<PersistAppStateV1, 'field1Renamed'> & {
    field1: RootAppState['field1Renamed'];
};

const migrations: MigrationManifest = {
  1: (state): PersistAppStateV1 | undefined => {
    if (!state) {
      return state;
    }
    const { field1, ...otherState } = state as PersistAppStateV0;
    return {
      ...otherState,
      field1Renamed: field1,
    };
  },
};

const migrate = createMigrate(migrations);

const persistConfig = {
  key: 'root',
  version: 1,
  migrate,
  whitelist: [
    'field1Renamed',
    'field2',
    'field3',
    'field4',
    'field5',
  ],
};
于 2021-06-15T15:50:53.910 回答