Основная проблема с ожиданием того, что 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
Вполне могут быть причины, по которым вы не можете выполнить этот рефакторинг, но работать с ним намного приятнее, и я все равно хотел бы попробовать, или, в случае неудачи, забыть о рефакторинге вашего исходного дублированного кода. на одну реализацию; дублирование раздражает, но по крайней мере компилятор все еще помогает вам.
Хорошо, надеюсь, это поможет; удачи!
Детская площадка ссылка на код