Could anyone help me with this unusual (as I think) problem?
Initially, I was implementing typed Map.
export enum KeyType {
aa = 'aa',
bb = 'bb'
}
export interface ValueTypes {
aa: boolean,
bb: string
}
interface TypedMap extends Map<KeyType, ValueTypes[KeyType]> {
get<TK extends KeyType>(k: TK): ValueTypes[TK] | undefined
set<TK extends KeyType>(k: TK, v: ValueTypes[TK]): this
}
The code above works well. Then I wanted to implement a function that able to set multiple values to this map:
function setMany<TKey, TVal> (
map: Map<TKey, TVal>,
change: (
Map<TKey, TVal> |
[TKey, TVal][]
)
): void {
const entries = change instanceof Map ? change.entries() : change
for (const [key, value] of entries) {
map.set(key, value)
}
}
How can I type [TKey, TVal][]
tuple array so that I have corresponding type check on input? Like:
const tm = new Map() as TypedMap
setMany(
tm,
[
[KeyType.aa, 'Foo'], // Error, aa requires boolean
[KeyType.bb, 'Bar'] // Nice, bb requires string
]
)
As I understand, I need something like this:
type ChangeTuple<TK extends KeyType> = [TK, ValueTypes[TK]]
but to work with argument and array. I tried this:
interface setManyV2 {
(
map: TypedMap,
change: (
TypedMap |
ChangeTuple[] // requires Generic type, but ChangeTuple<KeyType>[] doesn't work because TK doesn't extend anymore
)
): void
}
Test cases:
setMany(
tm,
[
[KeyType.aa, true], // correct, aa requires boolean
[KeyType.aa, false], // correct, aa requires boolean
[KeyType.bb, 'any string'], // correct bb requires string
[KeyType.aa, undefined], // incorrect, aa requires boolean, not undefined
[KeyType.aa, new Set()], // incorrect, aa requires boolean, not Set
[KeyType.aa, 'any string'], // incorrect, aa requires boolean, not string
[KeyType.bb, new Set()], // incorrect, aa requires string, not Set
[KeyType.bb, undefined], // incorrect, aa requires string, not undefined
[KeyType.bb, false], // incorrect, aa requires string, not boolean
[KeyType.bb, true] // incorrect, aa requires string, not boolean
]
)
const tm2 = new Map() as TypedMap
tm2.set(KeyType.aa, true)
tm2.set(KeyType.bb, 'string')
setMany(
tm,
tm2
)
I assume that you want to overload your Map
implementation by TypedMap
. If yes, there is a better way to do it.
In order to overload your Map
you need to intersect Map
types, like this: Map<'a', boolean> & Map<'b', string>
.
Since you have KeyType
and ValueTypes
, we can merge them into one data structure:
type MapOverloading<Keys extends string, Values extends Record<Keys, unknown>> = {
[Prop in Keys]: Map<Prop, Values[Prop]>
}
// type TypedMap = {
// aa: Map<KeyType.aa, boolean>;
// bb: Map<KeyType.bb, string>;
// }
type TypedMap = MapOverloading<KeyType, ValueTypes>
Now in order to produce Map overloading, we need to obtain a union of TypedMap
object values.
It is simple, just use this util:
type Values<T> = T[keyof T]
Now we need to convert union to intersection (UnionToIntersection
):
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type MapOverloading<Keys extends string, Values extends Record<Keys, unknown>> = {
[Prop in Keys]: Map<Prop, Values[Prop]>
}
type Values<T> = T[keyof T]
type TypedMap = UnionToIntersection<Values<MapOverloading<KeyType, ValueTypes>>>
More about creating dynamic overloadings you can find here
TypedMap
is an overloaded Map
data structure. Lets' test it:
const tm: TypedMap = new Map();
tm.set(KeyType.aa, true) // ok
tm.set(KeyType.bb, true) // expected error
COnsider this example:
export enum KeyType {
aa = 'aa',
bb = 'bb'
}
export interface ValueTypes {
aa: boolean,
bb: string
}
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type MapOverloading<Keys extends string, Values extends Record<Keys, unknown>> = {
[Prop in Keys]: Map<Prop, Values[Prop]>
}
type Values<T> = T[keyof T]
type TypedMap = UnionToIntersection<Values<MapOverloading<KeyType, ValueTypes>>>
const tm: TypedMap = new Map();
tm.set(KeyType.aa, true) // ok
tm.set(KeyType.bb, true) // expected error
type IsNever<T> = [T] extends [never] ? true : false
type TupleToMap<Tuple extends any[], ResultMap extends Map<any, any> = never> =
Tuple extends []
? ResultMap
: Tuple extends [[infer Key, infer Value], ...infer Rest]
? IsNever<ResultMap> extends true ? TupleToMap<Rest, Map<Key, Value>> : TupleToMap<Rest, ResultMap & Map<Key, Value>>
: never
type Validation<Tuple extends any[], CustomMap> = CustomMap extends TupleToMap<Tuple> ? Tuple : []
function setMany<
Key extends string,
Value,
CustmoMap extends Map<Key, Value>,
Tuple extends [Key, Value],
Change extends Tuple[],
>(
map: CustmoMap,
change: Validation<[...Change], CustmoMap>,
): void {
for (const [key, value] of change) {
map.set(key, value)
}
}
setMany(
tm,
[
[KeyType.aa, true], // ok
[KeyType.aa, false] // ok
]
)
setMany(
tm,
[
[KeyType.bb, 's'], // ok
[KeyType.bb, 'hello'], // ok
]
)
setMany(
tm,
[
[KeyType.bb, 's'], // ok
[KeyType.aa, true, // ok
]
)
setMany(
tm,
[
[KeyType.aa, undefined] // expected error
]
)
setMany(
tm,
[
[KeyType.aa, new Set()] // expected error
]
)
setMany(
tm,
[
[KeyType.bb, undefined] // expected error
]
)
setMany(
tm,
[
[KeyType.bb, new Set()] // expected error
]
)
setMany(
tm,
[
[KeyType.bb, false] // expected error
]
)
setMany(
tm,
[
[KeyType.bb, true] // expected error
]
)
setMany(
tm,
[
[KeyType.aa, 'any string'] // expected error
]
)
Playground Above example answers on your first question: How to make it work with tuples.
However there is a drawback, if one of tuple element is invalid, whole tuple will be highlighted
Please let me know if it works for you. If it is, I will provide more explanation.