cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Static members & class types

Learn what a class name means as a TypeScript type, how typeof gives the class its own type, and how to type static members and classes you pass around.

By the end, you can
  • 01
    Tell the two types apartSay when Product means one object, and when you need typeof Product, the type of the class itself.
  • 02
    Type static membersWrite static fields and methods, and read them from the class, not from its objects.
  • 03
    Pass classes aroundType a parameter that takes a class with a constructor signature like new (label: string) => Widget.

One class name, two types

A shop page shows a label for each product, like Tea: $3. Here is a Product class and a label function in plain JavaScript (Class basics covers classes). The last line passes the wrong thing:

A label for the class instead of a productPop out in the code editor (opens in a new tab)JavaScript
class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }
}

function label(item) {
  return `${item.name}: $${item.price}`;
}

console.log(label(new Product("Tea", 3)));
console.log(label(Product));

Lines 1 to 6 are the class: the constructor stores a name and a price on each new object. Lines 8 to 10 build a label from any item's name and price. Line 12 passes a real product and prints Tea: $3. Line 13 passes Product, the class itself, and prints Product: $undefined. Nothing warns you: the class has a name too (every function does, and the class's is the text "Product"), but no price.

Here is the same code in TypeScript, with the class shortened by parameter properties:

The same mistake with types (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
class Product {  constructor(public name: string, public price: number) {}} function label(item: Product) {  return `${item.name}: $${item.price}`;} console.log(label(new Product("Tea", 3)));console.log(label(Product));

Line 5 says label takes an item of type Product. When you use a class name as a type, it means one object made by the class: something with a name that is a string and a price that is a number. This is called the instance type, because an object made with new is an instance of the class. Line 9 passes one, and that's fine. Line 10 passes the class itself, and TypeScript marks it, on purpose:

  • Argument of type 'typeof Product' is not assignable to parameter of type 'Product' means "you passed the class, and label wants one of its objects". typeof Product is TypeScript's name for the type of the class itself; the next section covers it.
  • The second line says why they don't match: Property 'price' is missing in type 'typeof Product'.

Try each kind of value on line 10 and watch the list under the code:

Playground: what label gets on line 10
A label for a productPop out in the code editor (opens in a new tab)TypeScript
class Product {  constructor(public name: string, public price: number) {}} function label(item: Product) {  return `${item.name}: $${item.price}`;} console.log(label(new Product("Tea", 3)));console.log(label(Product));
Line 10 printsProduct: $undefined
What happens
label getsProduct
TypeScript1 error: line 10
Try it yourself

Line 10 passes the class itself. TypeScript says Argument of type 'typeof Product' is not assignable to parameter of type 'Product', and its second line says why: Property 'price' is missing in type 'typeof Product'. The class has a name (every function does: it's the text "Product"), but no price. So it prints Product: $undefined.

TypeScript checks the code for real, and the output is what line 10 prints. Each button changes only line 10. Reset goes back to The class.

A new Product and A plain object both work. The type Product doesn't care how an object was made, only that it has the right fields, as the Structural typing lesson showed. An object without a price fails for the same reason the class does.

Left, a window named Product holds the constructor, labelled typeof Product: the class itself. An arrow labelled new Product("Tea", 3) leads to a cart named tea holding name: "Tea" and price: 3, labelled Product: one object.the classProductconstructortypeof Productnew Product("Tea", 3)one objectname: "Tea"price: 3teaProduct
The class is a value you call with new; its type is typeof Product. Each object it makes has a name and a price; that shape is the type Product.
Real-life analogyA cookie cutter and its cookies

A baker has one star-shaped cookie cutter. Each press makes a new star cookie, and each cookie gets its own icing. The box labelled "star cookies" is for cookies. Putting the cutter in the box is a mistake, even though it's star-shaped too.

In real life: The cookie cutter
In TypeScript: The class, Product, whose type is typeof Product
In real life: Pressing the cutter into dough
In TypeScript: Calling new Product("Tea", 3)
In real life: Each cookie, with its own icing
In TypeScript: Each object, with its own name and price
In real life: A box labelled "star cookies" takes cookies, not the cutter
In TypeScript: item: Product takes objects, not the class
The main idea

A class gives you two types. The class name, Product, is the type of each object it makes. typeof Product is the type of the class itself: its constructor and its static members, the fields and methods that belong to the class instead of its objects.

The class's own type: typeof

A class is also a value: you can store it in a variable and pass it to a function. To write its type, TypeScript uses the word typeof inside a type. Here TypeScript works it out for you:

The class in a variablePop out in the code editor (opens in a new tab)TypeScript
class Product {  constructor(public name: string, public price: number) {}} const Maker = Product;//    ^?const cake = new Maker("Cake", 4);//    ^?console.log(typeof Product, cake.price);
  • Line 5 stores the class itself in Maker. The query on line 6 shows const Maker: typeof Product.
  • Line 7 calls new Maker("Cake", 4). Maker is the class, so this makes a product: the query on line 8 shows const cake: Product.
  • Line 9 prints function 4. This typeof is JavaScript's, in running code: it gives back text that names a kind of value, and a class is a "function" (Checking types covers it). In a type, after a colon, typeof is TypeScript's, and it gives a type.

Write typeof Product yourself when a function takes the class as a parameter:

A function that takes the classPop out in the code editor (opens in a new tab)TypeScript
class Product {  constructor(public name: string, public price: number) {}} function makeFree(Kind: typeof Product, name: string) {  return new Kind(name, 0);} const sample = makeFree(Product, "Tea sample");//    ^?console.log(sample.name, sample.price);

Line 5 says Kind is the class Product, not one product. So line 6 can call new Kind(name, 0), and TypeScript checks the arguments against Product's constructor: a string and a number. Line 9 passes the class, and the query on line 10 shows that sample is a Product. Line 11 prints Tea sample 0.

A class can only be called with new, and its type says so:

Calling a class without new (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
class Product {  constructor(public name: string, public price: number) {}} const tea = Product("Tea", 3);console.log(tea);

Line 5 forgets new. TypeScript says Value of type 'typeof Product' is not callable. Did you mean to include 'new'? In plain words: the class can't be called like a normal function. Run it anyway and JavaScript agrees: TypeError: Class constructor Product cannot be invoked without 'new'.

The table compares the two types. Read each row as a question, then the answer for each type.

The type Product and the type typeof Product
QuestionThe type ProductThe type typeof Product
What does it describe?One object made by the class: its fields and methods.The class itself: its constructor and its static members.
Which values have it?new Product("Tea", 3), or any object with the same shape.Product itself, and any value with the same constructor and static members.
Can you call new on it?No. An object isn't a constructor.Yes. new Kind("Cake", 4) gives a Product.
Where are static members?Not here. Objects don't have them.Here. Toast.shown is part of typeof Toast.

Static members belong to the class

A static field or method belongs to the class itself, not to each object. There is only one of it, and you reach it through the class name (Static members covers how they work in JavaScript). TypeScript types them like any other member. Here a toast message class counts how many toasts it has shown:

A toast class with a static counter and a static methodPop out in the code editor (opens in a new tab)TypeScript
class Toast {  static shown = 0;   constructor(public message: string) {    Toast.shown += 1;  }   static saved() {    return new Toast("Saved");  }} const first = Toast.saved();//    ^?const second = new Toast("Deleted");console.log(first.message, second.message, Toast.shown);
  • Line 2 is a static field: one shown, on the class, starting at 0. TypeScript works out that it's a number.
  • Line 5 runs in the constructor, once for each new toast, and adds 1 to Toast.shown, through the class name.
  • Lines 8 to 10 are a static method. Toast.saved() makes a toast with a ready-made message. A method like this, which makes objects for you, is often called a factory.
  • The query on line 14 shows const first: Toast: TypeScript works out the factory's return type.
  • Line 16 prints Saved Deleted 2: each toast's own message, then the one shared count.
At the top, a window named Toast holds shown: 2 and the method saved(), labelled on the class. Arrows lead down to two carts, first holding "Saved" and second holding "Deleted", labelled one message on each object.Toastshown: 2saved()on the class"Saved"first"Deleted"secondone messageon each object
shown and saved() are static, so there is one of each, on the class: Toast.shown. Each object has only its own message, so first.shown doesn't exist.

Static members are part of typeof Toast, the class's own type, and not of Toast, the type of each object. Type what line 14 prints, and watch which reads TypeScript accepts:

Playground: read from the class or from an object
A toast class with a static counterPop out in the code editor (opens in a new tab)TypeScript
class Toast {  static shown = 0;   constructor(public message: string) {    Toast.shown += 1;  }   static saved() {    return new Toast("Saved");  }} const first = Toast.saved();console.log(first.shown);
Line 14 readsfirst.shown
What happens
TypeScript1 error: line 14
It printsundefined
Try it yourself

first is a Toast object, and shown belongs to the class, not to its objects. TypeScript says Property 'shown' does not exist on type 'Toast'. Did you mean to access the static member 'Toast.shown' instead?. When it runs, it prints undefined.

The real TypeScript compiler checks the code each time you type. Only line 14 changes. Reset puts first.shown back.

first.shown gets Property 'shown' does not exist on type 'Toast'. Did you mean to access the static member 'Toast.shown' instead? TypeScript even suggests the fix. Toast.message is the mirror mistake: message belongs to each object, so the class doesn't have it. Both print undefined when they run.

Constructor signatures: any class you can call new on

typeof Product describes one particular class. Often a function should take any class that can make the kind of object it needs. For that, write a constructor signature: new, then the constructor's parameters, then an arrow and the type of object it makes.

A mount function that takes any widget classPop out in the code editor (opens in a new tab)TypeScript
class Widget {  constructor(public label: string) {}  render() {    return `<span>${this.label}</span>`;  }} class Button extends Widget {  render() {    return `<button>${this.label}</button>`;  }} function mount(Kind: new (label: string) => Widget, label: string) {  const widget = new Kind(label);  return widget.render();} console.log(mount(Button, "Buy"));console.log(mount(Widget, "Sale"));
  • Lines 1 to 6 make a Widget for a page, with a label and a render method that gives back its HTML. Lines 8 to 12 make Button, a subclass that renders a button instead (Class inheritance covers extends).
  • Line 14 reads: Kind is something you can call with new and one string, label, and it gives back a Widget. It looks like a function type with new in front.
  • Line 15 calls new Kind(label), and line 16 renders the widget.
  • Line 19 prints <button>Buy</button>, and line 20 prints <span>Sale</span>. Both classes fit.

Now try some values that don't fit. This time Widget is abstract, and a Badge class takes a number:

Playground: what mount gets on line 23
Mounting a widget from its classPop out in the code editor (opens in a new tab)TypeScript
abstract class Widget {  constructor(public label: string) {}  abstract render(): string;} class Button extends Widget {  render() {    return `<button>${this.label}</button>`;  }} class Badge {  constructor(public count: number) {}  render() {    return `<b>${this.count}</b>`;  }} function mount(Kind: new (label: string) => Widget, label: string) {  return new Kind(label).render();} console.log(mount(Button, "Buy"));
When it runs<button>Buy</button>
What happens
mount getsButton
TypeScriptNo errors
Try it yourself

Button can be called with new and a string label, and it makes a Widget. It fits new (label: string) => Widget, so there are no errors, and it prints <button>Buy</button>.

TypeScript checks the code for real, and the output is what this code does when it runs. Each button changes only line 23. Reset goes back to Button.

Badge has a render method, but its constructor takes a number, and its objects have no label, so it doesn't fit. Widget is abstract, and an abstract class can't be called with new, so a constructor signature turns it away. An arrow function makes a Button when you call it, but it can't be called with new.

At the top, mount accepts new (label: string) => Widget. Below, Button has constructor(label: string) and fits. Badge has constructor(count: number) and doesn't fit.what mount acceptsnew (label: string) => WidgetButtonconstructor(label: string)fitsBadgeconstructor(count: number)takes a number: no fit
mount accepts any class whose constructor takes a string label and makes a Widget. Button fits. Badge's constructor takes a number, so it doesn't.

You can also write a constructor signature inside an interface, without the arrow. This is handy when you want to give the type a name:

A constructor signature in an interfacePop out in the code editor (opens in a new tab)TypeScript
class Chip {  constructor(public label: string) {}} interface ChipClass {  new (label: string): Chip;} const Kind: ChipClass = Chip;const tea = new Kind("Tea");console.log(tea.label);

Lines 5 to 7 say a ChipClass is anything you can call with new and a string to get a Chip. Line 9 stores the Chip class with that type, line 10 makes a chip, and line 11 prints Tea.

The table compares typeof Widget with a constructor signature. Read each row as a question, then the answer for each.

typeof Widget and new (label: string) => Widget
Questiontypeof Widgetnew (label: string) => Widget
Which classes fit?Widget, and classes with the same constructor and static members.Any class whose constructor takes a string label and makes a Widget.
Does it include static members?Yes, every static member of Widget.No. It only describes calling new.
Can the function call new on it when Widget is abstract?No: Cannot create an instance of an abstract class.Yes. That's why it turns abstract classes away.

A toolbar built from widget classes

Here is everything together: a page builder that describes a toolbar as a list of widget classes and labels, builds each widget, and counts how many it made.

A toolbar from a list of classesPop out in the code editor (opens in a new tab)TypeScript
abstract class Widget {  static created = 0;   constructor(public label: string) {    Widget.created += 1;  }   abstract render(): string;} class Button extends Widget {  render() {    return `<button>${this.label}</button>`;  }} class Link extends Widget {  render() {    return `<a href="#">${this.label}</a>`;  }} type WidgetClass = new (label: string) => Widget; const toolbar: { Kind: WidgetClass; label: string }[] = [  { Kind: Button, label: "Buy" },  { Kind: Link, label: "Help" },]; for (const item of toolbar) {  console.log(new item.Kind(item.label).render());}console.log(Widget.created);
  • Lines 1 to 9 are an abstract Widget with a static counter. Line 5 adds 1 for every widget made, whatever its subclass, because each subclass runs this constructor.
  • Lines 11 to 21 are two widgets, Button and Link, each with its own render.
  • Line 23 gives the constructor signature a name with a type alias: WidgetClass.
  • Lines 25 to 28 list the toolbar. Each entry holds a class in Kind and a label. Putting Widget itself there would be an error, because it's abstract.
  • Lines 30 to 32 make each widget and print <button>Buy</button> and <a href="#">Help</a>. Line 33 prints 2, read from the class.

Now sort each card. Will TypeScript report an error, or is it fine?

Error or fine?
  • class Cart { static max = 10 } console.log(Cart.max);
  • class Cart { static max = 10 } console.log(new Cart().max);
  • class Cart { items = 0 } const cart: Cart = new Cart();
  • class Cart { items = 0 } const cart: Cart = Cart;
  • class Cart { items = 0 } const Maker: typeof Cart = Cart;
  • class Cart { items = 0 } const cart: Cart = { items: 2 };
  • class Cart { items = 0 } const cart = Cart();
  • class Cart { constructor(public owner: string) {} } const make: new (owner: number) => Cart = Cart;
Try it yourself
0 of 8 correct

Each card is checked on its own with this lesson's settings. Place it in the group TypeScript would put it in.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "The type Product means the class." No. It means one object made by the class. The class itself is typeof Product.
  • "A Product must be made with new Product." No. Any object with the same fields and methods fits, like { name: "Cake", price: 4 }.
  • "typeof in a type is the same as typeof in code." No. In code, JavaScript gives text such as "function" while the page runs. In a type, TypeScript gives the type of a value.
  • "Each object gets a copy of the static fields." No. There is one, on the class. Read it as Toast.shown, not first.shown.
  • "Any class with the right methods fits a constructor signature." No. The constructor's parameters must match too, so Badge, which takes a number, doesn't fit.
  • "An abstract class fits new (label: string) => Widget." No. You can't call new on an abstract class, so TypeScript turns it away.

Practice exercises

Each exercise is TypeScript. Press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.

Exercise 1 · Warm-upFind the line TypeScript marks

A cart class with a limit. Which line will TypeScript mark?

Starter codePop out in the code editor (opens in a new tab)TypeScript
class Cart {
  static maxItems = 10;
  items: string[] = [];
}

const cart = new Cart();
console.log(cart.maxItems);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upRead the class's type

    What type does TypeScript give Maker? Guess first. Then point at Maker, or press Edit & run and add a // ^? line under it.

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    class Cart {  items: string[] = [];} const Maker = Cart;

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCount every toast

      This prints 1, but two toasts were shown. Change the counter so it counts every toast. What does your fixed code print?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      class Toast {  shown = 0;   constructor(public message: string) {    this.shown += 1;  }} const saved = new Toast("Saved");const deleted = new Toast("Deleted");console.log(deleted.shown);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeAdd a guest factory

        A profile page needs a user for visitors who haven't signed in. Add the method line 5 calls. What does the fixed code print?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        class User {  constructor(public name: string) {}} const visitor = User.guest();console.log(visitor.name);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeType a class parameter

          TypeScript marks line 8, because Kind has no type. Give it one. What does the code print?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          class Chip {  constructor(public label: string) {}  render() {    return `[${this.label}]`;  }} function renderAll(Kind, labels: string[]) {  return labels.map((label) => new Kind(label).render()).join(" ");} console.log(renderAll(Chip, ["Tea", "Cake"]));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeAccept classes that can be made

            Line 13 gets Cannot create an instance of an abstract class. Change the type of Kind so the error goes. What does the fixed code print?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            abstract class Widget {  constructor(public label: string) {}  abstract render(): string;} class Button extends Widget {  render() {    return `<button>${this.label}</button>`;  }} function mount(Kind: typeof Widget, label: string) {  return new Kind(label).render();} console.log(mount(Button, "Buy"));

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask whether a name means the class or one of its objects. The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The Static members and class types quiz · 7 questionsScore: first tries count
              1. Question 1 of 7In const tea: Product = new Product("Tea", 3);, what does the type Product describe?

                Choose an answer to see the explanation.

              2. Question 2 of 7What type does TypeScript give Maker on line 5?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                class Banner {
                  constructor(public text: string) {}
                }
                
                const Maker = Banner;

                Choose an answer to see the explanation.

              3. Question 3 of 7What does TypeScript say about this cart, and what does it print?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                class Cart {
                  static maxItems = 10;
                }
                
                const cart = new Cart();
                console.log(cart.maxItems);

                Choose an answer to see the explanation.

              4. Question 4 of 7What does this tab counter print?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                class Tab {
                  static opened = 0;
                
                  constructor(public title: string) {
                    Tab.opened += 1;
                  }
                }
                
                new Tab("Home");
                new Tab("Cart");
                new Tab("Help");
                console.log(Tab.opened);

                Choose an answer to see the explanation.

              5. Question 5 of 7Which line does TypeScript flag when this code makes widgets?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                class Button {
                  constructor(public label: string) {}
                }
                
                class Badge {
                  constructor(public count: number) {}
                }
                
                function make(Kind: new (label: string) => { label: string }) {
                  return new Kind("Buy");
                }
                
                make(Button);
                make(Badge);

                Choose an answer to see the explanation.

              6. Question 6 of 7What does typeof Product mean in a type annotation, like Kind: typeof Product?

                Choose an answer to see the explanation.

              7. Question 7 of 7Which parameter type accepts every class whose constructor takes a string label and makes a Widget?

                Choose an answer to see the explanation.

              Key takeaways

              • A class name used as a type, like item: Product, means one object with the class's shape.
              • typeof Product is the type of the class itself: its constructor and its static members.
              • Static fields and methods belong to the class. Read them as Toast.shown; objects don't have them.
              • A constructor signature, new (label: string) => Widget, accepts any class you can call new on with those arguments, but not abstract classes.
              • A class must be called with new, and TypeScript marks a call without it.

              Remember the one-liner.
              Product is one object; typeof Product is the class that makes them, and static members live on the class.

              Coming next: Generic functions. It shows how to write a function like first(list) that gives back the same type it was given, whatever that type is.

              CompleteFrontend Clear concepts. Working examples.