Description
Search Terms
intersection array
Suggestion
Intersections of arrays are weird in my opinion. In TypeScript 4.1, only the first part of the intersection is considered. I think TypeScript should merge the array types, it would be especially convenient for objects:
type A = { a: string }[];
type B = { b: number }[];
type C = A & B;
// Behaves like
C = { a: string }[];
// Desired
C = { a: string, b: number }[];
Playground to try it out.
Use Cases
I want to be able to cherry-pick some paths from an interface. I have an Elasticsearch cluster and a TypeScript interface for the shape of the documents in the cluster.
With Elasticsearch, I'm able to retrieve only some values with their paths.
// With some documents of the following shape:
interface Post {
id: string;
views: number;
comments: {
id: string;
content: string;
author: {
id: string;
}
}[];
}
// And a list of source fields
const sourceFields = ['id', 'comments.id'];
// When we do the query with these source fields, at runtime we only have a subset of the interface, like this:
{ id: '1', messages: [{ id: '1' }] };
Using features introduced in TypeScript 4.1 I'm able to generate a type based on the interface and the source fields. And it works almost perfectly except for arrays. While it works fine with nested objects, and nullable and optional keys.
Usage:
// Source fields are known at build time
const sourceFields = ['id', 'comments.id'] as const;
type ActualPost = PartialObjectFromSourceFields<Post, typeof sourceFields[number]>;
/* ActualPost behaves like
{
id: string;
comments: {
id: string;
}[];
}
*/
PartialObjectFromSourceFields
declaration:
type ExtractPath<Obj, Path extends string> =
Obj extends undefined ? ExtractPath<NonNullable<Obj>, Path> | undefined :
Obj extends null ? ExtractPath<NonNullable<Obj>, Path> | null :
Obj extends any[] ? ExtractPath<Obj[number], Path>[] :
Path extends `${infer FirstKey}.${infer OtherPath}`
? (FirstKey extends keyof Obj
? { [k in FirstKey]: ExtractPath<Obj[FirstKey], OtherPath> }
: never)
: Path extends keyof Obj
? { [K in Path]: Obj[Path] }
: never;
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type Distribute<Obj, Fields> = Fields extends string ? ExtractPath<Obj, Fields> : never;
export type PartialObjectFromSourceFields<Obj, Fields> = UnionToIntersection<Distribute<Obj, Fields>>
But because of the limitation on array intersection, as soon as 2 paths on an array key are used, only the first one is considered:
const sourceFields = ['id', 'comments.id', 'comments.content'] as const;
type ActualPost = PartialObjectFromSourceFields<Post, typeof sourceFields[number]>;
// ActualPost is in fact:
{ id: string } & { comments: { id: string }[] } & { comments: { content: string }[] };
// And `content` cannot be accessed inside `comments`, only `id` is available
Examples
I think the same merging rules should apply as without arrays:
type A = string[] & number[];
// Behaves like
A = string[]
// Desired?
A = never[];
Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
I don't know if it would be a breaking change 🤔 I wonder if it might conflict with this behavior #38348