我正在尝试在 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 抱怨的原因是,虽然已知
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 对此的抱怨是正确的。这在实践中可能不太可能发生,但事实就是如此。如果您只想不管它而不担心它,那么您应该像您一直在做的那样使用类型断言。
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"]);
这里 类型对应于
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 代码链接