"Arbitrary" generic types

Viewed 163

Is there any TypeScript solution, that would help me have an "infinite" amount of generic types, without having to type all of them, of course?

Example bellow:

type Args<T extends React.ComponentType<any>> = { component: T, presetProps: React.ComponentProps<T>};

function buildComponents<T extends React.ComponentType<any>>(...args: Args<T>[] ) : any

function componentJ({name, age}: {name: string, age: number}){
  return <h1>123</h1>
}

function componentI({ age}: { age: number}){
  return <h1>123</h1>
}

buildComponents({component:componentJ, presetProps:{name: "Jon", age: 12}}, 
                {component: componentI, presetProps:{age: 44}})

With this code, I got an error on {component: componentI, presetProps:{age: 44}}, because it misses the name, but I wanted this to get the type from the componentI and not from the the componentJ, which was the case.

1 Answers

You might want to make your buildComponents function generic not in the union of types to pass to Args, but in the tuple of such types. The compiler will not want to infer a union from a heterogeneous list (where the elements may be of different types), since that is often an error:

function foo<T>(...args: T[]) { }
foo(1, 2, "3", 4); // error!
// -----> ~~~ string is not a number

On the other hand, tuple types are meant to support heterogeneous lists:

function bar<T extends any[]>(...args: T) {}
bar(1, 2, "3", 4); // okay

So let's try that with buildComponents():

declare function buildComponents<T extends React.ComponentType<any>[]>(
  ...args: { [I in keyof T]: Args<Extract<T[I], React.ComponentType<any>>> }
): any;

What I've done there is to map the tuple type T to a new tuple, so that each element T[I] becomes Args<T[I]>. There's a fiddly bit with the Extract utility type to work around a compiler bug (see microsoft/TypeScript#27995). But it works:

buildComponents(
  { component: componentJ, presetProps: { name: "Jon", age: 12 } },
  { component: componentI, presetProps: { age: 44 } }
); // okay

Another way to do this is to make T the tuple of the props type and not the component type. It is a bit cleaner:

declare function buildComponents<P extends any[]>(
  ...args: { [I in keyof P]: { component: React.ComponentType<P[I]>, presetProps: P[I] } }
): any;

and works at the call site also:

buildComponents(
  { component: componentJ, presetProps: { name: "Jon", age: 12 } },
  { component: componentI, presetProps: { age: 44 } }
); // okay

Playground link to code

Related