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.
- 01Tell the two types apartSay when
Productmeans one object, and when you needtypeof Product, the type of the class itself. - 02Type static membersWrite static fields and methods, and read them from the class, not from its objects.
- 03Pass 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:
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:
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, andlabelwants one of its objects".typeof Productis 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:
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));Product: $undefinedProduct1 error: line 10Line 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.
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.
new; its type is typeof Product. Each object it makes has a name and a price; that shape is the type Product.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 istypeof 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
nameandprice - In real life: A box labelled "star cookies" takes cookies, not the cutter
- In TypeScript:
item: Producttakes objects, not the class
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:
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 showsconst Maker: typeof Product. - Line 7 calls
new Maker("Cake", 4).Makeris the class, so this makes a product: the query on line 8 showsconst cake: Product. - Line 9 prints
function 4. Thistypeofis 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,typeofis TypeScript's, and it gives a type.
Write typeof Product yourself when a function takes the class as a parameter:
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:
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.
| Question | The type Product | The 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:
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.
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:
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);first.shown1 error: line 14undefinedfirst 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.
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.
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
Widgetfor a page, with a label and arendermethod that gives back its HTML. Lines 8 to 12 makeButton, a subclass that renders a button instead (Class inheritance coversextends). - Line 14 reads:
Kindis something you can call withnewand one string,label, and it gives back aWidget. It looks like a function type withnewin 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:
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"));<button>Buy</button>ButtonNo errorsButton 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>.
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.
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:
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.
| Question | typeof Widget | new (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.
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
Widgetwith 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,
ButtonandLink, each with its ownrender. - 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
Kindand a label. PuttingWidgetitself 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 prints2, read from the class.
Now sort each card. Will TypeScript report an error, or is it 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;
Each card is checked on its own with this lesson's settings. Place it in the group TypeScript would put it in.
Common mix-ups
- "The type
Productmeans the class." No. It means one object made by the class. The class itself istypeof Product. - "A
Productmust be made withnew Product." No. Any object with the same fields and methods fits, like{ name: "Cake", price: 4 }. - "
typeofin a type is the same astypeofin 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, notfirst.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 callnewon 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.
A cart class with a limit. Which line will TypeScript mark?
class Cart {
static maxItems = 10;
items: string[] = [];
}
const cart = new Cart();
console.log(cart.maxItems);class Cart { static maxItems = 10; items: string[] = [];} const cart = new Cart();console.log(Cart.maxItems);maxItems is static, so cart, one object, doesn't have it. Line 7 gets Did you mean to access the static member 'Cart.maxItems' instead?. Cart.maxItems reads it from the class and prints 10.
What type does TypeScript give Maker? Guess first. Then point at Maker, or press Edit & run and add a // ^? line under it.
class Cart { items: string[] = [];} const Maker = Cart;class Cart { items: string[] = [];} const Maker = Cart;// ^?Line 5 stores the class, not an object, so the query on line 6 shows const Maker: typeof Cart.
This prints 1, but two toasts were shown. Change the counter so it counts every toast. What does your fixed code print?
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);class Toast { static shown = 0; constructor(public message: string) { Toast.shown += 1; }} const saved = new Toast("Saved");const deleted = new Toast("Deleted");console.log(Toast.shown);A static field is shared by the whole class, so both toasts add to the same Toast.shown. The code prints 2 instead of 1.
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?
class User { constructor(public name: string) {}} const visitor = User.guest();console.log(visitor.name);class User { constructor(public name: string) {} static guest() { return new User("Guest"); }} const visitor = User.guest();console.log(visitor.name);A static method belongs to the class, so User.guest() works. It makes a user named Guest, and the code prints Guest.
TypeScript marks line 8, because Kind has no type. Give it one. What does the code print?
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"]));class Chip { constructor(public label: string) {} render() { return `[${this.label}]`; }} function renderAll(Kind: new (label: string) => Chip, labels: string[]) { return labels.map((label) => new Kind(label).render()).join(" ");} console.log(renderAll(Chip, ["Tea", "Cake"]));The constructor signature says Kind takes a string label and makes a Chip, so new Kind(label).render() is checked. The code prints [Tea] [Cake].
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?
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"));abstract class Widget { constructor(public label: string) {} abstract render(): string;} class Button extends Widget { render() { return `<button>${this.label}</button>`; }} function mount(Kind: new (label: string) => Widget, label: string) { return new Kind(label).render();} console.log(mount(Button, "Buy"));A constructor signature only accepts classes you can call new on, so new Kind(label) is allowed. Button fits, and the code prints <button>Buy</button>.
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.
Question 1 of 7In
const tea: Product = new Product("Tea", 3);, what does the typeProductdescribe?Choose an answer to see the explanation.
Question 2 of 7What type does TypeScript give
Makeron line 5?Read the code, then predictclass Banner { constructor(public text: string) {} } const Maker = Banner;Choose an answer to see the explanation.
Question 3 of 7What does TypeScript say about this cart, and what does it print?
Read the code, then predictclass Cart { static maxItems = 10; } const cart = new Cart(); console.log(cart.maxItems);Choose an answer to see the explanation.
Question 4 of 7What does this tab counter print?
Read the code, then predictclass 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.
Question 5 of 7Which line does TypeScript flag when this code makes widgets?
Read the code, then predictclass 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.
Question 6 of 7What does
typeof Productmean in a type annotation, likeKind: typeof Product?Choose an answer to see the explanation.
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 Productis 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 callnewon 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.