I have a processor class with a concat function that takes another processor instance which is worked on and then returns once again another processor instance.
I want that the concat function takes a concatenator parameter that has the same type as the V generic. This works well in most cases, but not if I use the {} type. This should not work but it does. I also tried using NoInfer but it doesn't work.
PS: I know that the {} type represents any non-nullish type, so I need it to throw a compile error since it could theoretically take any non nullish type, which i dont want since i want it to be of same V type.
How can I fix??
Code:
class Processor<T, V> {
current: T
next : V
constructor(current: T, next: V) {
this.current = current
this.next = next
}
concat<U>(concatenator: Processor<U, V>): Processor<U, V> {
// Do something
// Fake return
return '' as unknown as Processor<U, V>
}
}
let c: Processor<string, {}> = new Processor('a', {})
let a: Processor<string, string> = new Processor('a', 'a')
let t = c.concat(a)
Your Processor<T, V>
type is covariant in V
(see Difference between Variance, Covariance, Contravariance, Bivariance and Invariance in TypeScript) so if V1 extends V2
then Processor<T, V1> extends Processor<T, V2>
. And since the empty type {}
does indeed represent any non-nullish value, and since string extends {}
, then Processor<string, string> extends Processor<string, {}>
. This is all TypeScript working as designed, and if it does not need to runtime errors, then it is arguably the case that you should just leave it like this.
TypeScript's type system doesn't have "exact types" as requested in microsoft/TypeScript#12936, so you can't easily say "these properties and no more", so there's no simple Exact<{}>
that you can use instead of {}
for this purpose. Well, there's something close-ish; you could try {[k: string]: never}
instead of {}
:
let c: Processor<string, { [k: string]: never }> = new Processor('a', {})
let a: Processor<string, string> = new Processor('a', 'a')
let t = c.concat(a); // error!
// Type 'string' is not assignable to type '{ [k: string]: never; }'.
But {[k: string]: never}
is fragile, and it can't be extended to situations with a finite nonzero number of properties.
If your use case really requires that Processor<T, V1> extends Processor<T, V2>
if and only if V1
and V2
are the same type (or mutually assignable), then what you want is for Processor<T, V>
to be invariant in V
. There are ways for that to happen naturally, if some member of the class is contravariant in V
then the whole thing is invariant:
class Processor<T, V> {
next: V // next is covariant in V
randomThing?: (x: V) => void; // randomThing is contravariant in V
} // whole thing is invariant in V
let c: Processor<string, {}> = new Processor('a', {})
let a: Processor<string, string> = new Processor('a', 'a')
let t = c.concat(a); // error!
Or, if you want to manually ensure a stricter variance, you can use variance annotations on the type parameter. The out
modifier means "not contravariant" and the in
modifier means "not covariant", so invariant is specified by in out
("neither contravariant nor covariant"):
class Processor<T, in out V> {
// ⋯
}
let c: Processor<string, {}> = new Processor('a', {})
let a: Processor<string, string> = new Processor('a', 'a')
let t = c.concat(a); // error!
My suggestion would be to make sure that Processor<T, V>
is naturally invariant in V
by adding members that ensure this, instead of using variance markers or more complicated type juggling. If you can't do that because you really don't need extra members, then I would fall back to my earliest suggestion: just leave it as it is if you don't run into actual runtime errors because of this. TypeScript's type system isn't perfect, but covariance is useful and if TypeScript thinks Processor<T, V>
is covariant in V
then you might actually benefit from it. Do you really want TS to complain if it infers a Processor<T, "a">
where you wanted to use a Processor<T, string>
? If V
is invariant then those two types are unrelated. If you think Processor<T, "a"> extends Processor<T, string>
is a good thing, then leave your code alone and just deal with {}
in some other way.