DEV Community

Cover image for The Type of a `type` in Typescript
Max Heiber
Max Heiber

Posted on

The Type of a `type` in Typescript

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

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 like type aliases and interfaces 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 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 of C
  • Applying the type world's typeof operator to C gives 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;
Enter fullscreen mode Exit fullscreen mode

Two puzzles solved!

Consider again an example like this:

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

Enter fullscreen mode Exit fullscreen mode

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 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 As: 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?
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Top comments (9)

Collapse
 
stereobooster profile image
stereobooster • Edited

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.

Collapse
 
maxheiber profile image
Max Heiber

TypeScrpit has a type-level typeof a.k.a "type guard". It has the same spelling as value-level (JS) typeof but is distinct.

Collapse
 
stereobooster profile image
stereobooster • Edited

"Type guard" means that TS takes into account information about type (from runtime check) of JS's typeof. It is still JS typeof. For example

type Test = "a" | "b";
const x = "a" as Test;
typeof x; // `string` according to JS version, not `Test` according to TS 

Also "Type guard" !== typeof. Here are some example of "Type guards":

if (typeof padding === "number") {
  return Array(padding + 1).join(" ") + value;
}
if (x !== undefined) {
  return x
}
if (padder instanceof StringPadder) {
    padder; // type narrowed to 'StringPadder'
}
Thread Thread
 
maxheiber profile image
Max Heiber

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/JS typeof. Type-level typeof returns a type. JS typeof returns a string.

const three = 3;

// all uses of `typeof` below are in type position.
// this is not the same as JS `typeof`
type Three = typeof three;
const x: typeof three = 3;
declare function foo(param: typeof three): void;

typescriptlang.org/play/?ssl=9&ssc...

Thread Thread
 
stereobooster profile image
stereobooster

Indeed you are right

type Three = typeof three;

if typeof is in type position this is TS thing. I never paid attention to this

Thread Thread
 
maxheiber profile image
Max Heiber

cool, huh!

It sneaks under the radar, since, as far as I can tell, there is no documentation on it.

Thread Thread
 
stereobooster profile image
stereobooster • Edited

But the error is the same as if you try to use type in place of value

type Test = 1
const x = typeof Test;
// the same as 
console.log(Test);
 // 'Test' only refers to a type, but is being used as a value here.(2693)

So it seems like type checker itself doesn't differentiate TS and JS versions of typeof and always returns error for JS version.

Collapse
 
macsikora profile image
Pragmatic Maciej • Edited

The answer is that we can never ask about the types of types in TypeScript.

You can ask if type has a type(kind) at the type level also. You can do that by conditional type:

type IsKindOfString<T> = T extends string ? true : false
type ResultTrue =  IsKindOfString<"Hi I am type, don't confuse me with value"> // true
type ResultFalse = IsKindOfString<1> // false

Playground

Also my latest tweet about confusion in Type level and Value level operators:

Collapse
 
maxheiber profile image
Max Heiber • Edited

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 theory

  • extends is like subsetOf in set theory

  • TS'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.

"foo" // a value
Set{"foo"} // the singleton set containing the value "foo"
"foo" memberOf Set{"foo"} // true statement
Set{ the infinitude of strings } // set containing "foo", "blah blah blah", the Gettysburg address, etc.

Set{"foo"} subsetOf Set{ the infinitude of strings }   // true statement

Set{"foo"} memberOf  SetX // this is the kind of thing TS won't let us ask