Classes in TypeScript
Type a class's fields, constructor, methods, and getters, see why every field needs a starting value, and shorten constructors with parameter properties.
- 01Declare typed fieldsWrite each field and its type in the class body, and read what TypeScript works out from a starting value.
- 02Give every field a valueFix
strictPropertyInitializationerrors with a starting value, the constructor,?, or (carefully)!. - 03Write shorter, typed classesTurn constructor parameters into fields with parameter properties, and type methods, getters, and setters.
A class that knows its types
A shop page makes a Product object for every item it sells. Here is a JavaScript class that does it. A class is a recipe for objects: new Product(…) runs its constructor and gives back a new object (Class basics covers this). One price comes from a form, so it's text:
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
const tea = new Product("Tea", "3");
console.log(tea.price + 1);Lines 2 to 5 are the constructor. It stores the two arguments on the new object as this.name and this.price. Line 8 makes a tea that costs "3", in quotes, so the price is text. Line 9 adds 1 and prints 31, because + joins text instead of adding. Nothing warns you.
Here is the same class in TypeScript. Two lines are new, and the constructor's parameters have types:
class Product { name: string; price: number; constructor(name: string, price: number) { this.name = name; this.price = price; }} const tea = new Product("Tea", "3");console.log(tea.price + 1);Lines 2 and 3 are field declarations. They list the properties every Product has, and the type of each: name is a string (text) and price is a number. Line 5 types the constructor's parameters the same way. Lines 6 and 7 store them, as before.
Line 11 gets an error, on purpose, because it's the bug:
Argument of type 'string' is not assignable to parameter of type 'number'means "you're passing text where the constructor wants a number". An argument is a value you pass in a call, andnew Product(…)is a call to the constructor.- The code still runs and prints
31. TypeScript only warns you; it removes the types and the browser runs the rest.
Try it: type a different price for line 11 and watch the list under the code.
class Product { name: string; price: number; constructor(name: string, price: number) { this.name = name; this.price = price; }} const tea = new Product("Tea", "3");console.log(tea.price + 1);"3"1 error: line 11prints 31Line 11 passes the text "3" where the constructor wants a number. TypeScript says Argument of type 'string' is not assignable to parameter of type 'number'. When it runs, "3" + 1 joins the two and prints 31.
With 3 or Number("3"), the price is a number, so the error goes away and the code prints 4. An empty box and 3, 2 get a different error: the constructor takes exactly two arguments, a name and a price, and TypeScript counts them.
Product has a name that is text and a price that is a number. Each object new makes has those two fields, and TypeScript checks the values that go in.A shop prints one order form with labeled boxes: Name, and Price (numbers only). Every customer fills in a fresh copy. Before the clerk takes a copy, she checks each box. If someone wrote letters in the price box, she hands it back to be fixed, before anything is ordered.
- In real life: The blank form, printed once
- In TypeScript: The class,
class Product { … } - In real life: Each box on the form, with its label: Name, Price (numbers only)
- In TypeScript: Each field declaration:
name: string,price: number - In real life: A filled-in copy of the form
- In TypeScript: One object made by
new Product("Tea", 3) - In real life: The clerk who hands back a form with letters in the price box
- In TypeScript: TypeScript marking
"3"where a number goes
A TypeScript class is a JavaScript class with types. You declare each field and its type in the class body, and type the parameters of the constructor and the methods. TypeScript checks every new call and every use of the objects. Then the types are removed, and the class runs as plain JavaScript.
Declaring fields
In JavaScript, a constructor can add any property it likes with this.something = …. TypeScript wants to know an object's properties before the constructor runs, so it can check every use of them. Here is what happens if you skip the declaration:
class Cart { constructor() { this.total = 0; }}Line 3 stores 0 in this.total. TypeScript says Property 'total' does not exist on type 'Cart': it looked at the class body for a total and found none. The fix is to declare it. A field declaration can have a type, a starting value, or both:
class Cart { items: string[] = []; total = 0; coupon?: string;} const cart = new Cart();const total = cart.total;// ^?const coupon = cart.coupon;// ^?console.log(total, coupon);- Line 2 has a type and a starting value:
itemsis a list of strings (string[]) and starts empty. - Line 3 has only a starting value. TypeScript works out the type from it, just as it does for a variable. The query on line 9 shows
const total: number. - Line 4 has a question mark:
coupon?: stringmakes the field optional. It may hold a string or nothing at all, so the query on line 11 showsstring | undefined. The|means "or".
Line 12 prints 0 undefined: the total starts at 0 and nobody has set a coupon yet. A field line is still JavaScript after the types are removed: total = 0; gives every new cart its own total, as a JavaScript field does.
A field with neither a type nor a starting value gives TypeScript nothing to work with:
class Cart { items; total = 0;}Line 2 declares items and says nothing else. TypeScript reports Member 'items' implicitly has an 'any' type. The type any means "anything at all, and don't check it". With TypeScript's strict setting, which this lesson's examples use, TypeScript won't pick any for you silently. Write items: string[] = []; to say what it holds.
Every field needs a starting value
A field declared as email: string promises that email is always a string. TypeScript checks that promise: each field whose type doesn't allow undefined must get a value, either where it's declared or in the constructor. This check is called strictPropertyInitialization. It's a compiler setting, and the strict setting turns it on.
class UserProfile { name: string; email: string; constructor(name: string) { this.name = name; }} const profile = new UserProfile("Asha");console.log(profile.email.toUpperCase());Lines 2 and 3 declare name and email. The constructor sets name on line 6, and nothing sets email. TypeScript marks line 3: Property 'email' has no initializer and is not definitely assigned in the constructor. In plain words: this field has no starting value (an initializer), and the constructor doesn't always set it. When the code runs, line 11 calls toUpperCase() on undefined and stops with a TypeError.
name gets a value. When the code runs, the profile's email is undefined, which is what the error on line 3 warned about.There are four common ways to write line 3. Press each one and watch the errors and the output. The code prints the email with a template literal, so nothing throws this time:
class UserProfile { name: string; email: string; constructor(name: string) { this.name = name; }} const profile = new UserProfile("Asha");console.log(`Email: ${profile.email}`);Email: undefinedemail: string;1 error: line 3Line 3 says email is always a string, but nothing gives it a value. TypeScript says Property 'email' has no initializer and is not definitely assigned in the constructor. When it runs, email is undefined, and the page prints Email: undefined.
A starting value fixes the bug. The ? is honest too: it changes the type, so any code that uses email as text has to check for undefined first. The ! after a field name is called a definite assignment assertion. It removes the error without giving a value, so use it only when something TypeScript can't see really does set the field, like a library that fills it in.
The table puts the four side by side. Read each row as: what you write, what TypeScript then knows, and what the field holds.
| When you write | What TypeScript knows | What the field holds at first |
|---|---|---|
email = "not set"; | email is a string, worked out from the value. | The text "not set", from the very start. |
email: string; and this.email = … in the constructor | email is a string. TypeScript checked that every path through the constructor sets it. | Whatever the constructor stored. |
email?: string; | email is string | undefined, so you must check before using it as text. | undefined, until something sets it. |
email!: string; | email is a string, because you promised. TypeScript stops checking. | undefined, until something sets it. If nothing does, the promise was wrong. |
The constructor can set a field too. TypeScript follows every path through it:
class UserProfile { name: string; email: string; constructor(name: string, email?: string) { this.name = name; if (email) { this.email = email; } else { this.email = "not set"; } }} const profile = new UserProfile("Asha");console.log(profile.email);Line 5 takes an optional email parameter. Lines 7 to 11 set this.email in both branches of the if: the given email, or "not set". Every path sets it, so there's no error. Line 15 passes no email, and line 16 prints not set. Delete the else branch in Edit & run and the error on line 3 comes back.
TypeScript only looks inside the constructor itself, not inside the methods it calls:
class UserProfile { name: string; constructor() { this.reset(); } reset() { this.name = "Guest"; }}Line 5 calls reset(), and line 9 sets name. You can see that it works, but TypeScript still marks line 2. It doesn't follow method calls, because a subclass (a class that extends this one, as Class inheritance shows) can replace reset() with a version that doesn't set name. Set the field in the constructor, give it a starting value, or, if you're sure, write name!: string.
Projects that turn the check off set strictPropertyInitialization to false in their tsconfig.json, the file that holds a project's compiler settings. This example sets it, and the error disappears while the bug stays:
class UserProfile { name: string; email: string; constructor(name: string) { this.name = name; }} const profile = new UserProfile("Asha");console.log(profile.email);It's the profile from the start of this section, with no errors at all. Line 11 prints undefined, even though line 3 says email is a string. Keep the check on.
Parameter properties: fields from the constructor
Look back at the overview's class. Each field is written three times: once in the class body (name: string), once as a parameter (name: string), and once in the copy (this.name = name). TypeScript has a shortcut for this, called a parameter property: put public in front of a constructor parameter, and it becomes a field too.
class Product { constructor(public name: string, public price: number) {}} const tea = new Product("Tea", 3);const price = tea.price;// ^?console.log(tea.name, price);Line 2 is the whole class body. public name: string means three things: the constructor takes a name, every Product has a name field, and the argument is stored in it. The braces {} at the end are the constructor's empty body. The query on line 7 shows const price: number, and line 8 prints Tea 3.
public means anyone can use the field, which is already the default for fields. The next lesson covers private, protected, and readonly, which work here as well. Unlike the type annotations, a parameter property writes JavaScript. Here is what TypeScript gives the browser:
public in front of each parameter. The JavaScript gets a field for each one and a line in the constructor that copies the argument into it.class Product {
name;
price;
constructor(name, price) {
this.name = name;
this.price = price;
}
}
const tea = new Product("Tea", 3);
const price = tea.price;
console.log(tea.name, price);Lines 2 and 3 are the fields, and lines 5 and 6 copy the arguments into them. That's the same class the long way, written for you. Line 11 prints Tea 3.
Each parameter needs its own public. Press the buttons to change line 2:
class Product { constructor(public name: string, public price: number) {}} const tea = new Product("Tea", 3);console.log(`${tea.name}: $${tea.price}`);Tea: $3No errorsBoth parameters have public, so both become fields. tea.name and tea.price exist, and line 6 prints Tea: $3.
A parameter with no modifier is a plain parameter: the constructor gets the value, and the object doesn't keep it. With public on name only, tea.price doesn't exist, and line 6 gets an error. With No modifiers, neither does tea.name, so line 6 has two errors.
The table compares the long way and the shortcut. Read each row as a question, then each way's answer.
| Question | The long way | Parameter properties |
|---|---|---|
| Where the fields are listed | In the class body: name: string; and price: number;. | In the constructor's brackets: public name: string. |
| What the constructor body does | Copies each parameter: this.name = name;. | Nothing. The body can be empty: {}. |
| What the browser runs | The same class, without the types. | The same class as the long way: TypeScript writes the fields and the copies for you. |
| Tools that only remove types | Run it. | Can't run it. erasableSyntaxOnly reports it. |
Most TypeScript features vanish when the types are removed. Parameter properties don't: something has to write the fields and the copies. Node.js can run a .ts file by deleting its types, and it stops with an error at a parameter property. Projects run that way turn on the erasableSyntaxOnly setting (TypeScript 5.8 and later), which marks them:
class Product { constructor(public name: string) {}}This example sets erasableSyntaxOnly, so line 2 gets This syntax is not allowed when 'erasableSyntaxOnly' is enabled. The fix is the long way: a field in the class body and a copy in the constructor. Without that setting, as in the rest of this lesson, parameter properties are fine.
Methods, getters, and setters
A method is a function that belongs to the class. You type its parameters like any function's, and you can write the type of value it gives back, its return type, after the brackets:
class Product { constructor(public name: string, public price: number) {} label(): string { return `${this.name}: $${this.price}`; } discounted(percent: number) { return this.price - (this.price * percent) / 100; }} const tea = new Product("Tea", 4);const sale = tea.discounted(25);// ^?console.log(tea.label(), sale);- Line 4 says
labelreturns astring. If line 5 returned something else, TypeScript would mark it. - Inside a method,
thisis the object the method was called on, so TypeScript knowsthis.priceis a number. - Line 8 types
percentand leaves the return type out. TypeScript works it out from line 9: the query on line 15 showsconst sale: number. - Line 16 prints
Tea: $4 3: the label, then 4 minus 25% of 4.
A getter is a method that you read like a field: get total() runs each time code reads line.total (Getters & setters covers how they work in JavaScript). TypeScript adds one rule: a getter with no setter is read-only.
class CartLine { constructor(public name: string, public price: number, public quantity: number) {} get total() { return this.price * this.quantity; }} const line = new CartLine("Tea", 3, 2);line.total = 10;console.log(line.total);Line 2 uses parameter properties for the name, price, and quantity. Lines 4 to 6 are the getter: it multiplies the price by the quantity. Line 10 tries to set line.total, and TypeScript says Cannot assign to 'total' because it is a read-only property. JavaScript has nowhere to store the 10. In this lesson's editor, line 10 does nothing and line 11 prints 6. In strict mode, which every JavaScript module uses, line 10 throws a TypeError instead.
line.total looks like a field, but reading it runs get total(), which multiplies the price by the quantity each time. With no setter, TypeScript treats total as read-only.A setter, written set, runs when code assigns to the property. It's a good place to clean up a value before you store it:
class QuantityField { count = 1; get quantity() { return this.count; } set quantity(value) { this.count = Math.max(1, Math.round(value)); }} const field = new QuantityField();field.quantity = 2.6;console.log(field.quantity);field.quantity = -4;console.log(field.quantity);field.quantity = "5";- Line 2 stores the real number in a field called
count. - Lines 4 to 6 are the getter. It returns
count, so TypeScript works out thatquantityis a number. - Line 8's
valuehas no type written. When a setter's parameter has no type, TypeScript takes the getter's return type: point atvalueand you see(parameter) value: number. - Line 9 rounds the value and keeps it at least 1. Line 14 sets 2.6, and line 15 prints
3. Line 16 sets -4, and line 17 prints1. - Line 18 assigns the text
"5". The setter takes a number, so TypeScript marks it, on purpose:Type 'string' is not assignable to type 'number'.
Any code can still change count directly and skip the setter's checks. The next lesson shows how private stops that.
A shopping cart class
Here is everything together: a cart for a shop page, with a typed list of items, a parameter property, a method, and two getters. Line 1 is a type alias: type CartItem = … gives a name to the shape of one item.
type CartItem = { name: string; price: number; quantity: number }; class ShoppingCart { items: CartItem[] = []; constructor(public owner: string) {} add(name: string, price: number, quantity = 1) { this.items.push({ name, price, quantity }); } get count() { return this.items.length; } get total() { return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0); }} const cart = new ShoppingCart("Asha");cart.add("Tea", 3, 2);cart.add("Cake", 4);console.log(`${cart.owner} has ${cart.count} items: $${cart.total}`);- Line 4 declares
items, a list ofCartItemobjects that starts empty. - Line 6 makes
ownera field with a parameter property. - Line 8 types
add's parameters.quantity = 1is a default value, so TypeScript works out that it's a number and that callers may leave it out. - Lines 12 to 18 are two getters.
totaladds up price times quantity withreduce, which combines a list into one value (map, filter & reduce covers it). - Line 24 prints
Asha has 2 items: $10: two teas at 3 and one cake at 4.
Because the class has types, every mistake in using it is marked. Here are four uses; three get errors on purpose:
type CartItem = { name: string; price: number; quantity: number }; class ShoppingCart { items: CartItem[] = []; constructor(public owner: string) {} add(name: string, price: number, quantity = 1) { this.items.push({ name, price, quantity }); }} const cart = new ShoppingCart("Asha");cart.add("Tea", "3");cart.add("Cake");cart.items = [];cart.owner = 7;- Line 14 passes the price as text:
Argument of type 'string' is not assignable to parameter of type 'number'. - Line 15 leaves out the price:
Expected 2-3 arguments, but got 1. The quantity is optional, soaddtakes two or three. - Line 16 empties the list. That's allowed: an empty list is still a list of
CartItem. - Line 17 sets the owner to a number:
Type 'number' is not assignable to type 'string'.
Now sort each line. Will TypeScript report an error, or is it fine?
class Cart { total: number; }class Cart { total = 0; }class Cart { total?: number; }class Cart { constructor() { this.total = 0; } }class Tag { constructor(public label: string) {} }class Line { get total() { return 6; } } new Line().total = 7;class Cart { total!: number; }class Cart { items; }
Each card is one class, checked on its own with this lesson's settings. Place it in the group TypeScript would put it in.
Common mix-ups
- "The
!gives a field a value." No. It only tells TypeScript to stop checking that field. The field is stillundefineduntil some code sets it. - "A parameter needs
publicto be used inside the constructor." No. Any parameter can be used in the constructor.publicalso makes it a field on the object. - "TypeScript checks the values passed to
newwhile the page runs." No. The types are removed first. A price that arrives as text from a server gets into aProductwithout a warning. - "Setting a field in a method the constructor calls is enough." No. TypeScript only looks inside the constructor itself, so the field still gets an error.
- "A getter needs a setter." No. A getter on its own is fine. It makes a property you can read but not set.
- "Field declarations are only types and disappear." No. The type after the colon disappears, but the field itself stays in the JavaScript, like
total = 0;.
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.
An order class with three fields. Which line will TypeScript mark?
class Order {
id: number;
customer: string;
paid = false;
constructor(id: number) {
this.id = id;
}
}class Order { id: number; customer: string; // line 3: nothing gives it a value paid = false; constructor(id: number) { this.id = id; }}customer on line 3 must be a string, but it has no starting value and the constructor never sets it. TypeScript reports Property 'customer' has no initializer and is not definitely assigned in the constructor.
What type does TypeScript give size? Guess first. Then point at size, or press Edit & run and add a // ^? line under it.
class Settings { theme = "dark"; fontSize = 16;} const size = new Settings().fontSize;class Settings { theme = "dark"; fontSize = 16;} const size = new Settings().fontSize;// ^?fontSize = 16 gives the field the type number, worked out from 16. Reading it gives a number, so the query on line 7 shows const size: number.
Rewrite the class with parameter properties, so it does the same thing in fewer lines. What does it print?
class Product { name: string; price: number; constructor(name: string, price: number) { this.name = name; this.price = price; }} const cake = new Product("Cake", 4);console.log(`${cake.name} costs ${cake.price}`);class Product { constructor(public name: string, public price: number) {}} const cake = new Product("Cake", 4);console.log(`${cake.name} costs ${cake.price}`);public name: string and public price: number declare the fields and copy the arguments, so the class shrinks to one line. It still prints Cake costs 4.
Every new profile should show the picture /images/default.png until the user uploads one. Fix line 3. What does the fixed code print?
class UserProfile { name: string; avatarUrl: string; constructor(name: string) { this.name = name; }} const asha = new UserProfile("Asha");console.log(asha.avatarUrl);class UserProfile { name: string; avatarUrl = "/images/default.png"; constructor(name: string) { this.name = name; }} const asha = new UserProfile("Asha");console.log(asha.avatarUrl);A starting value gives every profile an avatar from the start, and TypeScript works out that it's a string. The error is gone, and the code prints /images/default.png instead of undefined.
Line 13 reads cart.total, which doesn't exist yet. Add a getter that adds up the prices. What does it print?
class Cart { prices: number[] = []; add(price: number) { this.prices.push(price); }} const cart = new Cart();cart.add(3);cart.add(3);cart.add(4);console.log(cart.total);class Cart { prices: number[] = []; add(price: number) { this.prices.push(price); } get total() { return this.prices.reduce((sum, price) => sum + price, 0); }} const cart = new Cart();cart.add(3);cart.add(3);cart.add(4);console.log(cart.total);The getter adds up the prices each time cart.total is read. TypeScript works out that it returns a number, the error on cart.total is gone, and it prints 10: 3 + 3 + 4.
A search box should store what the user typed without extra spaces and in lowercase. Line 10 gets an error. Add a setter that cleans the text. What does the fixed code print?
class SearchBox { text = ""; get query() { return this.text; }} const box = new SearchBox();box.query = " Green Tea ";console.log(box.query);class SearchBox { text = ""; get query() { return this.text; } set query(value) { this.text = value.trim().toLowerCase(); }} const box = new SearchBox();box.query = " Green Tea ";console.log(box.query);With a setter, query can be assigned. Its value takes the getter's type, string, so trim() and toLowerCase() are allowed. Line 14 stores green tea, and line 15 prints it.
Check your understanding
For each question, think about two moments: what TypeScript says before the code runs, and what the code does when it runs. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What does
strictPropertyInitializationcheck?Choose an answer to see the explanation.
Question 2 of 7Which line does TypeScript flag in this toast message class?
Read the code, then predictclass Toast { message: string; seconds: number; constructor(message: string) { this.message = message; } }Choose an answer to see the explanation.
Question 3 of 7What does this badge code print?
Read the code, then predictclass Badge { constructor(public label: string, count: number) {} } const badge = new Badge("New", 3); console.log(Object.keys(badge).join(", "));Choose an answer to see the explanation.
Question 4 of 7This profile uses
!. What does it print, and does TypeScript complain?Read the code, then predictclass Profile { name!: string; } console.log(new Profile().name);Choose an answer to see the explanation.
Question 5 of 7Which line does TypeScript flag in this timer?
Read the code, then predictclass Timer { seconds = 90; get minutes() { return this.seconds / 60; } } const timer = new Timer(); timer.minutes = 2;Choose an answer to see the explanation.
Question 6 of 7Why does TypeScript flag line 6 of this cart?
Read the code, then predictclass Cart { count = 0; } const cart = new Cart(); cart.count = "2";Choose an answer to see the explanation.
Question 7 of 7Why does
erasableSyntaxOnlyreport a parameter property?Choose an answer to see the explanation.
Key takeaways
- Declare every field in the class body, with a type, a starting value, or both:
items: string[] = []. - TypeScript checks each
newcall against the constructor's parameters, and each use of a field against its type. - With
strictPropertyInitialization, a field whose type doesn't allowundefinedneeds a value by the end of the constructor.?allowsundefined;!turns the check off for one field. - A parameter property, like
constructor(public name: string), declares a field and stores the argument in one go. - Methods are typed like functions. A getter without a setter is read-only, and a setter's parameter takes the getter's type.
Remember the one-liner.
Declare each field with its type, give it a value by the end of the constructor, and let TypeScript check every object you make.
Coming next: public, private, protected & #private. It shows who can use each field and method, and how TypeScript's private differs from JavaScript's #private.