The _ModelClass
argument to createCollection
is completely unused inside the JavaScript, right? It looks as though its only purpose is to give the TypeScript compiler an inference site for the generic parameter T
. If so, maybe the "right" solution is to do away with that argument entirely:
function createCollection<T extends Model>(
options: CollectionOptions
): Collection<T> {
const collection: Collection<T> = new Collection(options);
return collection;
}
and either have callers manually specify the type of the T
parameter as, say, User
:
const model = createCollection<User>({ key: 'id' }); // manually specified
model.add(new User()); // okay
or have them assign the return value to an manually annotated variable of type, say, Collection<User>
and have the compiler infer T
as User
contextually from the desired return type:
const model2: Collection<User> = createCollection({ key: 'id' });
model2.add(new User()); // okay
If you want to preserve the dummy _ModelClass
argument and your intent is to pass in a class constructor instead of a class instance, then createCollection()
has the wrong signature. Class declarations like your User
example introduce both a named interface type corresponding to the type of class instances, and a named value referring to the class constructor. And these are different, despite having the same name. The constructor value named User
is not itself of type User
.
The type named User
is sort of like {id: number; [key: string]: any;}
; an object with a numeric id
property and a bunch of other possible properties, while the type of the value User
(that is, typeof User
,) is sort of like new () => User
; a newable object that produces a value of type User
when called with new
.
If you care to read more about the distinction between types and values and how the same name can refer to a type and a value depending on context, you could look at another lengthy monologue of mine on this subject.
In any case, if your intent is to accept constructors and not instances, you should annotate the type of the _ModelClass
parameter not as T
, but as something like new (...args: any) => T
which means "a newable constructor that takes some number of arguments we don't care about and produces a value of type T
":
function createCollection<T extends Model>(
_ModelClass: new (...args: any) => T,
options: CollectionOptions
): Collection<T> {
const collection: Collection<T> = new Collection(options);
return collection;
}
Then your function will accept the User
constructor:
const model = createCollection(User, { key: 'id' });
model.add(new User()); // okay
But complain about a User
instance:
const modelBad = createCollection(new User(), { key: 'id' }); // error!
// Type 'User' is not assignable to type 'new (...args: any) => Model`
If for some reason you want the compiler to accept either an instance or a constructor as _ModelClass
, you can annotate that parameter as a union of the instance and the constructor types:
function createCollection<T extends Model>(
_ModelClass: (new (...args: any) => T) | T,
options: CollectionOptions
): Collection<T> {
const collection: Collection<T> = new Collection(options);
return collection;
}
And then it will work both ways:
const modelCtor = createCollection(User, { key: 'id' });
modelCtor.add(new User()); // okay
const modelInstance = createCollection(new User(), { key: 'id' }); // error!
modelInstance.add(new User()); // okay
Okay, hope that helps; good luck!
Playground link to code