Home > Software engineering >  Typescript type composition via Generics
Typescript type composition via Generics

Time:11-27

I'm struggling to keep my dynamically generated types (via generics) after a few usage in a factory. An example explains better than words: Typescript Playground

type ModularData = {};
//A module just hold data
type Module<D extends ModularData> = {
  data: D;
};

function moduleFactory<D>(data: D): Module<D> {
  return { data };
}

//Here this is perfect, each variable has clear types
const mod1 = moduleFactory({ ref: 0 });
const mod2 = moduleFactory({ otherRef: 123 });

type ComposedModule<C> = Module<C>[];
//Product is composed of multiple modules
type Product<C> = {
  modules: ComposedModule<C>;
};
function productFactory<C>(modulesList: ComposedModule<C>): Product<C> {
  return {
    modules: modulesList,
  };
}

// This should not fire an error and hover on final should list all ModularData that compose the final Product
const final = productFactory([mod1, mod2]);
// I want to be able to see autocompletion here and not fire any errors
final.modules[0].data.ref = 3;
final.modules[1].data.otherRef = 5;

Note that the data in modules can be anything. I've tried so many things, I can't find a way around.

Thanks!

CodePudding user response:

Here you have 2 problems:

  1. In this piece of code you are directly inserting the object without specifying that you have a common interface (type). Create a type for each object and then create a common type.
interface A {
  ref: number
}

interface B {
  otherRef: number
}

type AB = A | B;

//Now the error will disappear. Since you omitted the type of your array, typescript inferred the type of the first object
const final = productFactory<AB>([mod1, mod2]);
  1. When you use a shared interface, before accessing interface-specific fields you have to verify that the object belongs to that interface. To do this type of evaluation I like to insert a common field in interfaces that can be evaluated. Something like:
interface A {
  type: 'A' //Notice, enums are cleaner to do this kind of things
  ref: number
}

interface B {
  type: 'B'
  otherRef: number
}

const mod1 = moduleFactory({ type: 'A' as 'A', ref: 0 });
const mod2 = moduleFactory({ type: 'B' as 'B', otherRef: 123 });

const final = productFactory<AB>([mod1, mod2]);

//Access ref:
let refObject = final.modules[0].data;
if(refObject.type === 'A'){
 //Now you can access the object
 refObject.ref = 3;
}

CodePudding user response:

Your current type setup assumes that all modules in the list have the same data type C:

type ComposedModule<C> = Module<C>[];

The generic C could be a union of the two types, but that still would not allow you to access .ref on one and .otherRef on the other. What you really want is to know that the first item has data type {ref: number} and the second has data type {otherRef: number}.

To do this, the generic on your propertyFactory function describe the entire array as a tuple.

We want something like this:

function productFactory<List>(modulesList: List): {modules: List} {

But we also want to enforce that the List generic must be an array which contains Module objects. We do that using extends.

The last bit of annoyingness here involves readonly and as const. By default, your array [mod1, mod2] gets interpreted as an array of any length whose elements can be either {ref: number} or {otherRef: number}. But your last two lines require a more specific type. We need to know specific type based on the array index. In order to interpret the argument as a typed tuple, you need to use [mod1, mod2] as const. Using as const also means that the function must allow for readonly arrays.

type Product<List> = { // or <List extends readonly Module<any>[]>
  modules: List;
};

function productFactory<List extends readonly Module<any>[]>(modulesList: List): Product<List> {
  return {
    modules: modulesList,
  };
}

// final has type Product<readonly [Module<{ref: number;}>, Module<{otherRef: number;}>]>
const final = productFactory([mod1, mod2] as const);

// data has type {ref: number}
final.modules[0].data.ref = 3;

// data has type {otherRef: number}
final.modules[1].data.otherRef = 5;

TypeScript Playground Link

  • Related