I'd like to create a generic multi-dimensional map type where the last of N types is the final value and the preceding types will effectively be a key.
In other words:
let m:MDMap<Coat,Pie,Date,Location>;
//...
m.get(myWoolCoat,myApplePie,yesterday) // returns a Location
Without variadic generics though it looks like I'm going to end up with something like this:
export type MultiMap<
A extends any,
B extends any,
C = void,
D = void,
// ... etc.
> = D extends void
? C extends void
? Map<A,B>
: Map<A,Map<B,C>>
: Map<A,Map<B,Map<C,D>>>
let m:MultiMap<Coat,Pie,Date,Location>;
I know Typescript handles variadic tuple types but I haven't been able to bridge that ability to something resembling the above desire.
Should I make do with the work-around or is there some way to pull off some kind recursive generic?
CodePudding user response:
TypeScript doesn't have variadic generics, so there's no way to write something like MultiMap<...K, V>
meaning that there is some arbitrary number of key type parameters and one value type parameter. But, as you note, there are tuple types which can hold an arbitrary ordered list of types, so you might be satisfied with MultiMap<K, V>
where the K
is a tuple. So instead of MultiMap<Coat, Pie, Date, Location>
you'd write MultiMap<[Coat, Pie, Date], Location>
. It's just a few more characters anyway.
As for making MultiMap<[Coat, Pie, Date], Location>
resolve to Map<Coat, Map<Pie, Map<Date, Location>>>
, we can do that by defining MultiMap<K, V>
as a recursive conditional type where we use variadic tuple types to pull apart the K
tuple into its first element F
and the rest of the tuple R
:
type MultiMap<K extends any[], V> = K extends [infer F, ...infer R] ?
Map<F, MultiMap<R, V>> : V;
The syntax K extends [infer F, ...infer R] ? XXX : YYY
is known as conditional type inference where we can bring new type parameters into scope by having the compiler infer them. K extends [infer F, ...infer R] ? XXX : YYY
means that we want the compiler to try to match K
to a variadic tuple with some initial element F
and some rest element R
. If it works, then inside the XXX
type expression, F
will be the first element type and R
will be a shorter tuple representing the rest. That is, if K
is [Coat, Pie, Date]
then F
will be Coat
and R
will be [Pie, Date]
. And hence Map<F, MultiMap<R, V>>
will be Map<Coat, MultiMap<[Pie, Date], V>>
and you can see the recursion happen. If K
is an empty tuple, then we have no keys at all and we just use V
, so that the base case of the recursion terminates how we want.
Let's make sure it works:
type MyMap = MultiMap<[Coat, Pie, Date], Location>;
// type MyMap = Map<Coat, Map<Pie, Map<Date, Location>>>
Looks good!