Repository navigation
Intersection of arrays #41874
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 availableExamples
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
Activity
jcalz commentedon Dec 8, 2020
Related to #39693
RyanCavanaugh commentedon Dec 10, 2020
Array intersection is weird since there are many invariants of arrays and many invariants of intersections that can't be simultaneously met. For example, if it's valid to call
a.push(x)whenaisA, then it should be valid to writeab.push(x)whenabisA & B, but that creates an unsound read on(A & B)[number].In higher-order the current behavior is really the best we can do; in zero-order it's really preferable to just write
Array<A & B>,Array<A> | Array<B>, orArray<A | B>depending on which you mean to happen.TRCYX commentedon Mar 28, 2021
This seems to also affect readonly arrays, though they do not allow mutations like push.
kasamachenkaow commentedon Apr 29, 2021
Ryan Cavanaugh (@RyanCavanaugh) Does the
Design Limitationmeans it's not gonna be fixed?RyanCavanaugh commentedon Apr 29, 2021
Basically, yes.
kasamachenkaow commentedon Apr 30, 2021
Ryan Cavanaugh (@RyanCavanaugh) I see, thanks for clarification!
although I have 2 more questions:
bugtag attached to this issue(since if you access thru
arr[0]you get bothaandbbut get onlyain the map function?)RyanCavanaugh commentedon Apr 30, 2021
mapfunction doesn't get reduced to a single overloadkasamachenkaow commentedon Apr 30, 2021
Ryan Cavanaugh (@RyanCavanaugh) Thanks for the answers
kasamachenkaow commentedon May 1, 2021
I found the workaround way to solve an issue
I created
ArrayIntersect<T1, T2>like thishope it helps someone who needs the same use case like mine
gregoirechauvet commentedon May 2, 2021
kasamachenkaow awesome! It's exactly what I was looking for 🥳
lindapaiste commentedon Jan 19, 2023
I found this issue while searching for a problem that I am having with refining the type of an array. Your examples showed me that the order matters, which is wild. I never would have thought that the order of an intersection type would make a difference! I was able to fix my particular error by swapping the order of
BroadType & { values: string[] }to{ values: string[] } & BroadType.jcalz commentedon Jan 10, 2025
cross linking #50346 and #53355