1

Starting from these types:

type A = { commonKey: { a: string }[] };
type B = { commonKey: { b: number }[] };

Is it possible to obtain the following type? Without knowing about commonKey.

type C = { commonKey: { a: string, b: number }[] }

My attempt was type C = A & B, but the resulting type C isn't usable:

const c: C = // ...
c.commonKey.map(x => x.a) // `a` exists here, but not `b`

I would need a generic way to do this, independent of commonKey:

type ArrayContent = A['commonKey'][number] & B['commonKey'][number]
type C = { commonKey: ArrayContent[] };

Context

With TypeScript 4.1 template literal types and recursive conditional types, I'm trying to improve the types for our Elasticsearch queries. We have a generated type for the documents in our Elasticsearch cluster, like this:

interface Post {
  id: string;
  title: string | null;
  author: {
    name: string;
  };
  comments: {
    id: string;
    message: string;
  }[];
}

And with Elasticsearch at runtime you can limit which fields you retrieve, with the paths. There's no distinction in the syntax if the key is an array or a plain object.

const sourceFields = ['id', 'author.name', 'comments.message'];

I'm trying to create a new type, that using the document type and the source fields will build the type of what is actually retrieved. This is what I have so far:

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>>

Usage:

// Reusing the `Post` interface described above
const sourceFields = ['id', 'author.name', 'comments.message'] as const;
type ActualPost = PartialObjectFromSourceFields<Post, typeof sourceFields[number]>;

/* `ActualPost` is equivalent to:
{
  id: string;
  author: {
    name: string;
  };
  comments: {
    message: string;
  }[];
} */

It works well even if the key can be undefined or null, or for nested objects. But as soon as I want to retrieve two fields inside an array (['comments.id', 'comments.message']), I'm facing the issue described above. I can only access the first defined key. Any idea?

Samy
  • 1,651
  • 1
  • 11
  • 12
  • In fact, I submit the intersection of arrays as a feature request for TypeScript: https://github.com/microsoft/TypeScript/issues/41874 – Samy Dec 08 '20 at 11:54
  • I'm sorry, but it is not the best idea. In fact you are proposing an edge case for intersection of array elements. This is not desired behavior – captain-yossarian from Ukraine Dec 08 '20 at 11:58
  • I think I have a valid use case, and I do think the behavior is weird, to consider only the first element of the intersection. But, yeah, maybe my usage isn't really common. – Samy Dec 08 '20 at 12:12

1 Answers1

0

I hope it helps:

type Base = { commonKey: unknown[] }

// credits goes to https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type
type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

type A = { commonKey: { a: string }[] };

type B = { commonKey: { b: number }[] };

/**
 * 1) iterate through unions of array elements [A, B]
 * 2) T[number]['commonKey'][number] -> get element of commonKey array
 * 3) UnionToIntersection<T[number]['commonKey'][number]> -> convert union to UnionToIntersection
 * 4) ReadonlyArray<UnionToIntersection<T[number]['commonKey'][number]>> -> make array of intersected elements
 */
type MakeMagic<T extends ReadonlyArray<Base>> = {
  [P in keyof T[number]]: ReadonlyArray<UnionToIntersection<T[number]['commonKey'][number]>>
}

type Result = MakeMagic<[A, B]>
/**
 * 
 type Result = {
    commonKey: readonly ({
        a: string;
    } & {
        b: number;
    })[];
}
 */

You can read more about UnionToIntersection here

I assume that commonKey is always an array