Изменить значения объекта путем динамического поиска ключей. TypeScript - PullRequest
1 голос
/ 21 апреля 2020

Я пытаюсь написать функцию, которая возвращает другую функцию, которая позволяет мне изменять ключи объекта в TypeScript. Это для использования в редукторе React.

Если у меня есть объект состояния с ключами dogs, dogIds и другой объект состояния с ключами cats и catIds, я бы хотел написать patchGroup(group: string), чтобы, если я передать cat или dog, он возвращает функцию, которая позволяет мне изменять эти клавиши.

Я новичок в TypeScript, поэтому я попытался индексировать со строками. Однако из-за ошибки TypeScript я не могу использовать строки для индексации других моих 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, и я не хочу дублировать мой код.

Текущая попытка со строками (не работает)


const patchGroup = (group: string) => {
  // similar logic to `patchDog`
  const patch = (
    state: DogsState | CatsState,
    payload: DogResponse | DogCreateRequest | CatResponse | CatCreateRequest,
  ) => {
    const groupIdx = state[`{$group}Ids`].indexOf(payload[`${group}`][`${group}Id`]);
  // string failed; need to get the exact key somehow
  }

  return patch;
}

// ================= State Objects =================
interface DogsState {
  dogs: Array<Dog>;
  dogIds: Array<string>;
  loading: boolean;
  // etc. 
} 

interface CatsState {
  cats: Array<Cat>;
  catIds: Array<string>;
  loading: boolean;
  // etc. 
} 

// ================= Requests =================
type DogRequest = {
  dogId: string;
};
type DogCreateRequest = {
  dog: Dog;
};

type CatRequest = {
  catId: string;
};

type CatCreateRequest = {
  cat: Cat;
};

// ================= Responses =================
type DogResponse = {
  dog: Dog;
  lastUpdate: number;
  error: string;
}
type CatResponse = {
  cat: Cat;
  lastUpdate: number;
  error: string;
}

// ================= App Objects =================
interface Dog {
  dogId: string;
  name: string;
  // ... etc.
}

interface Cat {
  catId: string;
  name: string;
  // etc.
}

Пока что я просмотрел:

1 Ответ

1 голос
/ 21 апреля 2020

Основная проблема с ожиданием того, что TypeScript проверит безопасность типов в вашем сценарии, заключается в том, что в TypeScript в настоящее время нет способа объединить строковые литералы типов . См. microsoft / TypeScript # 12940 для обсуждения этого и microsoft / TypeScript # 12754 для предложения, которое, если оно будет реализовано, позволит это сделать. Поэтому, хотя компилятор понимает, что "cat" является строковым литеральным типом и может отличать guish от "dog", он не может знать, что "cat"+"Id" приведет к строковому литеральному типу "catId" , Он знает только, что "cat"+"Id" имеет тип string.

И поэтому он не знает, что делать, когда вы индексируете в Cat с "cat"+"Id". Он не может индексироваться в Cat с помощью обобщенного c string из-за отсутствия подписи строкового индекса.


Так что это зависит от вашей цели. Если ваша цель - просто устранить ошибки и не беспокоиться о проверке безопасности компилятором, тогда вы можете начать использовать утверждения типа , например:

state[`{$group}Ids` as keyof typeof state]

Но затем вы столкнетесь с новым проблема ... ваша внутренняя функция, кажется, принимает state и payload, что является объединением версий Cat и Dog. И это также не является безопасным типом, поскольку он позволяет вам вызывать patchGroup("dog")(someCatState, someCatPayload):

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

Чтобы подавить эти допустимые ошибки типа, вам нужно начать повсеместно утверждать, как это:

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

В этот момент вы можете просто go до any:

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

Бла.


Следующим шагом к восстановлению некоторой безопасности типов со стороны вызова будет создание patchGroup() generi c функции, которая позволяет вызывать себя только со всеми 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() по-прежнему небезопасна, но по крайней мере вызывающим абонентам предоставляется подпись безопасного типа, которая не будет принимать несовпадающие входные данные:

patchGroup("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"];

Компилятор будет все еще блокировать при каждой операции индексирования :

    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"]'.

Итак, мы, вероятно, пошли дальше, чем мы можем ожидать, что компилятор нам поможет. С конкатенацией имен свойств и использованием объединений типов лучшее, что я могу придумать, - это приведенная выше безопасная для вызова реализация-небезопасная версия.


С этого момента следующим шагом к созданию этого правила будет остановка пытаясь заставить компилятор понять этот вид конкатенации строк и кода вычисляемых свойств, и вместо этого провести рефакторинг ваших Cat и Dog интерфейсов для расширения одного Animal интерфейса. Не задавайте свойствам одноразовые имена, такие как catIds и dogIds; вместо этого просто дайте им одно и то же animalIds имя:

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

Вы можете преобразовать все ваши типы Dog и Cat в новый Animal -совместимый тип (код выписан в ссылке внизу). ) и тогда вам больше не понадобится patchGroup(). У вас может быть одна функция generi c patch:

const patch = <A extends Animal>(
  state: AnimalState<A>,
  payload: AnimalResponse<A> | AnimalCreateRequest<A>,
): AnimalState<A> => {
  const groupIdx = state.animalIds.indexOf(payload.animal.animalId);
  return {
    ...state,
    loading: false,
    animals: [...state.animals, payload.animal],
    animalIds: [...state.animalIds, payload.animal.animalId]
  };
}

Эта реализация проверена компилятором как безопасная и будет безопасна также на сайте вызовов:

patch(catsState, catResponse); // okay
patch(dogsState, dogResponse); // okay
patch(catsState, dogResponse); // error! DogResponse not valid

Вполне могут быть причины, по которым вы не можете выполнить этот рефакторинг, но работать с ним намного приятнее, и я все равно хотел бы попробовать, или, в случае неудачи, забыть о рефакторинге вашего исходного дублированного кода. на одну реализацию; дублирование раздражает, но по крайней мере компилятор все еще помогает вам.

Хорошо, надеюсь, это поможет; удачи!

Детская площадка ссылка на код

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...