Home > Enterprise >  Mapping typescript types
Mapping typescript types

Time:03-30

I am trying add types to a function, that takes as parameters an array of typed objects, and return a mapped array of another type :

const createAnimals = <T extends AnimalFactory<any>[]>(factories: T) => {
  return factories.map((f) => f());
};

Once this function is properly typed, it should throw an error with the following code :

const factories = [() => new Dog(), () => new Cat()];
const animals = createAnimals(factories);

// ❌ should fail !
animals[0].meow();

I would like the compiler to know that animals is of type [Dog, Cat] .

I have been trying to use infer, but without success so far.

CodePudding user response:

It's possible to do this, but the solution will require you to type factories as read-only, or the type will simply not contain enough information.

const factories = [() => new Dog(), () => new Cat()] as const;

If you declare a type for such factories (which aren't really about animals anymore, but let's stick with it)

type AnimalFactory<T> = () => T

you can create a mapped type that takes a tuple of AnimalFactory's and yields a tuple of the respective animal types:

type FactoryAnimals<T extends AnimalFactory<unknown>[]> =
  {[K in keyof T]: T[K] extends AnimalFactory<infer A> ? A : never}

Now you can use AnimalFactory for the return type of createAnimals:

const createAnimals = <T extends AnimalFactory<unknown>[]>
  (factories: readonly [...T]) => factories.map(f => f()) as FactoryAnimals<T>;

Unfortunately the as FactoryAnimals<T> assertion is necessary since TypeScript can't infer that doing a map on the tuple yields the mapped type. The readonly [...T] bit is to avoid having a read-only T, which would cause a read-only return type and require an extra assertion.

If you call createAnimals on the factories it produces a tuple of the desired type:

const animals = createAnimals(factories);
// type: [Dog, Cat]

// ❌ should fail !
animals[0].meow(); // Error: Property 'meow' does not exist on type 'Dog'.

And if you call it with an explicit array instead of a variable, you can leave out the as const, since the [...T] type causes factories to be considered a tuple:

const animals = createAnimals([() => new Dog(), () => new Cat()]);
// type: [Dog, Cat]

TypeScript playground

  • Related