redux - Typescript: Cum să tipați strict o funcție care transformă o hartă de funcții în funcții similare cu un parametru de tip diferit?
Încerc să tipizez strict o funcție globalizeSelectors care ar transforma un map de funcții selector din redux astfel încât să accepte un tip GlobalState în loc de tipul lor StateSlice bazat pe cheia StateSlice (unde StateSlice înseamnă că este o valoare a uneia dintre proprietățile obiectului GlobalState).
Partea dificilă este că tipurile de returnare ale selectorilor pot fi toate diferite și nu știu exact cum să tipizez această variație (sau dacă este chiar posibil). Pe baza documentației TypeScript, bănuiesc că acest lucru ar putea implica o utilizare inteligentă a operatorului infer, dar abilitățile mele de TypeScript nu sunt încă la acel nivel.
Iată ce am până acum: (BTW, pentru voi, cei care folosiți Redux, nu țineți cont de faptul că acești selectorii nu gestionează props sau argumente suplimentare - le-am eliminat pentru a simplifica puțin acest lucru)
import { mapValues } from 'lodash'
// my (fake) redux state types
type SliceAState = { name: string }
type SliceBState = { isWhatever: boolean }
type GlobalState = {
a: SliceAState;
b: SliceBState;
}
type StateKey = keyof GlobalState
type Selector<TState, TResult> = (state: TState) => TResult
type StateSlice<TKey extends StateKey> = GlobalState[TKey]
type GlobalizedSelector<TResult> = Selector<GlobalState, TResult>
const globalizeSelector = <TKey extends StateKey, Result>(
sliceKey: TKey,
sliceSelector: Selector<StateSlice<TKey>, Result>
): GlobalizedSelector<Result> => state => sliceSelector(state[sliceKey])
// an example of a map of selectors as they might be exported from their source file
const sliceASelectors = {
getName: (state: SliceAState): string => state.name,
getNameLength: (state: SliceAState): number => state.name.length
}
// fake global state
const globalState: GlobalState = {
a: { name: 'My Name' },
b: { isWhatever: true }
}
// so this works...
const globalizedGetName = globalizeSelector('a', sliceASelectors.getName)
const globalizedNameResult: string = globalizedGetName(globalState)
const globalizedGetNameLength = globalizeSelector(
'a',
sliceASelectors.getNameLength
)
const globalizedNameLengthResult: number = globalizedGetNameLength(globalState)
/* but when I try to transform the map to globalize all its selectors,
I get type errors (although the implementation works as untyped
javascript):
*/
type SliceSelector<TKey extends StateKey, T> = T extends Selector<
StateSlice<TKey>,
infer R
>
? Selector<StateSlice<TKey>, R>
: never
const globalizeSelectors = <TKey extends StateKey, T>(
sliceKey: TKey,
sliceSelectors: {
[key: string]: SliceSelector<TKey, T>;
}
) => mapValues(sliceSelectors, s => globalizeSelector(sliceKey, s))
const globalized = globalizeSelectors('a', sliceASelectors)
/*_________________________________________^ TS Error:
Argument of type '{ getName: (state: SliceAState) => string; getNameLength: (state: SliceAState) => number; }' is not assignable to parameter of type '{ [key: string]: never; }'.
Property 'getName' is incompatible with index signature.
Type '(state: SliceAState) => string' is not assignable to type 'never'. [2345]
*/
const globalizedGetName2: string = globalized.getName(globalState)
Răspuns #1
În globalizeSelectors tipul sliceSelectors este {[key: string]: SliceSelector<TKey, T> }. Dar cine ar trebui să fie T în acest caz? În versiunea ta simplă T va fi tipul de retur al acelui selector de slice particular, dar când faci maparea mai multor T nu pot fi toate tipurile de retur.
Soluția pe care aș folosi-o este să folosesc T pentru a reprezenta întregul tip al sliceSelectors cu restricția că toți membrii trebuie să fie de tip SliceSelector<TKey, any>. any de acolo reprezintă doar faptul că nu ne pasă care este tipul de retur al selectorilor de slice.
Chiar dacă nu ne pasă care este tipul de returnare al fiecărui selector de felie, T va captura tipul corect (adică tipurile de returnare ale fiecărei funcții din obiect nu vor fi any, ci tipul real). Putem apoi folosi T pentru a crea un tip mapat care să globalizeze fiecare funcție din obiect.
import { mapValues } from 'lodash'
// my (fake) redux state types
type SliceAState = { name: string }
type SliceBState = { isWhatever: boolean }
type GlobalState = {
a: SliceAState;
b: SliceBState;
}
type StateKey = keyof GlobalState
type GlobalizedSelector<TResult> = Selector<GlobalState, TResult>
const globalizeSelector = <TKey extends StateKey, Result>(
sliceKey: TKey,
sliceSelector: Selector<StateSlice<TKey>, Result>
): GlobalizedSelector<Result> => state => sliceSelector(state[sliceKey])
// an example of a map of selectors as they might be exported from their source file
const sliceASelectors = {
getName: (state: SliceAState): string => state.name,
getNameLength: (state: SliceAState): number => state.name.length
}
// fake global state
const globalState: GlobalState = {
a: { name: 'My Name' },
b: { isWhatever: true }
}
type Selector<TState, TResult> = (state: TState) => TResult
type StateSlice<TKey extends StateKey> = GlobalState[TKey]
// Simplified selctor, not sure what the conditional type here was trying to achive
type SliceSelector<TKey extends StateKey, TResult> = Selector<StateSlice<TKey>, TResult>
const globalizeSelectors = <TKey extends StateKey, T extends {
[P in keyof T]: SliceSelector<TKey, any>
}>(
sliceKey: TKey,
sliceSelectors: T
) : { [P in keyof T]: GlobalizedSelector<ReturnType<T[P]>> } => mapValues(sliceSelectors, s => globalizeSelector(sliceKey, s as any)) as any // Not sure about mapValues
const globalized = globalizeSelectors('a', sliceASelectors)
const globalizedGetName2: string = globalized.getName(globalState)
Singura problemă minoră este că mapValues are nevoie de niște afirmații de tip pentru a funcționa, nu cred că mapValues este echipat să gestioneze aceste tipuri.
