【问题标题】: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 上的数组类型?

【问题讨论】:

    标签: arrays typescript generics typescript-generics


    【解决方案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' ]);,我认为这应该是一个错误,因为我假设数组的目标类型来自状态而不是来自新的价值观。
        猜你喜欢
        • 2020-05-09
        • 1970-01-01
        • 2021-11-26
        • 1970-01-01
        • 2020-02-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多