Skip to content

Intersection of arrays #41874

Closed
Closed
@gregoirechauvet

Description

@gregoirechauvet

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions