在 Typescript 中,我如何将 Array<T> 转换为 Map<K, V> 并在 T 是元组 [K, V] 时推断 K 和 V 而如果不是则具有编译时保护

In Typescript, how can I convert an Array<T> to a Map<K, V> and infer K and V if T is a tuple [K, V] while having compile time protection if it isn't

标题中的问题几乎说明了一切。问题是 T 不能被限制。

这是我尝试过的:

class ArrayWrapper<T> {
  constructor(private arr: T[]) {}

  toMap<K, V>() {
    return new Map(<T extends [K, V] ? [K, V][] : never>this.arr);
  }
}

我也试过这个:

class ArrayWrapper<T> {
  constructor(private arr: T[]) {}

  toMap() {
    return new Map(<T extends [infer K, infer V] ? [K, V][] : never>this.arr);
  }
}

在上述两种情况下,编译器都会给我一个“将类型 'T[]' 转换为 ... 可能是一个错误,因为两种类型都没有与另一种充分重叠。如果这是故意的,请将表达到'unknown'第一个”错误。

所以我尝试了一些不同的东西,而不是从不,我将条件类型的错误分支更改为 never[]。这消除了编译错误,但现在当我做类似的事情时:

class ArrayWrapper<T> {
  constructor(private arr: T[]) {}

  toMap() {
    return new Map(<T extends [infer K, infer V] ? [K, V][] : never[]>this.arr);
  }
}

const notConvertibleToMap = [1, 2, 3];
const convertibleToMap: [number, string][] = [[1, 'one'], [2, 'two'], [3, 'three']];

const arr1 = new ArrayWrapper(notConvertibleToMap);
const map1 = arr1.toMap();

const arr2 = new ArrayWrapper(convertibleToMap);
const map2 = arr2.toMap();

编译器为 map1map2 推断 Map

我希望编译器能够:

就像我一开始说的,显然是这样限制class中T的类型

class ArrayWrapper<T extends [K, V], K, V> {
  constructor(private arr: T[]) {}

  toMap() {
    return new Map(this.arr);
  }
}

将允许 KV 的正确推断,但我不想限制 T。我需要这个 class 来接受任何类型的包含值的数组T.

对此进行了额外的尝试:

class ArrayWrapper<T> {
  constructor(private arr: T[]) {}

  toMap() {
    if (this.isArrayOfTuple(this.arr)) {
      return new Map(this.arr);
    }

    throw new Error("called toMap() on ArrayWrapper that is not an array of tuples");    
  }

  isArrayOfTuple<K, V>(arr: T[]): arr is [K, V][] {
    return arr[0] instanceof Array && arr[0].length === 2;
  }
}

但不幸的是,编译器抱怨 isArrayOfTuple 的 return 类型,说

A type predicate's type must be assignable to its parameter's type.
  Type '[K, V][]' is not assignable to type 'T[]'.
    Type '[K, V]' is not assignable to type 'T'.
      'T' could be instantiated with an arbitrary type which could be unrelated to '[K, V]'

如果对于某些 KV,如果 T 不能分配给 [K, V],您希望编译器使调用 toMap() 出错,那么在某种意义上,在这种情况下输出类型是什么并不重要。它可以是 Map<unknown, unknown>Map<never, never> 或任何东西,只要 toMap() 调用是编译器错误。我想你最终会遇到运行时错误(如果你真的关心的话,你可以 wade through the spec)所以函数不会 return...“实际”return 类型是 never 可以安全地扩展为 Map<unknown, unknown> 或任何你想要的,而不会导致类型安全问题。

无论如何,要使编译器发生错误,您可以给 toMap() 一个 this parameter which requires this be of ArrayWrapper<[any, any]> or something equivalent. You could use conditional type inference 以从 T 手动推断 KV:

toMap(this: ArrayWrapper<[any, any]>) {
    type K = T extends [infer K, any] ? K : never;
    type V = T extends [any, infer V] ? V : never;
    return new Map<K, V>(this.arr);
}

但将 toMap() 设为 generic method 并自动推断 KV 更容易:

toMap<K, V>(this: ArrayWrapper<[K, V]>) {
    return new Map<K, V>(this.arr);
}

任何一种方法都会为您提供您正在寻找的行为:

const arr1 = new ArrayWrapper(notConvertibleToMap);
const map1 = arr1.toMap(); // <-- compiler error
// --------> ~~~~
// The 'this' context of type 'ArrayWrapper<number>' is not assignable to method's 
// 'this' of type 'ArrayWrapper<[unknown, unknown]>'.

const arr2 = new ArrayWrapper(convertibleToMap);
const map2 = arr2.toMap(); // okay
// const map2: Map<number, string>

Playground link to code