2

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.

3
  • What do your attempts look like? What is AnimalFactory? A type you already have? Or something you tried to use to make this work? Commented Mar 28, 2022 at 17:21
  • Why do you want to use generics for this? It doesn't immediately seem necessary. Commented Mar 28, 2022 at 17:26
  • 1
    thanks for your feedback @T.J.Crowder . I will edit my question. Commented Mar 28, 2022 at 20:46

1 Answer 1

2

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

Sign up to request clarification or add additional context in comments.

3 Comments

I was so close when I had to pack it in for the night. Just as well that I did, though, because I don't think I'd've got the factories: readonly [...T] part. Very cool!
@T.J.Crowder Thank you! (and also thanks for creating the animals, I gave them a second life on my playground :-)
Thanks for the nice explanation :) Still, I am surprised on how complex it has to be ! Mixing all the as const, factories: readonly [...T], and {[K in keyof T]: T[K] extends AnimalFactory<infer A> ? A : never}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.