Authored by Allison Kaptur
I’ve been writing typed Python with
mypy for many years, but I had never worked with TypeScript before 2021. When I started working in TS, I was surprised by how different its philosophy is from mypy. In this post, I’ll describe several of the biggest surprises I encountered.
What is a type? Part 1: Extremely specific type inference
When I started adding types to my Python code years ago, I found it fairly intuitive to understand what a type was. For coders familiar with Python, you can get started with a single sentence: think of a type as a Python class. That class might be a user-defined class that you wrote yourself, or a builtin class like
int. A function that takes an instance of class
A should be annotated with the type signature
A. So getting started with type annotations didn’t feel too hard. 
Effective TypeScript offers a useful definition of a type in TypeScript: “Think of a type as a set of values.” One way to define a set of values is with a class (“every value that is an instance of class X”), but of course you can define a set of values infinitely many other ways too.
To be concrete, if you write Python code like this:
then the mypy type checker will infer the type of
str. Sounds great to me — yes,
“hi” is definitely a string.
But if you write JS/TS code like this:
then the TypeScript type checker will infer the type of
“hi” isn’t a type — it’s a value!
const x = “hi” example, TypeScript has inferred that
x can have exactly one value,
“hi” (since it’s a
const). So the type of
x — the set of values that
x could have — is just
“hi”. A set containing one value feels like a very different kind of type than a class!
After a few months of working with TypeScript, I came to appreciate TS’s tendency towards narrow type inference. In TS, thinking of a type as a set of values makes it a snap to model flags, state indicators, and other small sets of strings. One example that crops up all the time in our Vue code at Pilot:
The type annotation
state: State is a much stronger guarantee than saying that
state is a string. It gives you immediate feedback if you typo the state (as, say,
“errror”) or pass a variable that’s not constrained to those three values into your state-management function.
It is possible to accomplish the same narrow type annotation in Python, but you have to do it explicitly — the type checker won’t infer it. 
Thinking of types as roughly equivalent to runtime classes — which isn’t a bad starting place in Python — held me back when I started working in TypeScript, where types are often much narrower.
What is a type? Part 2: Structural typing
The first thing I found jarring about TypeScript was how often the types were narrower than I expected. The second thing was how often types were wider than I expected — they contained more values than just the instances of a class.
TypeScript uses structural typing. When I first read about structural and nominative typing, my eyes glazed over at the jargon, but the core idea is pretty simple: structural typing is about the shape of a type, whereas nominative typing is about the name. TypeScript is mostly structural. Mypy is mostly nominative. Let’s look at some examples to make this concrete.
Nominative typing in Mypy
Mypy is dominated by nominative typing. In general, the type of an object is its class: what you’d get from
type(a) at runtime. If you have two classes that have the same shape, mypy doesn’t care about that. To be concrete, the following code produces a type error in mypy:
B have the same shape, the type checker treats them as unrelated. I found this intuitive — if I think I have an instance of
A when I really have an instance of
B, that’s often a bug in my code.
Structural typing in TS
Consider the equivalent code in TS:
Neither of these calls of
my_func is an error in TypeScript. In TS, type checking is all about the shape of an object. Since both of these objects have the same shape, the type checker treats them as interchangeable.
object). Does this example change if
B are runtime classes, as in the Python example? No, it doesn’t change at all! Below, both
B are classes (which means there are objects in both the type space and the value space, i.e. in both TS and the resulting JS), and there’s still no error from TypeScript.
This is the heart of structural typing: two types that have the same shape are effectively interchangeable.
Even more surprising to me was that TypeScript’s checking of the shape of a type is not limited to the specified keys. If an object has at least the keys and values types that a TS type annotation expects, it passes the type checker, so even the following is not a type error:
Structural typing in Mypy
I said above that mypy is mostly a nominative type checker: based on the names and identities of classes. However, it’s also possible to do TypeScript-style structural typing in mypy.
Structural subtyping was introduced in mypy in 2017, and landed in Python as of version 3.8. By explicitly specifying a protocol — what you might think of colloquially as an interface, or an abstract base class — you can explain the intended shape of your type to the Python type checker. To model the TypeScript behavior above with mypy, you could write code like this:
Nominative typing in TypeScript
I also said above that TypeScript is mostly structural. But just like it’s possible to do structural type checking in Mypy, it’s also possible to do more nominative type checking in TypeScript. The primary way to accomplish this is to use a tagged union (or discriminated union), introduced in TypeScript 2.0.
In a tagged union, you explicitly annotate a type definition with some key that has a literal value. Below, we use
kind, but that’s arbitrary — the name of the key doesn’t matter.
Note that any object that satisfies the shape of
A (including the tag, so with a
“a”) will pass the type checker. The following is not a type error:
Other goodies: Mapped types and type manipulation
In the previous examples, the behavior in TypeScript was expressible in Mypy and vice versa. However, TypeScript also packs a number of concepts that can’t be easily expressed in mypy.
After a few months working with it, I’m really starting to enjoy some of these bells and whistles. Type manipulation like
Omit, mapped types, conditional types, and the truly wild template literal types open up a rich language of type expression.
To pick just one example, in some of our test helpers, we want a function that returns a particular type, supplies defaults for all fields, and allows the caller to override some, none, or all of the fields. In TypeScript that annotation is trivial to write using
Partial, which constructs a type identical to the input type but with all the properties made optional. In a test, the caller can override some of the fields or none of them, without losing type coverage.
Learning TypeScript for Pythonistas
If you’ve been working with types in Python for a long time, I recommend spending some time exploring TypeScript. I found that the difference in philosophy deepened my understanding of Python types, and clarified the difference between nominative and structural typing in mypy, which is capable of both.
I found two resources helpful for learning TypeScript:
- Effective TypeScript. This book is another winner from the Effective series. Its primary benefit for me was providing the language to ask a useful question, and without that, it’s very hard to make progress. For example, it’s much easier to search “user-defined type guard” than to try to figure out the meaning of
is, or to search “type assertion” or “const assertion” instead of
- The TypeScript playground. The TypeScript playground lets you write code snippets and typecheck them, as you might expect. But what I really love about it is its clear presentation of how that code snippet behaves in the type space and in the value space. I found it very useful to hone my intuition about runtime behavior by checking whether a given operation did anything in the value space.
I wish I’d found these two resources earlier. If you’re ramping up on TypeScript, I recommend them highly.
Of course we’re hiring
 I did struggle initially with some aspects of type theory that aren’t made explicit in untyped Python code, like generics, unions, and type variables.
 Historically, if you wanted to type-check that a state flag in Python had only the three values “loading”, “loaded”, or “error”, the only way to do it was to create an
enum.Enum with those three values. This gives us the ability to annotate a type as one of a small set of values, but it accomplishes it by making a class covering only those values — bringing us back to the idea that a type is effectively a class.
As of 2019, you can also add the type annotation
Literal in Python. Now you can write Python code that’s closer to the TypeScript version:
I haven’t seen widespread adoption of
Literal in Python so far. This is doubtless partially because it’s a new concept. But in my opinion, another hurdle that
Literal breaks the Python/mypy heuristic that a type is basically a class. If you’re not used to thinking of types as sets of values,
Literal doesn’t make much sense.
as const is necessary in the object definition so that TS understands
“a” to be a literal, rather than inferring it as a string. A key
kind with a string value doesn’t match the type — it has to be a literal.