2

Given an array of items that implemented a specific class with a generic type, I would like to create combined type from the array items - For each item, take its generic (defined as first generic argument for example) and create a type that implements all the the generic values of the items.

For Example:

// Defining 2 types with different set of methods
type ObjectWithMethods1 = {
  call1(a: string): void;
}

type ObjectWithMethods2 = {
  call2(): number;
}

class BaseItem<T> {
}

// Defining 2 classes that both extends the same class/abstract class,
// each passing a different generic value.
class Item1 implements BaseItem<ObjectWithMethods1> {
}
class Item2 implements BaseItem<ObjectWithMethods2> {
}

// An array that contains instances of the same base class, however each of its items
// used a different type in the BaseItem generics  
const arr: BaseItem<any>[] = [new Item1(), new Item2()]

// How to define the typing of a "getCombinedObject" method that its return type will be an object 
// that implements both ObjectWithMethods1 and ObjectWithMethods2,
// e.g. ObjectWithMethods1 & ObjectWithMethods2
const combined = getCombinedObject(arr);
combined.call1('test'); //void
combined.call2(); //number

I've tried achieving it in few different ways, can fetch the generic values of the array but failed to achieve the aggregated values of the array.

Its something conceptually similar to this (without the additional depth created due to the of the iteration of the array items):

type CombinedObject<TArray extends TItem[], TItem extends Record<string, any>> = {
  [key: string]: {
    [Index in keyof TArray]: TArray[Index] extends { [key]: any } ? TArray[Index][key] : never;
  };
};

Thanks alot!

Aviran Cohen
  • 5,581
  • 4
  • 48
  • 75

1 Answers1

2

Description is in comments

// Defining 2 types with different set of methods
type ObjectWithMethods1 = {
    call1: (a: string) => void;
}

type ObjectWithMethods2 = {
    call2: () => number;
}

class BaseItem<T> {
    /**
     * We need to make this class unique
     */
    tag = 'base'
}

/**
 * It is important to extend and implement
 */
class Item1 extends BaseItem<ObjectWithMethods1> implements ObjectWithMethods1 {
    call1 = (a: string) => 'str'
}

/**
 * It is important to extend and implement
 */
class Item2 extends BaseItem<ObjectWithMethods2> implements ObjectWithMethods2 {
    call2 = () => 42;
}

class Item3 { }

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;


/**
 * Elem extends BaseItem<any> - makes sure that each instance extends BaseItem
 * UnionToIntersection - will merge all instances into one
 */
function getCombinedObject<Elem extends BaseItem<any>, Arr extends Elem[]>(arr: [...Arr]): UnionToIntersection<[...Arr][number]>
function getCombinedObject<Elem extends BaseItem<any>, Arr extends Elem[]>(arr: [...Arr]) {
    return arr.reduce((acc, elem) => ({ ...acc, ...elem }), {})
}

const combined = getCombinedObject([new Item1(), new Item2()]);
combined.call1('test'); // string
combined.call2(); // number

Playground

P.S. Avoid declaring bivariant methods:

type ObjectWithMethods1 = {
  call1(a: string): void;
}

This is unsafe. More information you can find here

I just talk about the need of the types for the development time

Consider this example:

class BaseItem<T>{ }

interface ObjectWithMethods1 {
    call1(a: string): void;
}
class Item1 implements BaseItem<ObjectWithMethods1> { }

Since T generic type is unused in BaseItem class implementation, it is completely ignored in class Item1.

TS is unable to distinguish BaseItem<ObjectWithMethods1> and BaseItem<ObjectWithMethods2>.

See example:

declare var x: BaseItem<ObjectWithMethods1>
declare var y: BaseItem<ObjectWithMethods2>
x = y // ok
y = x // ok

Hence, these types from TS perspective are equal.

  • First of all thank you. The solution you provided is using the actual implementation of the item instead of its generics, So I modified your solution to be based on the generic argument, And there is a TS Error: "Object is of type 'unknown'." https://justpaste.it/8qg12 (the link generated was too long for SO comment) The funny part is that I am using WebStorm and it provides the auto-complete correctly, but I don't know how can I fix the TS error. – Aviran Cohen Jul 20 '21 at 11:54
  • Its worth mentioning I don't care about the actual method implementation, but just on the typings that will allow to infer such an object returned from a method based on the array it receives, so type-wise the methods will be available on the returned object. – Aviran Cohen Jul 20 '21 at 11:59
  • This is because you removed method implementation. See in TS playground compiled code without actual method implementation. You will get empty objects. This is why you are getting `unknown`. TS knows that there are no method implementations – captain-yossarian from Ukraine Jul 20 '21 at 12:10
  • Just to clarify again - The missing part is that I would like the types to be inferred based on the Generic types they receive and not on the Item1/Item2 own implementation in the example you provided. Do you know if it can be achieved? I've made a concrete implementation of both ObjectWithMethods1 and ObjectWithMethods2 types (I've created a classes) and in getCombinedObject I wrote "return {} as any;" as I don't want to focus on the value itself that is being returned and same errors persist. Here is the code: https://justpaste.it/8ytwi – Aviran Cohen Jul 20 '21 at 12:49
  • @AviranCohen it is impossible. Generics are erased in compile time. In this case you need to extends Item1 directly from ObjectWithMethods1 – captain-yossarian from Ukraine Jul 20 '21 at 13:37
  • Yes I know they are erased at compile time, but I just talk about the need of the types for the development time, for example to allow auto complete infered from the other generic types as I described - e.g. just defining the type, but the actual compiled JS code don't really care what was the original method typescript-ish signature and generics defined with TS, all of that is not relevant after the compile in that matter as far as I know. I just want to infer return type from given array argument types (instead of 1 type which will ofc works). It is not possible? What am I missing? :) – Aviran Cohen Jul 20 '21 at 14:17
  • If it was a single argument with generics type the TS would work. So here in my case, the only difference I need to create type based on inferring array of types which all implements the same Base (just for development-type intelisense), so I wonder if this is where it exceeds TS capabilities or just a small change might make it work. – Aviran Cohen Jul 20 '21 at 14:25