The Type of a Type in TypeScript

An illustration of Russell’s paradox from an article on the topic: https://medium.com/@jiwonjessicakim/russells-paradox-f8897

I’ll present four puzzles and two solutions about classes and types.

These are just puzzles I find fun, but reading this might have the happy side effect of getting you out of a TypeScript jam in the future. All four puzzles have come up in real-life situations that I’ve been asked about.

Puzzle 1

TypeScript has a neat feature, type-level typeof. It's probably inspired by the distinct value-level JS feature also called typeof:

typeof null;       // "object" 
typeof class A {}; // "function"

TypeScript’s type-level typeof thing gives you an alias for the type of the variable thing:

const thing = 3 * 2;
type Num = typeof thing;
const y: Num = 1; // OK

But what is the type of Num?

type TypeType = typeof Num;
// error: 'Num' only refers to a type, but is being used as a value here

TypeScript won’t even let us ask the question.

Coq, Agda, and Idris all let you ask this type of question, but it is un-askable in TypeScript.

So we’ve disovered this rule: in TS, types don’t have types. Fair enough. So what’s going on in the following example?

class A {}
const a: A = new A();
type AA = typeof A; // OK

The second line shows us that A is a type (we are using it to say that a is of type A). Then the third line shows us that we can take the type of A. So it looks like we've taken the type of a type!

Puzzle 1 is: “How can types both have and not have types in TypeScript?

Puzzle 2

Puzzle 2 is: How can AA be both equal and not equal to Abelow?

class A{}
type AA = A;
// if AA and A really are the same type,
// then we'd expect them to behave the same way
type works = typeof A; // OK
type doesNotWork = typeof AA; // Error

When reading the example above, bear in mind the principle of Identity of Indiscernables. If A and AA really are equal, then we'd expect to be able to use AA wherever we use A. But the last two lines of the example show a case where A is allowed but AA is not.

Solution to Puzzles 1 and 2

At this point, there are two puzzles:

  1. How could AA be both equal and not equal to A?
  2. How can types both have and not have types in TypeScript?

The puzzles are just illusions caused by the interaction of two TS features:

  • TS has two worlds of things: the World of Values and the World of Types
  • class declarations introduce both a type and a value

Things declared with type and interface exist in the World of Types.

Things declared with const, let and var exist in the World of Values.

You're allowed to reuse names in the same scope as long as they refer to things in different worlds:

type Thing = number;
const Thing = "hello"; // no error

But two things in the same world in the same scope cannot have the same name:

const thing2 = 3;
const thing2 = {}; // error: Cannot re-declare block-scoped variable "thing2"

Ignore apparent violations of the rule for interface, namespace, and enum. This is the dread "declaration merging" feature I wrote about earlier.

As you saw above, we can use type to name something in the World of Types and const to name something in the World of Values. But a class declaration is magical: it simultaneously names something in the world of values and names another thing in the world of types!

The next example illustrates this double-declaration behavior. Note that:

  • the type C is the type of instances of C
  • Applying the type world’s typeof operator to Cgives you the type of the value-land C, which is a constructor function.
class C { a = 1 };
type CInstance = C; // alias the `C` from the World of Types
const Constructor = C; // alias the `C` from the World of Values
const c: C = new C();
const c2: CInstance = new C();
const cons: typeof C = Constructor;
// The next line errors, because CInstance does not have the same dual-nature as `C`
// CInstance is *only* a type, so you cannot calculate its type
const cons2: typeof CInstance = Constructor;

Two puzzles solved!

Consider again an example like this:

class A {}
type AA = A;
type Z = typeof A; // OK
type ZZ = typeof AA; // Error

Puzzle 1 was “How could AA be both equal and not equal to A?".

The answer is that there are two As: one in the world of types and one in the world of values. AA is equal to the Ain the world of types, but not equal to the A in the world of values.

Puzzle 2 was “How can types both have and not havetypes in TypeScript?”

The answer is that we can never ask about the types of types in TypeScript. The seeming-violation of this rule was only because we were dealing with two As: a type-level Aand a value-level A. We can ask for the typeof the value-level A, but TS won't let us ask about the type of the type-level A or of any of its aliases.

Puzzle 3

Puzzle 3 is: why are there no errors in the code example below?

class Klass {};
const konstructor = Klass;
const obj1: Klass = {};
const func: typeof Klass = konstructor;
const obj2: Klass = konstructor; // why no error here?

Puzzle 4

Puzzle 4 is: “How can we correctly-type a function that returns a class?”

For example, is there a way to get things like this to compile?

const MyClass = createClass({});
const m: MyClass = new MyClass();

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store