Home > OS >  Typescript - Create custom type from common properties of an Array of object
Typescript - Create custom type from common properties of an Array of object

Time:12-10

I have an array of objects, which have a common key "foo" and I am trying to create a custom type out of this property. I am not looking for its value that I can get from a reduce but more its Type. Here is a link to the TypeScript playground with a reproduced example and the expected output Type I need to retrieve:

class Object1 {
  static foo = { reducer_one: { a: 1 } }
  object_1_method() { }
}

class Object2 {
  static foo = { reducer_two: { b: 2 } }
  object_2_method() { }
}

class Object3 {
  static foo = { reducer_three: { c: 3 } }
  object_3_method() { }
}

const myArray = [Object1, Object2, Object3 ];

const reducers = myArray.reduce((acc, cur) => {
    return {
        ...acc,
        ...cur.foo
    }
}, {});
const myArray = [Object1, Object2, Object3 ];

I am trying to get a reduced object type gathering all the different foo properties of all the initial objects in the array.

const finalObject = {
   reducer_one: {...},
   reducer_two: {...},
   reducer_three: {...}
}

I tried to reduce the array to an object with the objects' respective foo property in order to retrieve its type but the output type of the reduce remains an empty object.

Thanks in advance for your help. Hope the description of the issue is clear enough

CodePudding user response:

const finalObject = myArray.reduce((obj, clazz) => {
   Object.keys(clazz.foo).forEach(prop => {
      obj[prop] = clazz.foo[prop];
   });
   return obj;
}, {});

CodePudding user response:

You will pretty much need to use a type assertion to tell the compiler what type reducers is going to be. Let's call this type ReducedType with the understanding that we will have to define it at some point.


If you use Array.reduce() or any other method to walk through an array and add properties gradually, the compiler can't really follow what's happening. See How can i move away from a Partial<T> to T without casting in Typescript for a similar issue.

You could try to use the Object.assign() function to do this all at once:

const maybeReducers =
  Object.assign({}, ...myArray.map(cur => cur.foo)) // any

but the TypeScript library typings for Object.assign() operating on a variadic number of inputs just returns a value of the any type, which doesn't help you either.

Let's stick with reduce():

const reducers = myArray.reduce((acc, cur) => {
  return {
    ...acc,
    ...cur.foo
  }
}, {} as ReducedType);

So we're asserting that the initial {} value passed in is ReducedType. This assertion is sort of a lie that eventually becomes true.


So how do we compute ReducedType from the type of myArray? Let's look at myArray with [the TypeScript typeof operator]:

type MyArray = typeof myArray
// type MyArray = (typeof Object1 | typeof Object2 | typeof Object3)[]

First we want to get the type of the elements of that array. Since you get elements from an array by indexing into them with a numeric index, we can take the MyArray type and index into it with the number type:

type MyArrayElement = MyArray[number]
// type MyArrayElement = typeof Object1 | typeof Object2 | typeof Object3

That's a union of the three class constructor types. From there we want to get the type of each union member's foo property. We can do that with another indexed access (which distributes across unions):

type MyArrayElementFooProp = MyArrayElement["foo"]
/* type MyArrayElementFooProp = {
    reducer_three: {
        c: number;
    };
} | {
    reducer_one: {
        a: number;
    };
} | {
    reducer_two: {
        b: number;
    };
} */

So that is looking closer to what you want... except it's a union and you really want the intersection of them, since your ReducedType should have all of those properties.

(And no, I don't know why the compiler chooses to order the union with three at the front. It doesn't affect type safety at any rate.)

There is a way to transform a union into an intersection using TypeScript's distributive conditional types and conditional type inference, shown here without additional explanation (see the other q/a pair)

type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

type IntersectionOfFoos = UnionToIntersection<MyArrayElementFooProp>
/* type IntersectionOfFoos = {
    reducer_three: {
        c: number;
    };
} & {
    reducer_one: {
        a: number;
    };
} & {
    reducer_two: {
        b: number;
    };
} */

This is exactly the type we want, although it isn't represented too pleasantly. A type like {x: 0} & {y: 1} is equivalent to but less pleasing than {x: 0; y: 1}. We can use a simple mapped type to convert from the former form to the latter:

type ReducedType =
  { [K in keyof IntersectionOfFoos]: IntersectionOfFoos[K] }
/* type ReducedType = {
    reducer_three: {
        c: number;
    };
    reducer_one: {
        a: number;
    };
    reducer_two: {
        b: number;
    };
} */

And we can stop there if you want. It is of course possible to put this together in a single type alias, although it's less obvious what we're doing:

type ReducedType =
  typeof myArray[number]["foo"] extends infer F ?
  (F extends unknown ? (x: F) => void : never) extends
  (x: infer I) => void ?
  { [K in keyof I]: I[K] }
  : never : never

/* type ReducedType = {
    reducer_three: {
        c: number;
    };
    reducer_one: {
        a: number;
    };
    reducer_two: {
        b: number;
    };
} */

So there you go, now the compiler knows the type of reducers strongly enough to let you index into it:

console.log(reducers.reducer_one.a.toFixed(2)); // 1.00
console.log(reducers.reducer_two.b.toFixed(3)); // 2.000
console.log(reducers.reducer_three.c.toFixed(4)); // 3.0000

Playground link to code

  • Related