I'm trying to strongly-type a globalizeSelectors
function that would transform a map of redux selector functions such that they will accept a GlobalState
type instead of their StateSlice type based on the key of their StateSlice (where StateSlice means it's a value of one of the GlobalState object's properties).
The tricky part is that the return types of the selectors can all be different, and I don't quite know how to type that variation (or if it's even possible). Based on the typescript docs, I'm guessing this might involve some clever use of the infer
operator, but my typescript-fu isn't quite at that level yet.
Here's what I've got so far: (BTW, for you reduxy types, nevermind the fact that these selectors don't handle props or additional args -- I've removed that to simplify this a bit)
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)
Introduction to TypeScript function types A function type has two parts: parameters and return type. When declaring a function type, you need to specify both parts with the following syntax: (parameter: type, parameter:type,...) => type
Weak Map in TypeScript The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced. The keys must be objects and the values can be arbitrary values. Keys of WeakMaps are of the type Object only.
The Array.map() is an inbuilt TypeScript function that is used to create a new array with the results of calling a provided function on every element in this array. Syntax: array.map(callback[, thisObject])
Specifying Type Arguments TypeScript can usually infer the intended type arguments in a generic call, but not always. For example, let’s say you wrote a function to combine two arrays: function combine < Type > (arr1: Type [], arr2: Type []): Type [] {
In globalizeSelectors
the type sliceSelectors
is {[key: string]: SliceSelector<TKey, T> }
. But who should T
be in this case? In your simple version T
is going to be the return type of that particular slice selector, but when you map multiple T
can't be all the return types.
The solution I would use is to use T
to rerpesent the whole type of sliceSelectors
with th restriction that all memebers must be of type SliceSelector<TKey, any>
. The any
there just represents we don't care what the return type of the slice selectors are.
Even though we don't care what the return type of each slice selector is, T
will capture the type acturately (ie the return types of each function in the object will not be any
but the actual type). We can then use T
to create a mapped type that globalizez each function in the object.
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)
The only small issue is that mapValues
needs some type assertions to work, I don't think mapValues
is equiped to deal with these types.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With