I have the following State
shape and want to define a flattened slice
const that has the types of the state properties without needing to explicitly define/reference them again, so I would like a mapped type like MapFlatSlice
:
type State = {
fruit: {
mango: 'haden' | 'keitt';
papaya: 'sunrise' | 'strawberry';
};
colors: {
green: 10 | 20 | 30;
};
season: {
nartana: 'april' | 'may' | 'june' | 'july' | 'august';
};
};
type MapFlatSlice = {
// ...
};
const slice = {
fruit: ['mango', 'papaya'],
colors: ['green'],
season: ['nartana'],
} as const;
type S = MapFlatSlice<typeof slice>;
Where S
should be:
type S = {
mango: 'haden' | 'keitt';
papaya: 'sunrise' | 'strawberry';
green: 10 | 20 | 30;
nartana: 'april' | 'may' | 'june' | 'july' | 'august';
};
In the above example, slice
is used for something like redux's mapStateToProps
, like this:
const makeSlice = (s: Slice): (t: MapFlatSlice<typeof s>) => void => {
// ...
};
So given:
const s1 = makeSlice(slice);
Then s1
would be:
const s1 = (props: {
mango: "haden" | "keitt";
papaya: "sunrise" | "strawberry";
green: 10 | 20 | 30;
nartana: "april" | "may" | "june" | "july" | "august";
}) => {};
(where the type of props
in s1
is the same as S
from above)
So I think that MapFlatSlice
should be something like...
type MapFlatSlice<S extends { [k in keyof State]?: readonly (keyof State[k])[] }> = {
[k in keyof S]: {
[_k in S[k][number]]: ...
};
};
But I don't know how to "flatten" it, and also it doesn't work to index S[k]
with number
.
I'd be inclined to approach this as follows:
Which types are "sliceable"? That is, when you write
MapFlatSlice<T>
, what are the allowable types forT
? I think it's any typeT extends Sliceable<T>
whereSliceable<T>
is defined as:That is, if
T
is sliceable, each property at keyK
must be a (possibly read-only) array of keys ofState[K]
. IfK
is not a key ofState
, then there should be no property at keyK
(so the property is of typenever
).Now that we've got a constraint on the input to
MapFlatSlice
, what should the output be? I find this easiest to break into two steps. We will take the input typeT
and "invert" the keys and values, to give us something closer to the shape of what we want which still has enough information in it to get the job done.Here it is:
If we apply that directly to
typeof slice
, you'll see what we get:The keys are the ones you want in your final output, and the values are the relevant keys from
State
we need to consult.Given such an intermediate type as a new input
U
, we can do the final transformation:Recall that
U[K]
is the key fromState
, whileK
is the subkey. So we want to writeState[U[K]][K]
. But the compiler can't tell that such index accesses are valid, so we useIdx<O, P>
in place ofO[P]
, which we define now:Let's say we have an object type
O
and a key typeP
, but the compiler doesn't know thatP
is a key ofO
even though we think it is. That means we can't directly write the indexed access typeO[P]
without error. Instead, we can define anIdx
type alias which checksP
before indexing:Now, anywhere
O[P]
yields an error, we can replace it withIdx<O, P>
. And so insideDoSlice
,State[U[K]][K]
becomesIdx<Idx<State, U[K]>, K>
.And finally,
MapFlatSlice<T>
is defined in terms of the constraint and the output operation:So, does it work?
Looks good. You can verify that other mappings should also work, and that adding incorrect properties to
slice
should result in compiler warnings.Playground link to code