Skip to content

Intersection of arrays #41874

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
4 of 5 tasks
gregoirechauvet opened this issue Dec 8, 2020 · 12 comments
Closed
4 of 5 tasks

Intersection of arrays #41874

gregoirechauvet opened this issue Dec 8, 2020 · 12 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@gregoirechauvet
Copy link

gregoirechauvet commented Dec 8, 2020

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

Playground demo

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

@jcalz
Copy link
Contributor

jcalz commented Dec 8, 2020

Related to #39693

@RyanCavanaugh
Copy link
Member

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) when a is A, then it should be valid to write ab.push(x) when ab is A & 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>, or Array<A | B> depending on which you mean to happen.

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Dec 10, 2020
@TRCYX
Copy link
Contributor

TRCYX commented Mar 28, 2021

This seems to also affect readonly arrays, though they do not allow mutations like push.

@kasamachenkaow
Copy link

@RyanCavanaugh Does the Design Limitation means it's not gonna be fixed?

@RyanCavanaugh
Copy link
Member

Does the Design Limitation means it's not gonna be fixed?

Basically, yes.

@kasamachenkaow
Copy link

@RyanCavanaugh I see, thanks for clarification!

although I have 2 more questions:

  1. Why the issue is still open though?
  2. Why there is no bug tag attached to this issue
    (since if you access thru arr[0] you get both a and b but get only a in the map function?)

@RyanCavanaugh
Copy link
Member

  1. It hasn't been automatically closed yet
  2. Because it's a design limitation that the map function doesn't get reduced to a single overload

@kasamachenkaow
Copy link

@RyanCavanaugh Thanks for the answers

@kasamachenkaow
Copy link

kasamachenkaow commented May 1, 2021

I found the workaround way to solve an issue
I created ArrayIntersect<T1, T2> like this

hope it helps someone who needs the same use case like mine

@gregoirechauvet
Copy link
Author

@kasamachenkaow awesome! It's exactly what I was looking for 🥳

@lindapaiste
Copy link

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.

interface BroadType {
    values: (string | boolean)[];
    // Has a bunch of other properties in the actual use case.
}

const badFunction = (input: BroadType & { values: string[] }) => {
    // Error because `term` has type `string | boolean`.
    return input.values.map(term => term.toLowerCase())
}

const goodFunction = (input: { values: string[] } & BroadType) => {
    // This is fine, `term` has type `string`.
    return input.values.map(term => term.toLowerCase());
}

@jcalz
Copy link
Contributor

jcalz commented Jan 10, 2025

cross linking #50346 and #53355

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

6 participants