Kiwi
Kiwi

Reputation: 1093

How to preserve generic type when passing parameters forward?

It seems as though typeof a function or class with generic parameters replaces all of the generics with unknown. Is there a way to preserve that information?

For example, the variables exampleNumber and exampleString below will be of type Example<unknown>, but my goal is for them to be Example<number> and Example<string> respectively.

class Example<T> {
    constructor(public value: T) {}
}

function newExample(...args: ConstructorParameters<typeof Example>) {
    return new Example(...args);
}

const exampleNumber = newExample(3);
const exampleString = newExample('foo');

Upvotes: 0

Views: 1094

Answers (1)

jcalz
jcalz

Reputation: 328598

TypeScript's type system does not have higher kinded types of the sort needed to directly represent the operation you're trying to do. Before TypeScript 3.4/3.5 I'd say that the only way for you to proceed is to manually write out the function typing you want:

function newExampleManual<T>(...args: [T]) {
  return new Example(...args);
}

TypeScript 3.4 introduced some support for higher order type inference from generic functions and TypeScript 3.5 introduced the analogous support for generic constructors, which gives you a way to get the function type you want... with the proviso that this doesn't happen purely at the type level: instead you need to represent the transformation as the effect of a generic function that accepts other (possibly generic) functions or constructors as callbacks.

Specifically:

const ctorToFn =
  <A extends any[], R>(ctor: new (...args: A) => R) => (...args: A) => new ctor(...args);
        
const newExample = ctorToFn(Example);
// const newExample: <T>(value: T) => Example<T>

You can see that ctorToFn() turns any constructor into a function in the obvious way: use the passed arguments to invoke the construct signature of the constructor. Then ctorToFn(Example) produces the newExample implementation you want, more or less... and automagically, it also gives it the type signature you want: namely the generic function type <T>(value: T) => Example<T>.

Then this works as desired:

const exampleNumber = newExample(3);
// const exampleNumber: Example<number>
const exampleString = newExample('foo');
// const exampleString: Example<string>

Playground link to code

Upvotes: 3

Related Questions