Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unsafe implicit conversion of generics in TypeScript

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?

like image 298
ominug Avatar asked Mar 08 '23 08:03

ominug


2 Answers

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!

like image 58
jcalz Avatar answered Mar 11 '23 23:03

jcalz


Since typescript 4.7, you can now explicitly specify type variance:

  • in made type contravariant
  • out made type covariant
  • in out made type invariant

Back to your question, you can just make IBox invariant:

interface IBox<in out T> {
  value: T;
}
like image 43
undefined Avatar answered Mar 12 '23 00:03

undefined