打字稿泛型,获取数组内容的类型

Typescript generics, get type of Array contents

我已经构建了一个方法,它将一个数组与传入的另一个数组连接起来。该数组位于一个名为 state 的父对象上,该对象通常在我的方法中键入。

我的方法是这样的:

export const mutateConcat = <T, K extends keyof T>(state: T, key: K, val: T[K]) => {
  ((state[key] as unknown) as any[]) = ((state[key] as unknown) as any[]).concat(val);
}

显然这很丑陋,我想摆脱对 unknownany[] 的强制转换。我还希望能够确定数组内容的类型。

基本上,我需要确保 T[K] 是一个数组,理想情况下我想将数组内容的类型分配给另一个通用参数,比如 U

我试过 <T, K extends keyof T, U extends Array<T[K]>>,但它会将 U 分配给数组本身的类型,而不是数组内容的类型。

我问的可能吗? extends keyof 是否可以仅过滤为数组类型的键,以及这些数组内容的类型是否可以分配给另一个类型参数?

提前致谢

编辑: 因此,我设法通过关闭 val 变量来获取数组的类型。所以如下:

export const mutateConcat = <T, U, K extends keyof T>(state: T, key: K, val: T[K] & U[], arrayDistinctProperty?: keyof U) => {
  // U now contains the array type, and `arrayDistinctProperty` is typed to a property on that array type object
  ((state[key] as unknown) as any[]) = ((state[key] as unknown) as any[]).concat(val);
}

然而,这并不能解决转换的丑陋问题,因为打字稿不确定 T[K] 是数组类型。是否可以告诉它只允许我的 key 参数是 T 上的数组类型?

方法 #1

/**
 * Push into the existing array.
 */
export const mutateConcat1 = <
  TState extends Record<TKey, unknown[]>,
  TKey extends string,
  >(
    state: TState,
    key: TKey,
    newArray: TState[TKey]
  ) => {
  state[key].push(...newArray);
};

方法 #2

这里as的演员阵容可能是不可避免的。如果没有它,我们会收到,Type 'unknown[]' is not assignable to type 'TState[TKey]'. 那是因为我们告诉编译器数组的类型是 unknown[]Record。我尝试 infer 数组类型但无法使其工作。不过,考虑到我们确定 state[key]newArray 属于同一类型,转换看起来非常安全。要是我知道如何向编译器解释就好了。

/**
 * Overwrite the original array with concat.
 */
export const mutateConcat2 = <
  TState extends Record<TKey, unknown[]>,
  TKey extends string,
  >(
    state: TState,
    key: TKey,
    newArray: TState[TKey]
  ) => {
  state[key] = state[key].concat(newArray) as TState[TKey];
};

测试

/**
 * Test Cases
 */

const goodState = { array1: [1, 2, 3] };
const badState = { array1: new Date() };

// ok
mutateConcat1(goodState, 'array1', [4, 5, 6]);
mutateConcat2(goodState, 'array1', [4, 5, 6]);

// errors
mutateConcat1(goodState, 'array1', ['4', 5, 6]);
mutateConcat1(goodState, 'array2', [4, 5, 6]);
mutateConcat1(badState, 'array1', [4, 5, 6]);

mutateConcat2(goodState, 'array1', ['4', 5, 6]);
mutateConcat2(goodState, 'array2', [4, 5, 6]);
mutateConcat2(badState, 'array1', [4, 5, 6]);

Link to code.

另一个答案是正确的,但我想补充一点,如果你使 state 成为一个相对具体的 Record<K, U[]> 类型,而不是更通用的 T extends Record<K, U[]> 类型。这有助于编译器认为赋值是安全的,因为 T extends {a: string[]} 可能是 {a: Array<"lit">},然后 obj.a.push("oops") 将是一个错误:

const mutateConcat = <K extends keyof any, U>(
  state: Record<K, U[]>,
  key: K,
  val: U[]
) => {
  state[key] = state[key].concat(val); // okay
};

我看到的唯一问题是,如果您在调用 mutateConcat() 时将对象文字传递给 state 参数,您可能会在非数组属性上得到不受欢迎的 excess property checking

mutateConcat({ a: [1, 2, 3], b: 1 }, "a", [4, 5, 6]); // excess property checking 

我对 mutateConcat() 的签名所做的任何事情都会导致编译器在实现内部过于混乱,你又回到了 casting asserting。如果你仍然想走这条路,你可以通过不使用新的对象文字来避免过多的 属性 检查,如:

const obj = { a: [1, 2, 3], b: 1 };
mutateConcat(obj, "a", [4, 5, 6]); // okay

这是否满足您的用例取决于您。好的,祝你好运!

Link to code

所以,在这个和 reddit 线程之间,我相信我有满足我需求的最佳解决方案。这是我想出的:

const mutateConcat = <
  TState extends Record<TKey, any[]>,  
  TKey extends keyof TState,  
  TElement extends TState[TKey] extends Array<infer TEl> ? TEl : never  
>(state: TState, key: TKey, val: T[K], elementProperty?: keyof TElement) => { 
  state[key].push(...val);
  if (elementProperty) {
    // filter state[key] to have distinct values off of element property
  }
}

我选择在 TState 中使用 any[] 因为当我尝试 TElement[] 时还有其他问题,因为状态可以有任何类型的属性并且它没有对齐以它被调用的方式。这个方法实际上被用作 vuex 突变方法作为高阶函数,所以我不关心 state 参数的极端类型安全。 keyof 参数是重要的参数,因为它们将由开发人员传递,并且此解决方案强制要求这两个参数都对传递的对象有效。

编辑:这里是所有感兴趣的人的完整功能:

const mutateConcat = <
  TState extends Record<TKey, any[]>,
  TKey extends keyof TState,
  TElement extends TState[TKey] extends Array<infer TEl> ? TEl : never
>(
  key: TKey,
  sessionKey?: string,
  distinctProperty?: keyof TElement,
) => (state: TState, val: TElement[]) => {
  state[key].push(...val);

  if (distinctProperty) {
    removeDuplicates(state[key], distinctProperty);
  }

  if (sessionKey) {
    setSessionItem(sessionKey, state);
  }
};