Created my own utility type to solve this in TS version 4.3.5. I use it in place of the regular ConstructorParameters
when dealing with generics, and it seems pretty scalable, though be sure to read the explanation section if you want to be sure. Here it is as a file with its helpers, along with a couple of example playgrounds:
GenericConstructorParameters.ts
// helpers
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
type IsUnknown<T> = unknown extends T ? IfAny<T, never, true> : never;
type UnknownReplacer<T, K> = K extends [infer WithThis, ...infer WithRest] ? T extends [infer ReplaceThis, ...infer ReplaceRest] ? IsUnknown<ReplaceThis> extends never ? [ReplaceThis, ...UnknownReplacer<ReplaceRest, K>] : [WithThis, ...UnknownReplacer<ReplaceRest, WithRest>] : []: T
// GenericConstructorParameters: Takes two arguments
// Arg 1. a constructor
// Arg 2. a tuple of types (one type for each generic in the constructor)
type GenericConstructorParameters<T extends abstract new (...args: any) => any, K> = UnknownReplacer<ConstructorParameters<T>, K>
Here it is in action with the classes in my original question: Playground Link
And here it is if the Parent
class takes multiple generics: Playground Link
Why it works
GenericConstructorParameters
takes two arguments. It works by calling ConstructorParameters
on its first argument (a constructor, just like the first argument of ConstructorParameters
) to get a tuple of constructor argument types. Because generics become unknown
when extracted by ConstructorParameters
, we then replace each unknown
type in the resulting tuple with a type provided by our second argument (a tuple of replacement types).
Note that while I've only ever seen unknown
replace a generic as a result of calling ConstructorParameters
on a generic class, I don't know if that's guaranteed in every scenario. So I'm hoping someone else can verify that.
If that is correct, then reliably determining what is or isn't truly unknown
is the next thing to do. I created the IsUnknown
utility type (expanded below for readability) to do that, with help from the IfAny
utility which I got from this SO answer. However, this is another instance of not being certain this works in every single scenario. If it does though, then that should mean this is scalable and should work no matter what other types my super class uses, including any
types.
type IsUnknown<T> =
unknown extends T // unknown only extends either itself or any (I think)
? IfAny<T, never, true> // so if we can narrow it down, just check if it is any
: never;
I created the UnknownReplacer
utility (expanded below for readability) to do most of the leg work. I figured out how to do recursion on variadic tuples thanks to this SO answer, and through that I'm able to replace unknowns from the constructor parameters with whatever the next unused type is in our replacements tuple.
type UnknownReplacer<T, K> =
K extends [infer WithThis, ...infer WithRest] // if K is a populated tuple of types (i.e. one for each unknown to replace)
? T extends [infer ReplaceThis, ...infer ReplaceRest] // ...then if T is a populated tuple of types (i.e. from our constructor arguments)
? IsUnknown<ReplaceThis> extends never // ......then check if the first type in T is NOT unknown
? [ReplaceThis, ...UnknownReplacer<ReplaceRest, K>] // .........and if not unknown, return the first type from T with a recursive call on the remaining args from T
: [WithThis, ...UnknownReplacer<ReplaceRest, WithRest>] // .........but if it is unknown, return the first type from K with a recursive call on the remaining args from T and K
: [] // ......but if T is empty (or invalid), return an empty tuple as we've run out of things to check
: T // ...but if K is empty (or invalid), return T as there's nothing we can use to replace anyway
Finally the GenericConstructorParameters
type just sort of wraps things together nicely.
I don't know how niche this use case is, or if someone's solved this before, but I'm hoping this is (actually correct and) able to help someone else out if they come across the same problem.
Looking for feedback!