TypeScript's unknown type and type variance
Marijn Haverbeke's blog<br>(license)
TypeScript's unknown type and type variance
Saturday, January 3, 2026 types typescript data structures<br>Type systems have a tendency of sneaking up on you. You start just<br>trying to enforce some obvious invariants like “I shouldn't be able<br>to assign a string value to a Boolean-typed variable”, and before you<br>know what's happened you're reasoning about subtyping relations and<br>type parameters.
One thing that I keep running into, but for a long time refused to<br>properly get to the bottom of, is that using unknown in TypeScript<br>would so often lead to complicated type errors.
TypeScript has three funky special types:
any is your basic way to make the type system shut up. It is a<br>supertype and a subtype of every other type. It can be useful,<br>but if you use it widely you might as well not check your types at<br>all, because it generates type system soundness holes big enough to<br>drive a truck through.
unknown indicates a type that we know nothing about. It is a<br>supertype of everything, and a subtype only of itself and any.<br>This means you can pass anything to a function that takes an<br>unknown parameter, but you can't use an unknown value in a<br>place where a properly typed value is required. It is a less<br>dangerous way to indicate that we don't know the type of something.<br>Unlike with any, you'll actually have to perform some kind of<br>explicit downcast in order to use the untyped value.
Finally never is a subtype of everything, but a supertype only of<br>itself and any. This is most often used to indicate unreachable<br>code (a function that always throws, for example, returns never)<br>or forbidden data structure shapes.
The situation I want to talk about here is type-parameterized data<br>structures that are used in a heterogeneous way. As a concrete<br>example, say you have a Widget type, where each widget has a<br>parameter of type T and a type of type WidgetType which defines<br>what it looks like and how it and behaves.
The type parameter is useful, because if you have text widget<br>Widget you want to be able to treat widget.param as a<br>string. But if you have a collection of widgets, which may have<br>different parameter types, how do you type that?
Widget[] is wonderful, of course. This is the old way of doing<br>this in pre-version-3.0 TypeScript. Never produces any type system<br>complaints... because it completely turns off type checking on these<br>parameters.
Since that moots a lot of the advantages of doing type checking in the<br>first place, the general recommendation is to use the unknown type.<br>So our array is now a Widget[]. Great.
Except that widgetArray.push(textWidget) now produces a puzzling<br>type error (“Widget is not assignable to<br>Widget”). If our generic widget type is not a supertype of<br>specific widget types, that makes this pattern very difficult to work<br>with. Wasn't unknown a supertype of everything? What is going on?
Variance is what's<br>going on. Variance is one of those unwelcome complications that come<br>up when you start defining a halfway powerful type system. I'll<br>refrain from explaining it in depth here—you can find plenty of good<br>explanations on the internet—but it roughly boils down to this:
If B is a subtype of A
then (b: B) => number is a supertype of (a: A) => number
Some ways to use types, such as taking them as function parameters,<br>invert the subtyping relationship. If the parameter to function F is<br>a subtype of the parameter to function G, then G's type, because<br>you can pass it a subset of the types that F takes, is a supertype<br>of F's type.
For parameterized data structures, this means that T is no longer<br>a subtype of T when it contains functions that take values of the<br>type of the type parameter as arguments.
So if the widget looks something like this...
type Widget = {<br>parameter: T,<br>type: {render: (parameter: T) => Pixels}
... then Widget is no longer a subtype of<br>Widget. And that is why using unknown often just<br>doesn't work as well as you'd hope.
One way around this is to painstakingly make sure that your data<br>structures stay “covariant”. If I remove the type field from my<br>widgets, the problem goes away.
But there are a lot of situations where that is really inconvenient,<br>or even impossible. For those, the only workable situation I've found<br>is to create a “projected” type, a subtype of Widget with<br>the contravariant pieces removed. TypeScript's type-manipulating<br>operators fortunately make this relatively easy.
type AnyWidget = Omit, "type">
You can think of this as the thing we were trying to express with<br>Widget in the first place—a generic subtype of widget where<br>we don't know what's in it. A list of widgets would now use<br>AnyWidget[], to which the type system will allow us to add more<br>specific widget types.
Of course, when it is time to actually render such a widget, you'll<br>need to cast it back to Widget or do other type-casting<br>acrobatics. But in my experience the code that needs to do this is<br>usually relatively...