通过动态查找键来修改对象值TypeScript

时间:2020-04-21 01:43:16

标签: javascript reactjs typescript

我正在尝试编写一个函数,该函数返回另一个函数,该函数使我可以在TypeScript中修改对象的键。用于React减速器中。

如果我有一个键为dogsdogIds的状态对象,而另一个键为catscatIds的状态对象,我想写一个{{ 1}},这样如果我传入patchGroup(group: string)cat,它将返回一个函数,让我可以修改这些键。

我是TypeScript的新手,所以我尝试使用字符串建立索引。但是,TypeScript出错了,因为我无法使用字符串来索引其他dog ...

明确说明/使用类型的示例

例如,我为狗定义以下状态:

types

然后我可以通过辅助功能interface DogsState { dogs: Array<Dogs>; dogIds: Array<string>; loading: boolean; // etc. } 修改状态:

patchDog

我现在想要一只猫用

function patchDog(
  state: DogsState,
  payload: DogResponse | DogCreateRequest,
  ): DogsState {
    const dogIndex = state.dogIds.indexOf(payload.dog.dogId)
    return {
      ...state,
      loading: false,
      dogs: [...state.dogs, payload.dog],
      dogIds: [...state.dogIds, payload.dog.dogId]
    }
}

稍微修改interface CatsState { cats: Array<Cats>; catIds: Array<string>; loading: boolean; // etc. } 很简单,我不想重复我的代码。

当前尝试使用字符串(不起作用)

patchDog

到目前为止,我已经调查了:

1 个答案:

答案 0 :(得分:1)

期望TypeScript在您的方案中验证类型安全性的主要问题是TypeScript当前没有连接string literal types的方法。有关此问题的讨论,请参见microsoft/TypeScript#12940,有关建议,请参见microsoft/TypeScript#12754(如果实施)将允许这样做。因此,尽管编译器理解"cat"是文字字符串类型并且可以将其与"dog"区别开来,但它无法知道"cat"+"Id"会导致字符串文字类型{{1 }}。它只知道"catId"的类型为"cat"+"Id"

因此,当您使用string索引到Cat时,它不知道要做什么。由于缺少字符串索引签名,因此无法使用通用的"cat"+"Id"索引到Cat


因此,这取决于您的目标是什么。如果您的目标只是抑制错误,而不用担心编译器的安全性验证,那么可以像这样开始使用type assertions

string

但是随后您遇到了一个新问题……您的内部函数似乎接受state[`{$group}Ids` as keyof typeof state] state版本的并集payloadCat。而且也不安全,因为它允许您调用Dog

patchGroup("dog")(someCatState, someCatPayload)

为了消除这些合法的类型错误,您需要像这样在所有地方开始声明:

patchGroup("dog")(catsState, catResponse); // no error, oops

这时,您最好一直走到any

const patchGroup = (group: string) => {
  const patch = (
    state: DogsState | CatsState,
    payload: DogResponse | DogCreateRequest | CatResponse | CatCreateRequest,
  ): DogsState | CatsState => {
    const groupIdx = (state[`{$group}Ids` as keyof typeof state] as any as Array<string>).
      indexOf((payload[`${group}` as keyof typeof payload] as Cat | Dog)[`${group}Id` as keyof (Cat | Dog)] as any as string);
    return {
      ...state,
      loading: false,
      [`{$group}s`]: [...state[`{$group}s` as keyof typeof state] as any as Array<Cat | Dog>, payload[group as keyof typeof payload]],
      [`{$group}Ids`]: [...state[`{$group}Ids` as keyof typeof state] as any as Array<string>, payload[group as keyof typeof payload][`{$group}Id`]]
    } as any as DogsState | CatsState;
  }
  return patch;
}

布拉。


从调用端恢复某种类型安全性的下一步是使const patchGroup2 = (group: string) => { const patch = ( _state: DogsState | CatsState, _payload: DogResponse | DogCreateRequest | CatResponse | CatCreateRequest, ): DogsState | CatsState => { const state: any = _state; const payload: any = _payload; const groupIdx = state[`{$group}Ids`].indexOf((payload[`${group}`])[`${group}Id`]); return { ...state, loading: false, [`{$group}s`]: [...state[`{$group}s`], payload[group]], [`{$group}Ids`]: [...state[`{$group}Ids`], payload[group][`{$group}Id`]] }; } return patch; } 成为generic函数,该函数仅允许自己使用全部patchGroup()或全部{{1 }}输入:

Cat

Dog的实现仍然不是类型安全的,但是至少会向调用者显示一个安全的类型签名,该签名不会接受不匹配的输入:

interface PatchGroup {
  cat: (state: CatsState, payload: CatResponse | CatCreateRequest) => CatsState,
  dog: (state: DogsState, payload: DogResponse | DogCreateRequest) => DogsState
}
const patchGroup = <K extends keyof PatchGroup>(group: K): PatchGroup[K] =>
  (state: any, payload: any) => {
    const groupIdx = state[`{$group}Ids`].indexOf((payload[`${group}`])[`${group}Id`]);
    return {
      ...state,
      loading: false,
      [`{$group}s`]: [...state[`{$group}s`], payload[group]],
      [`{$group}Ids`]: [...state[`{$group}Ids`], payload[group][`{$group}Id`]]
    };
  }

试图获得patchGroup()的实现以供编译器验证为安全类型可能只是不值得的努力。这里的绊脚石之一是不支持我所说的correlated record typespatchGroup("dog")(catsState, catResponse); // error now // -------------> ~~~~~~~~~ // 'CatsState' is not assignable to 'DogsState'. 接口之间的关系和patchGroup()接口之间的关系很难表示为CatXXX联合之间的关系。即使我告诉编译器,它也需要了解有关您的问题中使用的字符串文字之间的关系的所有信息,例如:

DogXXX

编译器将在每次索引操作时静止

CatXXX | DogXXX

因此,我们可能已经超出了预期的编译器可以为我们提供帮助的范围。通过连接属性名称和使用类型的并集,我能想到的最好的就是上面的调用安全的实现不安全的版本。


从这里开始,使这个理智的下一步将是停止尝试迫使编译器理解这种字符串连接和计算属性代码,而是重构interface CatBundle { ks: "cats", id: "catId", ids: "catIds" obj: Cat, req: CatRequest, crq: CatCreateRequest, rsp: CatResponse, stt: CatsState } interface DogBundle { ks: "dogs", id: "dogId", ids: "dogIds" obj: Dog, req: DogRequest, crq: DogCreateRequest, rsp: DogResponse, stt: DogsState } interface Animals { dog: DogBundle, cat: CatBundle } // start implementation const patchGroup = <K extends keyof Animals>( k: K) => (state: Animals[K]['stt'], payload: Animals[K]['rsp'] | Animals[K]['crq'] ): Animals[K]['stt'] => { const id = k + "Id" as Animals[K]["id"]; const ks = k + "s" as Animals[K]["ks"]; const ids = k + "Ids" as Animals[K]["ids"]; 和{{1} }接口以扩展单个 const groupIdx = state[ids].indexOf(payload[k][id]); // error! // ~~~~~~~~~~ ~~~~~~~~~~ // Animals[K]["ids"] cannot be used to index type 'Animals[K]["stt"] // Type 'K' cannot be used to index type 'Animals[K]["rsp"] | Animals[K]["crq"]'. 接口。不要给属性CatDog这样的随机数名称;而是给他们使用相同的Animal名称:

catIds

您可以将所有dogIdsanimalIds类型重构为新的interface AnimalState<A extends Animal> { animals: Array<A>; animalIds: Array<string>; loading: boolean; // etc } interface DogsState { animals: Array<Dog>; animalIds: Array<string>; loading: boolean; // etc. } interface CatsState { animals: Array<Cat>; animalIds: Array<string>; loading: boolean; // etc. } 兼容类型(代码在底部的链接中写出),然后就不需要{{ 1}}。您可以有一个通用的Dog函数:

Cat

此实现已由编译器验证为安全,并且在调用站点也将安全:

Animal

您可能无法执行重构,这可能是有原因的,但是与它一起工作要好得多,我还是很想尝试,否则,忘记将原始重复的代码重构为单一实施;复制很烦人,但至少编译器仍在帮助您。

好的,希望能有所帮助;祝你好运!

Playground link to code