尽管属性看起来是数组类型,但无法在 TypeScript 中将对象属性初始化为空数组

问题描述 投票:0回答:1

我正在尝试在 TypeScript 中编写一种方法来展平和选取分页输出的元素。一个示范性的例子是:

const paginatedOutput: { results: number[], page: number, meta: string[] }[] = [{ page: 1, results: [1, 2, 3], meta: ['a', 'b', 'c'] }, { page: 2, results: [3, 4, 5], meta: ['d', 'e', 'f'] }];
 
flattenAndPick(paginatedOutput, ['results', 'meta']);
 
// Output:
//
// {
//    results: [1, 2, 3, 4, 5, 6],
//    meta: ['a', 'b', 'c', 'd', 'e', 'f']
// }

因此方法

flattenAndPick
连接数组内对象的数组类型元素。

到目前为止我想出的解决这个问题的通用类型和方法是:

/**
 * Generic type which takes an object `T` and a type `K` and returns a type
 * which is an object with only those properties of `T` which are of type `K`.
 *
 *
 * Example usage:
 * ```
 * type A = { a: string, b: number };
 *
 * type B = PickByType<A, number>
 *
 * const b: B = { a: '1', b: 2 } // Error
 *
 * const b2 = { b: 2 } // Allowed
 * ```
 */
type PickByType<Obj, Type> = {
  [key in keyof Obj as Obj[key] extends Type | undefined ? key : never]: Obj[key];
};

此泛型类型将用于从本身就是数组的底层数组对象中提取这些属性,因为它们表示分页的结果,稍后将其连接在一起。

进行拾取和连接的方法是:

/**
 * Flatten arrays of repeated objects which themselves contain arrays, and pick and concatenate
 * data in those arrays.
 *
 * Use case: Picking data of interest from paginated outputs.
 *
 * Example usage:
 *
 * ```
 * const paginatedOutput = [{ page: 1, results: [1, 2, 3], meta: ['a', 'b', 'c'] }, { page: 2, results: [3, 4, 5], meta: ['d', 'e', 'f'] }];
 *
 * flattenAndPick(paginatedOutput, ['results', 'meta']);
 *
 * // Output:
 * //
 * // {
 * //    results: [1, 2, 3, 4, 5, 6],
 * //    meta: ['a', 'b', 'c', 'd', 'e', 'f']
 * // }
 * ```
 */
const flattenAndPick = <T extends object, K extends keyof PickByType<T, any[]>>(
  array: T[],
  properties: K[],
): Partial<PickByType<T, any[]>> => {

  // Initialise an object which is just the underlying array object with only its array-type
  // properties
  const pickedObject: Partial<PickByType<T, any[]>> = {};

  // Iterate over selected properties and initialise empty arrays for these properties
  properties.forEach((property) => {
    pickedObject[property] = [] as T[K]; // <-- Note the assertion here - want to remove this
  });

  
  array.forEach((element) => {
    properties.forEach((property) => {
      pickedObject[property] = (pickedObject[property] as any[]).concat(element[property]) as T[K]; // <-- and these, although I suspect its the same issue as above
    });
  });

  return pickedObject;
};

我正在努力删除上述方法中的类型断言。没有第一个断言的错误是

Type 'undefined[]' is not assignable to type 'T[K]'

PickByType
似乎按预期工作:

type A = { a: string; b: number; c?: number; d: string[] };

type B = PickByType<A, any[]>;

type C = keyof B;

type D = { [key in C]: B[key] };

type E = C[];


const d: D = { a: '1' }; // Error

const d2: D = { b: 2, a: '1' }; // Error

const d3: D = {}; // Error

const d4: D = { b: 2: 'd': ['a'] }; // Error

const d5: D = { d: ['a'] }; // Fine


const e: E = ['d']; // Fine

const e1: E = ['a']; // Error

那么为什么我不能在

pickedObject[property] = []
中初始化
flattenAndPick
呢?在我看来,
T[K]
应该延伸
any[]
。任何对基本问题有指导意义的文档链接也将不胜感激。

typescript typescript-typings typescript-generics
1个回答
0
投票

TypeScript 抱怨的原因是,虽然已知

T[K]
可以分配给 any[]
,但您不一定可以将 
[]
 类型的值分配给 
T[K]
。那里的分配方向是向后的。也许
T[K]
是一个固定长度的
元组类型

const entries = Object.entries({ a: 1, b: 2, c: 3 }).map(x => ({ x })); const flattened = flattenAndPick(entries, ["x"]); const len = flattened.x?.length // ^? const len: 2 | undefined console.log(len) // 6, not 2
这里 

entries

 是一个对象数组,每个对象都有一个 
x
 属性,它是一个 
pair (长度为 2 的元组)。因此 TypeScript 认为 flattened
 有一个 
x
 属性,如果定义的话,它也是一对。但当然不是。

或者也许

T[K]

 具有普通数组所没有的附加属性:

const v = flattenAndPick( [{ a: Object.assign([1, 2, 3], { prop: "hello" }) }], ["a"] ).a;
根据您的打字,

v

的类型应该是
(number[] & { prop: string }) | undefined
。  您已经告诉 TypeScript,如果定义了 
a
 结果的 
flattenAndPick()
 属性,则将具有类型为 
prop
string
 属性。所以下面的代码编译没有问题:

const w = v?.prop.toUpperCase(); // ^? const w: string | undefined
但是当然会出现运行时错误,因为 

v.prop

 不存在。

在这两种情况下,你都在某个地方分配了

[]

,TypeScript 无法确定它是否适用,而 TypeScript 对此的抱怨是正确的。这在实践中可能不太可能发生,但事实就是如此。如果您只想不管它而不担心它,那么您应该像您一直在做的那样使用
类型断言


另一方面,如果你想要 TypeScript 认为更准确的类型,你可以这样做:

const flattenAndPick = <T extends object, K extends keyof T>( array: ({ [k: string]: any } & { [P in K]: T[P][] })[], properties: K[], ): { [P in K]?: T[P][] } => { const pickedObject: { [P in K]?: T[P][] } = {}; properties.forEach((property) => { pickedObject[property] = []; }); array.forEach((element) => { properties.forEach((property) => { pickedObject[property] = ( pickedObject[property]!).concat(element[property]); }); }); return pickedObject; }; const ret = flattenAndPick([{ a: "", b: [1, 2, 3] }, { b: [4] }], ["b"]);
这里 

generic T

 类型对应于 
array 属性内数组的 element types
。  因此,如果 
array
[{ a: "", b: [1, 2, 3] }, { b: [4] }]
,那么 
T
 应该类似于 
{b: number}

那么

array

T上的映射类型
,这样
T
的每个属性都会被做成
array
中的数组。另外, 
array
 的类型与 
{[k: string]: any}intersected
 ,这是一种特殊大小写的类型(请参阅 microsoft/TypeScript#41746
中的此评论)以匹配任何内容。我这样做是为了我们不会触发过多的属性检查。我们希望 a 中的
[{ a: "", b: [1, 2, 3] }, { b: [4] }]
被简单地
忽略
,而不是标记为警告。 无论如何,现在几乎没有什么断言了。我们知道

[]

T[P][]
T[K][]
类型的有效值,因为该类型的“数组”部分不是通用的,并且不能神奇地变得比数组更窄。 只有
element
类型是通用的,并且由于我们只是复制它们而不是尝试合成它们,所以一切都很好。 仍然有一个断言,即

!

上的 非空断言 (

pickedObject[property]
)。这是因为 TypeScript 无法遵循这样的逻辑:在第一次
forEach()
调用之后,所有数组都将被初始化。如果你想避免这种情况,你可以这样做:
const flattenAndPick = <T extends object, K extends keyof T>(
    array: ({ [k: string]: any } & { [P in K]: T[P][] })[],
    properties: K[],
): { [P in K]?: T[P][] } => {
    const pickedObject: { [P in K]?: T[P][] } = {};
    array.forEach((element) => {
        properties.forEach((property) => {
            pickedObject[property] = (
                pickedObject[property] ?? []
            ).concat(element[property]);
        });
    });
    return pickedObject;
};

以便初始化与串联发生在同一位置。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.