I think variance can be simulated by typing the private field as an existing covariant/contravariant/invariant type
class CovariantFoo<T> {
private phantom!: T
}
class ContravariantFoo<T> {
private phantom!: (_: T) => void
}
class InvariantFoo<T> {
private phantom!: (_: T) => T
}
(inspired by Rust [0])