The TypeScript compiler tsc
compiles the following code without complaints, even with the --strict
flag. However, the code contains a basic bug, which is prevented in languages like Java or C#.
interface IBox<T> {
value: T;
}
const numberBox: IBox<number> = { value: 1 };
function insertString(items: IBox<string | number>): void {
items.value = 'Test';
}
// this call is problematic
insertString(numberBox);
// throws at runtime:
// "TypeError: numberBox.value.toExponential is not a function"
numberBox.value.toExponential();
Can tsc
be configured so that such a bug is recognized?
TypeScript doesn't really have a good general way to handle contravariance or invariance. To a rough approximation, if you are outputting something (function outputs, read-only properties) you can output something narrower but not wider than the expected type (covariance), and if you're inputting something (function inputs, write-only properties) you're allowed to accept something wider but not narrower than the expected type (contravariance). If you're reading and writing the same value, then you're not allowed to narrow or widen the type (invariance).
This issue doesn't show up as much in Java (not sure about C#) mostly because you can't easily create unions or intersections of types, and because in generics there are extends
and super
constraints that act as markers for covariance and contravariance. You do see this in Java arrays, at least, which are unsoundly considered covariant (try the above with Object[]
and Integer[]
and you will see fun happen).
TypeScript has generally done a good job with treating function outputs as covariant. Until TypeScript v2.6, the compiler treated function inputs as bivariant, which is unsound (but has some useful effects; read the linked FAQ entry). Now there is a --strictFunctionTypes compiler flag that lets you enforce contravariance for function inputs for standalone functions (not methods).
Currently, TypeScript treats property values and generic types as covariant, meaning they are fine for reading but not fine for writing. That leads directly to the issue you're seeing. Note that this is also true for property values so you can reproduce this problem without generics:
let numberBox: { value: number } = { value: 1 };
function insertString(items: { value: string | number }): void {
items.value = 'Test';
}
insertString(numberBox);
numberBox.value.toExponential();
I don't have great advice other than "be careful". TypeScript is not intended to have a strictly sound type system (see non-goal #3); instead, the language maintainers tend to address issues only to the extent that they cause real-world bugs in programs. If this kind of thing affects you a lot, maybe head over to Microsoft/TypeScript#10717 or a similar issue and give it a 👍 or describe your use case if you think it's persuasive.
Hope that's helpful. Good luck!
Since typescript 4.7, you can now explicitly specify type variance:
in
made type contravariantout
made type covariantin out
made type invariantBack to your question, you can just make IBox
invariant:
interface IBox<in out T> {
value: T;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With