The Type of a Type in TypeScript
I’ll present four puzzles and two solutions about class
es and type
s.
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 A
below?
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:
- How could
AA
be both equal and not equal toA
? - 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
, andenum
. 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 ofC
- Applying the type world’s
typeof
operator toC
gives you the type of the value-landC
, 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 Valuesconst 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 A
s: one in the world of types and one in the world of values. AA
is equal to the A
in 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 A
s: a type-level A
and 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();