Reputation: 2012
Inception time!!
Consider the following set-up:
interface Props {
[k: string]: any;
}
abstract class Model<P extends Props> {
// omitted...
}
abstract class Factory<M extends Model<P>, P extends Props> {
abstract createInstance(props: P): M;
}
Now every time I implement a "Factory" I will have to supply the "Props" type explicitly, but the "Model" type already has it:
interface UserProps extends Props {}
class UserModel extends Model<UserProps> {}
class UserFactory extends Factory<UserModel, UserProps> {}
Is it possible to use the generic type of an extended type what was supplied as generic type? So in the use case presented above:
Is it possible to use the type "Props", an extension of the required generic type "P" in "Model", as a generic type in "Factory", a class what requires generic type M to be extended with the "Model" type?
The implementation for this use case presented above works fine but unfortunately will become more challenging to maintain when the amount of "Factory" implementations grow.
There are several things I tried but none of these where syntactically correct:
P extends Props
, gave the following errors:
abstract class Factory<M extends Model<P extends Props>> {
// ^1 ^2
abstract createInstance(props: P): M;
}
Props as P
, gave the following errors:
abstract class Factory<M extends Model<Props as P>> {
// ^1 ^2^3
abstract createInstance(props: P): M;
}
Am I missing something or is it simply not there (yet)?
Upvotes: 1
Views: 91
Reputation: 329943
Warning: a type like class Model<P extends Props> {}
with no structural dependence on P
is not good example code. It has weird effects; for example, Model<SomeProps>
and Model<SomeOtherProps>
will be completely identical to each other, and both will be completely identical to the empty object type {}
. In order to avoid such weirdness, in what follows I will add a property so that there is some structural dependence:
abstract class Model<P extends Props> {
abstract props: P; // structural dependence
}
You can't declare a type parameter P
where you're trying to do it; as you can see, it's syntactically invalid. There are only a few places where you can bring new type parameters into existence... if you don't want to write class Factory<M extends Model<P>, P extends Props> {}
, you will have to do something else.
What you really want to do here is to use something like an existentially-quantified generic type parameter, as requested in microsoft/TypeScript#14466. Imagine you could say exists P extends Props
to mean "for some P
that extends Props
" without having to specify it. And you'd declare something like class Factory<M extends Model<P>, exists P extends Props> {}
, and then you could later refer to Factory<UserModel>
and have P
inferred. But there is no direct support for existential types in TypeScript.
Or you could imagine something like partial type parameter inference, as requested in microsoft/TypeScript#26242, where you could manually specify one of your two type parameters while letting the compiler infer the other one. And you'd declare something like class Factory<M extends Model<P>, P extends Props = infer> {}
and then you could later refer to Factory<UserModel>
and have P
inferred. But again, there is no direct support for this.
Barring the ideal solutions, I think the closest you'll be able to get to what you're looking for is something like this:
type PropsType<M> = M extends Model<infer P> ? P : never;
abstract class Factory<M extends Model<any>> {
abstract createInstance(props: PropsType<M>): M;
}
Here, PropsType<T>
takes a Model<P>
for some P
, and returns P
. Since we're not going to define P
inside of Factory
, you need some way to get P
from M
... hence PropsType
.
And then your Factory
only declares M extends Model<any>
. We are using the any
"type", which is intentionally unsound so you are allowed to use it in place of a real Props
-like type, no matter what sort of dependence Model<P>
has on P
.
Now the following will work as you want:
declare class UserModel extends Model<UserProps> {
props: UserProps;
}
declare class UserFactory extends Factory<UserModel> {
createInstance(props: UserProps): UserModel;
}
The caveat here is that, since any
is intentionally unsound, you could pass some weird things as M
:
interface BadModel {
props: undefined; // undefined should not work
}
class Oops extends Factory<BadModel> {
createInstance(props: never): BadModel {
return { props: undefined };
}
}
BadModel
doesn't extend Model<P>
for any valid P
, but it does extend Model<any>
, so it's accepted by Factory
. There might be things that you could do to try to prevent such weirdness, but I wouldn't bother unless you are likely to run into it in practice.
The point is: using any
is a workaround for the lack of existential types, and such edge cases are what makes this a workaround and not a perfect solution.
Upvotes: 2