I'll present four puzzles and two solutions about TypeScript class
es and type
s.
The cover image is from another article.
These are just puzzles I find fun, but reading this might have the happy side effect of getting you out of a TS 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 AType = typeof A;
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?
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 like type
aliases and interface
s live in the World of Types. Variables 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 cannot have the same spelling.
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
declarations 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 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 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 have types 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();
Top comments (9)
typeof is JS operator. Features which exist in JS are the same in TS. TS doesn't alter behaviour of JS, it augments it. That is why you can't expect that JS operator would work with TS type.
TypeScrpit has a type-level typeof a.k.a "type guard". It has the same spelling as value-level (JS)
typeof
but is distinct."Type guard" means that TS takes into account information about type (from runtime check) of JS's
typeof
. It is still JStypeof
. For exampleAlso "Type guard" !==
typeof
. Here are some example of "Type guards":I rushed and used the wrong terminology in my last reply, sorry about that.
Here is type-level
typeof
, I promise it is a thing and is distinct from value-level/JStypeof
. Type-leveltypeof
returns a type. JStypeof
returns a string.typescriptlang.org/play/?ssl=9&ssc...
Indeed you are right
if
typeof
is intype
position this is TS thing. I never paid attention to thiscool, huh!
It sneaks under the radar, since, as far as I can tell, there is no documentation on it.
But the error is the same as if you try to use type in place of value
So it seems like type checker itself doesn't differentiate TS and JS versions of
typeof
and always returns error for JS version.You can ask if type has a type(kind) at the type level also. You can do that by conditional type:
Playground
Also my latest tweet about confusion in Type level and Value level operators:
Thanks for your comment!
Your example using conditional types shows that we can ask TypeScript questions about types.
But one question we cannot ask is what the type of a type is.
This is a translation of your example from types to sets (in the sense of "set theory"). The TLDR is:
typeof
is like "memberOf" in set theoryextends
is likesubsetOf
in set theoryTS's type theory is like a set theory where sets cannot be members of other sets. But they are allowed to be subsets of other sets.