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

Posted

技术标签:

【中文标题】打字稿泛型,获取数组内容的类型【英文标题】:Typescript generics, get type of Array contents 【发布时间】:2019-12-21 11:30:51 【问题描述】:

我已经构建了一个方法,它将一个数组与传入的另一个数组连接起来。该数组位于一个名为 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

我尝试过&lt;T, K extends keyof T, U extends Array&lt;T[K]&gt;&gt;,但它会将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);

但是,这并不能解决强制转换的丑陋问题,因为 typescript 不能确定 T[K] 是一个数组类型。是否可以告诉它只允许我的key 参数成为T 上的数组类型?

【问题讨论】:

【参考方案1】:

方法#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[]' 不能分配给 type 'TState[TKey]'。 这是因为我们告诉编译器该数组是 @987654325 中的 unknown[] 类型@。我尝试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.

【讨论】:

【参考方案2】:

所以,在这个和 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 变异方法,所以我不关心状态参数的极端类型安全。 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);
  
;

【讨论】:

【参考方案3】:

另一个答案是正确的,但我想补充一点,如果您将state 设为相对具体的Record&lt;K, U[]&gt; 类型,而不是更通用的T extends Record&lt;K, U[]&gt; 类型,则可以使其工作。这有助于编译器将赋值视为安全,因为T extends a: string[] 可能是a: Array&lt;"lit"&gt;,然后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 ?

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

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

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

Link to code

【讨论】:

keyof any 对我来说是新的。整洁的。在这种情况下简单地做K extends string有什么好处? 这里的一个限制是它没有通过这个测试:const obj = a: [1, 2, 3] ; mutateConcat(obj, "a", [ 1, 'b', 'c' ]);,我认为这应该是一个错误,因为我假设数组的目标类型来自状态而不是来自新的价值观。

以上是关于打字稿泛型,获取数组内容的类型的主要内容,如果未能解决你的问题,请参考以下文章

打字稿泛型类型 T toString

打字稿泛型:从函数参数的类型推断类型?

打字稿泛型-“扩展对象”毫无意义吗?最佳做法是啥?

打字稿泛型类参数

带有约束的打字稿泛型不能分配给泛型接口

typescript 打字稿泛型示例