I'm working on a project where I need to instantiate multiple copies of a particular class, but with different parameters. I'm creating a utility function withAlteredConstructorArgs
to help me with this, but I'm having trouble getting the types quite right.
Here is an example of how I would like to use this utility:
const createCycler = withAlteredConstructorArgs(
Cycler,
function reverseElementArgs<T>(elements: Array<T>): [Array<T>] {
return [elements.reverse()];
}
);
const cycler = createCycler(['foo', 'bar', 'baz', 'quux']);
console.log(cycler.get(), cycler.get());
// > should log 'quux', 'baz'
The function I've written works as intended, but does not typecheck the way I expected. I have created a TypeScript Playground to fully demonstrate, but here is the implementation:
function withAlteredConstructorArgs<Args extends Array<any>, T>(
c: { new(...args: Args): T },
fn: (...args: Args) => Args
): (...args: Args) => T {
return (...args: Args) => new c(...fn(...args))
}
I recognize that I am not constraining T
enough, and that's why I'm getting the error message: 'any[]' is assignable to the constraint of type 'Elements', but 'Elements' could be instantiated with a different subtype of constraint 'any[]'
. I understand this error and have fixed it before, but not in a problem relating to class constructor arguments. What is the correct syntax for properly constraining the constructor parameters for T
?
I recognize that I could probably work around this by creating a factory function and removing constructors altogether from the problem statement, but I feel as though understanding this approach would better improve my understanding of TypeScript in general. Also, I have hit similar issues with the factory approach, so it seems as though the constructor method would be the way to go anyways.
CodePudding user response:
What is the correct syntax for properly constraining the constructor parameters for
T
?
I thought I understood your question based on the code you've written until I read that, which makes me less sure about which argument you would like to be the provider of the type information creating the constraint and which argument you'd like to be constrained.
I'm going to respond based on how you wrote your code: that you want the parameters of the constructor provided as the first argument to provide the type information for Args
, and for the callback function to take arguments of that type.
A potential point of confusion could be multidimensional arrays: the Args
are an array of arguments, and your class constructor for Cycler
accepts an array as its first and only argument (so, an array within an array).
Another potential point of confusion here is that you seem to want higher-kinded types, but that's not available in TypeScript (yet, at least). It seems like you want createCycler
to be generic (it isn't in your example), and you can make it generic by using a generic type assertion on it. (In other cases, you could use a generic type annotation, but I don't think that will work here.)
// Define the function:
type Fn<
Params extends unknown[] = any[],
Result = any,
> = (...params: Params) => Result;
type Ctor<
Params extends unknown[] = any[],
Result = any,
> = new (...params: Params) => Result;
// > = { new (...params: Params): Result }; // Alt syntax
function withAlteredConstructorArgs<C extends Ctor>(
ctor: C,
fn: Fn<ConstructorParameters<C>, ConstructorParameters<C>>,
): Fn<ConstructorParameters<C>, InstanceType<C>> {
return (...args: ConstructorParameters<C>) => new ctor(...fn(...args));
}
// Use:
// First, an example using a simpler class which DOESN'T take a generic array
// and then use it as the type for its first constructor parameter:
class Person {
constructor (readonly first: string, readonly last: string) {}
log () {
console.log(this.first, this.last);
}
}
const createPerson = withAlteredConstructorArgs(
Person,
(
first, // correctly inferred as string
last, // again, string
) => [
first.toLowerCase(),
last.toUpperCase()
] as [string, string], // TS doesn't infer tuples, so an assertion is needed here
);
const p = createPerson('Wolfgang', 'Mozart');
p.log() // "wolfgang" "MOZART"
// Now, your Cycler class:
class Cycler<Elements extends Array<any>> {
private index = 0;
constructor(private elements: Elements) {}
get() {
const el = this.elements[this.index];
this.index;
this.index %= this.elements.length
return el
}
}
// I inferred that you want this to be generic:
// so that the types of the arguments passed to the function that it RETURNS
// will be used for the resulting Cycler instance
const createCycler = withAlteredConstructorArgs(
Cycler,
(elements) => [elements.reverse()] as [unknown[]],
) as <T extends any[]>(elements: T) => Cycler<T>;
const cycler = createCycler(['foo', 'bar', 'baz', 'quux']); // Cycler<string[]>
console.log(cycler.get(), cycler.get()); // "quux" "baz"
CodePudding user response:
I was able to get your code to compile and produce the expected output with the following changes:
- Changing the definition of
Cycler
fromclass Cycler<Elements extends Array<any>>
toclass Cycler<E>
. - Changing the signature of
Cycler
's constructor fromconstructor(private elements: Elements)
toconstructor(private elements: E[])
.