如何键入具有相应类型的元组数组?

How to type tuple array with corresponding types?

谁能帮我解决这个不寻常的(我认为的)问题?

最初,我实现的是类型化 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
}

上面的代码运行良好。然后我想实现一个能够为这个映射设置多个值的函数:

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)
  }
}

如何键入 [TKey, TVal][] 元组数组以便对输入进行相应的类型检查?喜欢:

const tm = new Map() as TypedMap

setMany(
  tm,
  [
    [KeyType.aa, 'Foo'], // Error, aa requires boolean
    [KeyType.bb, 'Bar'] // Nice, bb requires string
  ]
)

据我了解,我需要这样的东西:

type ChangeTuple<TK extends KeyType> = [TK, ValueTypes[TK]]

但要使用参数和数组。我试过这个:

interface setManyV2 {
  (
    map: TypedMap,
    change: (
      TypedMap |
      ChangeTuple[] // requires Generic type, but ChangeTuple<KeyType>[] doesn't work because TK doesn't extend anymore
    )
  ): void
}

测试用例:

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
)

我假设您想通过 TypedMap 重载您的 Map 实现。如果是,还有更好的方法。

为了使您的 Map 过载,您需要交叉 Map 类型,如下所示:Map<'a', boolean> & Map<'b', string>.

既然你有KeyTypeValueTypes,我们可以将它们合并成一个数据结构:


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>

现在为了产生 Map 重载,我们需要获得 TypedMap 个对象值的并集。 很简单,只需使用这个工具:

type Values<T> = T[keyof T]

现在我们需要将并集转换为交集 (UnionToIntersection):

// credits goes to 
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>>>

您可以找到有关创建动态重载的更多信息 here TypedMap 是重载的 Map 数据结构。让我们来测试一下:

const tm: TypedMap = new Map();

tm.set(KeyType.aa, true) // ok
tm.set(KeyType.bb, true) // expected error

C考虑这个例子:

export enum KeyType {
    aa = 'aa',
    bb = 'bb'
}

export interface ValueTypes {
    aa: boolean,
    bb: string
}


// credits goes to 
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 以上示例回答了您的第一个问题:How to make it work with tuples.

但是有一个缺点,如果一个元组元素无效,整个元组将被高亮显示

请告诉我它是否适合您。如果是的话,我会提供更多的解释。