How to type tuple array with corresponding types?

0

Issue

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
)

Solution

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.

Answered By – captain-yossarian

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave A Reply

Your email address will not be published.

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More