TypeScript - What is a class?

In TypeScript, a class is really two types and a function. One type is the type of the function itself, and the other type is the type of the object returned when you call the function. Try this:

class C {
    foo() {}
}

const c1: typeof C = C;
const c2: C = new C();
const c3: C = C;

Note that c3 is a type error. C is not of type C. C is a function, a named object that exists at runtime, and like any identifier its type can be written as typeof C, so c1 has to work. But at compile time, C is an interface, conformed to by the object we get back from new C(), so that’s why c2 works.

Interfaces are used in TypeScript to describe anything that has properties. In JavaScript a function can have properties of its own, so the definitive way to describe a function is with an interface. This lambda-like style:

type F = (arg1: number, arg2: string) => string;

is an abbreviation for:

interface F {
    (arg1: number, arg2: string): string;
}

It looks just like a method, except without a name. So if we have f that implements F, we can directly call it:

f(5, "hi");

But it may be that we can also say f.blah(), because F also has a blah property that happens to be another function:

interface F {
    (num: number, str: string): { num: number; str: string };
    blah(): void;
}

Another thing we can only do with the interface style of function-type is to define multiple ways to call the function:

interface F {
    (num: number, str: string): { num: number; str: string };
    (num: number): boolean;
}

And finally (especially relevant here), we can say that the function must be called with the new prefix:

interface F {
    new (num: number, str: string): { num: number; str: string };
}

Though we can also do that with the lambda-like version:

type F = new (num: number, str: string) => { num: number; str: string };

Putting these pieces together, suppose we have class

class C {
    i = 5;
    static s = "hi";
}

// Usage:
console.log(C.s);
console.log(new C().i);

We can completely describe the type of C “by hand” like this:

interface TypeOfC {
    new (): {
        i: number;
    };

    s: string;
}

So TypeOfC describes a new-able function, because we declare that it has new-able function with no name. s is a property of the function, whereas i is a property of the objects created when you call the function.

We can declare a const of type TypeOfC and assign C to it:

// Compatible:
const c2: TypeOfC = C;

// Usage:
console.log(c2.s);
console.log(new c2().i);

Now, we can abbreviate our TypeOfC re-declaration because TypeScript already gives a name to the type returned from the function: it’s called C:

interface TypeOfC {
    new (): C;
    s: string;
}

Recall what we established right at the start: when you write a class C, you give two meanings to that name. In the namespace of types, you create a type C that describes the objects returned by new C(). In the namespace of runtime objects, you create a function C. TypeScript never confuses the two meanings because every spot in your code is unambiguously referring to either a compile-time type or a runtime object.

And we don’t need to declare TypeOfC at all, because we can just use TypeScript’s built-in feature for getting the type of a named object: typeof C. In JavaScript, typeof takes an expression and returns a string describing its type (to an extent…) In TypeScript that is still exactly the same, of course, but also typeof can be used in type declarations to get the type of an expression.

Hence:

// Compatible:
const c2: typeof C = C;

// Usage:
console.log(c2.s);
console.log(new c2().i);



Not yet regretting the time you've spent here?

Keep reading:

  • Vectors - Intuitions
  • Poorly Structured Notes on AI Part 4
  • Poorly Structured Notes on AI Part 3
  • Poorly Structured Notes on AI Part 2
  • Poorly Structured Notes on AI Part 1