TypeScript's unknown type and type variance
Type systems have a tendency of sneaking up on you. You start justtrying to enforce some obvious invariants like ���I shouldn't be ableto assign a string value to a Boolean-typed variable���, and before youknow what's happened you're reasoning about subtyping relations andtype parameters.
One thing that I keep running into, but for a long time refused toproperly get to the bottom of, is that using unknown in TypeScriptwould 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 asupertype and a subtype of every other type. It can be useful,but if you use it widely you might as well not check your types atall, because it generates type system soundness holes big enough todrive a truck through.
unknown indicates a type that we know nothing about. It is asupertype of everything, and a subtype only of itself and any.This means you can pass anything to a function that takes anunknown parameter, but you can't use an unknown value in aplace where a properly typed value is required. It is a lessdangerous way to indicate that we don't know the type of something.Unlike with any, you'll actually have to perform some kind ofexplicit downcast in order to use the untyped value.
Finally never is a subtype of everything, but a supertype only ofitself and any. This is most often used to indicate unreachablecode (a function that always throws, for example, returns never)or forbidden data structure shapes.
The situation I want to talk about here is type-parameterized datastructures that are used in a heterogeneous way. As a concreteexample, say you have a Widget type, where each widget has aparameter of type T and a type of type WidgetType which defineswhat it looks like and how it and behaves.
The type parameter is useful, because if you have text widgetWidget you want to be able to treat widget.param as astring. But if you have a collection of widgets, which may havedifferent parameter types, how do you type that?
Widget[] is wonderful, of course. This is the old way of doingthis in pre-version-3.0 TypeScript. Never produces any type systemcomplaints... because it completely turns off type checking on theseparameters.
Since that moots a lot of the advantages of doing type checking in thefirst place, the general recommendation is to use the unknown type.So our array is now a Widget[]. Great.
Except that widgetArray.push(textWidget) now produces a puzzlingtype error (���Widget is not assignable toWidget���). If our generic widget type is not a supertype ofspecific widget types, that makes this pattern very difficult to workwith. Wasn't unknown a supertype of everything? What is going on?
Variance is what'sgoing on. Variance is one of those unwelcome complications that comeup when you start defining a halfway powerful type system. I'llrefrain from explaining it in depth here���you can find plenty of goodexplanations on the internet���but it roughly boils down to this:
If B is a subtype of Athen (b: B) => number is a supertype of (a: A) => numberSome ways to use types, such as taking them as function parameters,invert the subtyping relationship. If the parameter to function F isa subtype of the parameter to function G, then G's type, becauseyou can pass it a subset of the types that F takes, is a supertypeof F's type.
For parameterized data structures, this means that T is no longera subtype of T when it contains functions that take values of thetype of the type parameter as arguments.
So if the widget looks something like this...
type Widget = { parameter: T, type: {render: (parameter: T) => Pixels}}... then Widget is no longer a subtype ofWidget. And that is why using unknown often justdoesn't work as well as you'd hope.
One way around this is to painstakingly make sure that your datastructures stay ���covariant���. If I remove the type field from mywidgets, the problem goes away.
But there are a lot of situations where that is really inconvenient,or even impossible. For those, the only workable situation I've foundis to create a ���projected��� type, a subtype of Widget withthe contravariant pieces removed. TypeScript's type-manipulatingoperators fortunately make this relatively easy.
type AnyWidget = Omit, "type">You can think of this as the thing we were trying to express withWidget in the first place���a generic subtype of widget wherewe don't know what's in it. A list of widgets would now useAnyWidget[], to which the type system will allow us to add morespecific widget types.
Of course, when it is time to actually render such a widget, you'llneed to cast it back to Widget or do other type-castingacrobatics. But in my experience the code that needs to do this isusually relatively well-isolated.
Marijn Haverbeke's Blog
- Marijn Haverbeke's profile
- 46 followers

