Why are TypeScript arrays covariant?

As you’ve noted, array covariance is unsound and can lead to errors at runtime. One of TypeScript’s Design Non-Goals is

  1. Apply a sound or “provably correct” type system. Instead, strike a balance between correctness and productivity.

which means that if some unsound language feature is very useful, and if requiring soundness would make the language very difficult or annoying to use, then it’s likely to stay, despite potential pitfalls.

Apparently there comes a point when it is “a fool’s errand” to try to guarantee soundness in a language whose primary intent is to describe JavaScript.


I’d say that the underlying issue here is that TypeScript wants to support some very useful features, which unfortunately play poorly together.

The first is subtyping, where types form a hierarchy, and individual values can be of multiple types. If a type S is a subtype of type T, then a value s of type S is also a value of type T. For example, if you have a value of type string, then you can also use it as a value of type string | number (since string is a subtype of string | X for any X). The entire edifice of interface and class hierarchy in TypeScript is built on the notion of subtyping. When S extends T or S implements T, it means that S is a subtype of T. Without subtyping, TypeScript would be harder to use.

The second is aliasing, whereby you can refer to the same data with multiple names and don’t have to copy it. JavaScript allows this: const a = {x: ""}; const b = a; b.x = 1;. Except for primitive data types, JavaScript values are references. If you tried to write JavaScript without passing around references, it would be a very different language. If TypeScript enforced that in order to pass an object from one named variable to another you had to copy all of its data over, it would be harder to use.

The third is mutability. Variables and objects in JavaScript are generally mutable; you can reassign variables and object properties. Immutable languages are easier to reason about / cleaner / more elegant, but it’s useful to mutate things. JavaScript is not immutable, and so TypeScript allows it. If I have a value const a: {x: string} = {x: "a"};, I can follow up with a.x = "b"; with no error. If TypeScript required that all aliases be immutable, it would be harder to use.

But put these features together and things can go bad:

let a: { x: string } = { x: "" }; // subtype
let b: { x: string | number }; // supertype 
b = a; // aliasing
b.x = 1; // mutation
a.x.toUpperCase(); // 💣💥 explosion

Playground link to code

Some languages solve this problem by requiring variance markers. Java’s wildcards serve this purpose, but they are fairly complicated to use properly and (anecdotally) considered annoying and difficult.

TypeScript has decided not to do anything here and treat all property types as covariant, despite suggestions to the contrary. Productivity is valued above correctness in this aspect.


For similar reasons, function and method parameters were checked bivariantly until TypeScript 2.6 introduced the --strictFunctionTypes compiler option, at which point only method parameters are still always checked bivariantly.

Bivariant type checking is unsound. But it’s useful because it allows mutations, aliasing, and subtyping (without harming productivity by requiring developers to jump through hoops). And method parameter bivariance results in array covariance in TypeScript.


Okay, hope that helps; good luck!

Leave a Comment