If you have written a class in plain JavaScript before, you already know most of what you need. A class is a blueprint: you describe what an object should look like and what it can do, and then you create as many objects from that blueprint as you like. TypeScript does not throw any of that away. It keeps the same syntax and adds one thing on top — types — so the editor can catch mistakes before your code ever runs.
This article walks through what TypeScript adds to a class, one feature at a time. We will start from a familiar JavaScript class, then layer on typed fields, access modifiers, readonly, parameter properties, interfaces, and a quick look at abstract classes. By the end you will be able to read and write a typed class with confidence.
If classes themselves are still new to you, it is worth reading JavaScript Classes first. Everything here assumes you already know the basic shape of a class.
Starting from a plain JavaScript class
Here is an ordinary JavaScript class that represents a user. Nothing special yet.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greeting() {
return `Hi, I'm ${this.name}`;
}
}
const john = new User("John Doe", "john@example.com");
This works, but JavaScript will happily let you do silly things. You could write new User(42, true) and nobody complains until something breaks later. You could type john.emial (a typo) and get undefined instead of an error. TypeScript exists to close those gaps.
Typed fields
The first thing TypeScript adds is the ability to declare your fields with a type. You list each property at the top of the class, write its name, a colon, and the type it holds.
class User {
name: string;
email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
greeting(): string {
return `Hi, I'm ${this.name}`;
}
}
const john = new User("John Doe", "john@example.com");
Two things changed. The fields name and email are now declared as string at the top, and the constructor parameters got types too. The greeting() method also says it returns a string. Now if you call new User(42, true), TypeScript stops you immediately and explains that 42 is not a string.
Declare fields, then assign them
In TypeScript you usually list every field at the top of the class with its type. The constructor still assigns the values, but the declaration is what tells TypeScript the shape of each object. Think of it as a label on the box before you put anything inside.
You can also give a field a default value, and TypeScript will infer its type from that value:
class User {
name: string = "Anonymous";
isActive: boolean = true;
}
Here you do not even need to write the types — TypeScript reads "Anonymous" and knows name is a string, and reads true and knows isActive is a boolean.
Access modifiers: public, private, protected
In plain JavaScript, every property on an object is reachable from anywhere. Sometimes that is fine. Often it is not — some details are internal plumbing and you do not want other code poking at them. TypeScript gives you three keywords to control who can touch a field or method.
| Modifier | Who can access it |
|---|---|
public |
Anyone, from anywhere (this is the default) |
private |
Only code inside the same class |
protected |
Code inside the class and any class that extends it |
Here is the same User class with a private field for a password hash, which the outside world should never read directly.
class User {
public name: string;
private passwordHash: string;
constructor(name: string, passwordHash: string) {
this.name = name;
this.passwordHash = passwordHash;
}
public checkPassword(hash: string): boolean {
return this.passwordHash === hash;
}
}
const jane = new User("Jane Doe", "a1b2c3");
jane.name; // OK — public
jane.checkPassword("x"); // OK — public method
jane.passwordHash; // Error — passwordHash is private
The last line is the point. Outside the class, passwordHash does not exist as far as TypeScript is concerned. You are forced to go through checkPassword, which is exactly the controlled door you wanted.
protected sits in the middle. A protected member is hidden from the outside world, but a subclass can still use it.
class Account {
protected balance: number = 0;
}
class SavingsAccount extends Account {
addInterest(rate: number): void {
this.balance += this.balance * rate; // allowed: subclass can see protected
}
}
These checks are compile-time only
private and protected are enforced by TypeScript while you write code, not by the browser at runtime. After your TypeScript is compiled to JavaScript, the field is just a normal property. If you need a value to be truly hidden even at runtime, use JavaScript’s native #private fields instead. For day-to-day app code, the TypeScript modifiers are usually enough.
readonly: set once, never change
Some values should be locked in the moment an object is created — an ID, a creation date, things that simply should not change afterwards. The readonly keyword does that. You can assign the field in the declaration or in the constructor, but nowhere else.
class User {
readonly id: string;
name: string;
constructor(id: string, name: string) {
this.id = id; // OK — assigning in the constructor
this.name = name;
}
}
const john = new User("u-001", "John Doe");
john.name = "Johnny"; // OK
john.id = "u-002"; // Error — id is readonly
readonly is about intent. It tells anyone reading your code, and TypeScript itself, that this value is fixed. Trying to reassign it is caught before the program runs.
Parameter properties: a handy shortcut
Look back at the typed User class. There was a repetitive pattern: declare a field, accept a parameter with the same name, then copy the parameter into the field. TypeScript offers a shortcut that collapses all three steps into one. If you put an access modifier (or readonly) directly in front of a constructor parameter, TypeScript creates the field and assigns it for you.
class User {
constructor(
public name: string,
private email: string,
readonly id: string
) {}
greeting(): string {
return `Hi, I'm ${this.name}`;
}
}
const jane = new User("Jane Doe", "jane@example.com", "u-002");
That tiny class is equivalent to declaring three fields and assigning each one in the constructor body. The constructor body is even empty here because there is nothing left to do. This is called a parameter property, and you will see it often in real TypeScript code.
The modifier is what makes it a field
A constructor parameter only becomes a class field when it has a modifier in front of it — public, private, protected, or readonly. A plain constructor(name: string) with no modifier is just a normal parameter that disappears when the constructor finishes.
Implementing an interface
An interface is a description of a shape: a list of properties and methods that something must have, with no actual code inside. Classes can promise to follow an interface using the implements keyword. TypeScript then checks that the class really does provide everything the interface requires.
interface Greetable {
name: string;
greeting(): string;
}
class User implements Greetable {
constructor(public name: string) {}
greeting(): string {
return `Hi, I'm ${this.name}`;
}
}
If you forgot to write the greeting method, or spelled it wrong, TypeScript would refuse to compile and tell you the class does not match Greetable. This is great for keeping a team honest: agree on an interface, and every class that implements it is guaranteed to have the same shape.
A single class can implement more than one interface — just separate them with commas: class User implements Greetable, Serializable {}.
Abstract classes, briefly
Sometimes you want a base class that should never be created on its own, only extended. An abstract class is exactly that — a partial blueprint. It can contain finished methods, but it can also declare abstract methods that have no body, forcing every subclass to fill them in.
abstract class Shape {
abstract area(): number; // no body — subclasses must provide one
describe(): string {
return `This shape has an area of ${this.area()}`;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
const c = new Circle(5);
c.describe(); // "This shape has an area of 78.53..."
new Shape(); // Error — cannot create an instance of an abstract class
Shape gives you a shared describe() method for free, but it refuses to be built directly and demands that each subclass define area(). That is the whole idea: shared behaviour up top, required gaps that children must fill.
Quick recap
TypeScript does not reinvent classes — it adds a layer of safety on top of the JavaScript classes you may already know. Here is the short version of everything we covered:
- Typed fields declare what type each property holds, so wrong values are caught early.
- Access modifiers (
public,private,protected) control who can reach a field or method. readonlylocks a value after it is set, usually in the constructor.- Parameter properties let you declare and assign a field straight from a constructor parameter by adding a modifier in front of it.
implementsmakes a class promise to match an interface, and TypeScript verifies it.- Abstract classes are base blueprints that cannot be instantiated and can force subclasses to implement certain methods.
None of this changes how your code runs in the browser — these checks happen while you write, then melt away into ordinary JavaScript. The payoff is that a whole category of bugs gets caught before you ever hit refresh. Start by adding types to your fields, and bring in the rest as you need it.