Classes in TypeScript Explained
TypeScript classes build on JavaScript's class syntax with type annotations for fields, constructors, and methods. Learn the basics of writing typed classes from scratch.
TypeScript classes are a blueprint for creating objects with a fixed shape. A TypeScript class looks almost identical to a JavaScript class, but you add type annotations to the fields, constructor parameters, and methods. TypeScript uses those types to check your code at compile time and then strips them away, leaving plain JavaScript.
Here is the smallest useful class:
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const origin = new Point(0, 0);
console.log(origin.x, origin.y);Running this code produces the following output. You can verify the result by copying the example into a TypeScript file and running it with Node.js or in the TypeScript playground.
0 0The field declarations x: number and y: number tell TypeScript what shape every Point instance will have. The constructor fills in those fields when you call new Point(0, 0).
TypeScript checks that the arguments you pass to the constructor match the declared types.
Field Declarations
A field declaration gives a name and a type to a class property. It tells TypeScript "every instance of this class has this property, and it holds this kind of value."
class User {
name: string;
age: number;
active: boolean;
}
const user = new User();
user.name = "Ada";
user.age = 30;
user.active = true;Fields are public and writable by default. Anyone can read or change user.name after the instance is created. You can also initialize fields directly instead of setting them in the constructor:
class Counter {
value: number = 0;
increment() {
this.value += 1;
}
}
const counter = new Counter();
counter.increment();
console.log(counter.value);This prints 1, since increment adds 1 to the initial value of 0. Each call to increment reads the current value field and writes back one higher, so the change is visible on the same counter instance afterward.
1When a field has an initializer, TypeScript infers the type from that value. Writing value = 0 is the same as writing value: number = 0.
The type annotation is optional here because TypeScript knows 0 is a number.
The strictPropertyInitialization Check
TypeScript's strictPropertyInitialization setting (part of strict mode) requires every field to be definitely assigned:
class BadGreeter {
name: string;
}TypeScript checks the code and reports the following error. The compiler stops here and does not emit JavaScript until you fix the issue.
Property 'name' has no initializer and is not definitely assigned in the constructor.TypeScript is right to complain. If you create new BadGreeter(), the name field is undefined at runtime even though you declared it as a string.
That is a bug waiting to happen. Fix it by adding an initializer or a constructor that sets the field.
class GoodGreeter {
name: string;
constructor() {
this.name = "world";
}
}Now every instance of GoodGreeter is guaranteed to have a string in its name field. TypeScript only checks assignments in the constructor body itself. It does not analyze methods called from the constructor, because a subclass might override those methods.
Methods
A method is a function that lives on a class. It can use type annotations on parameters and return values, just like a regular function. Here is a Rectangle class with a method that computes area and a method that scales the instance:
class Rectangle {
width: number; height: number;
constructor(width: number, height: number) {
this.width = width; this.height = height;
}
area(): number {
return this.width * this.height;
}
scale(factor: number): void {
this.width *= factor; this.height *= factor;
}
}The area method returns a value and does not touch the instance. Calling it prints the product of the two fields:
const rect = new Rectangle(10, 5);
console.log(rect.area());This prints 50, the product of the width and height fields. Nothing about rect changes here, since area only reads the two fields and returns a computed number rather than storing it back on the instance.
50The scale method is different: it modifies the instance instead of returning a value. Calling rect.scale(2) doubles both fields in place:
const rect = new Rectangle(10, 5);
rect.scale(2);
console.log(rect.width, rect.height);This prints 20 10, since scale multiplies both fields by the factor argument. Unlike area, scale writes new values back onto width and height, so the rect instance itself ends up permanently changed after the call.
20 10Inside a method, always use the this. prefix to access fields and other methods. An unqualified name refers to a variable in the enclosing scope, not a class member.
Writing just x inside a method looks for a local variable or parameter named x, not the class field.
Fields vs Methods at a Glance
| Feature | Field | Method |
|---|---|---|
| Purpose | Holds data | Performs an action |
| Type annotation | x: number | (n: number): void |
| Can be readonly | Yes | Methods cannot be readonly (use get instead) |
| Initialized in | Declaration or constructor | Defined once on the prototype |
| Access with | this.x | this.method() |
The Definite Assignment Assertion
Sometimes you know a field will be initialized, but not inside the constructor. An external library might fill it in, or a framework might set it through a lifecycle hook.
Use the ! operator to tell TypeScript to trust you:
class Component {
container!: HTMLElement;
mount(selector: string) {
this.container = document.querySelector(selector)!;
this.container.textContent = "Mounted";
}
}The ! after container suppresses the strictPropertyInitialization error. Use it sparingly.
If you forget to call mount before accessing container, the field will be undefined at runtime and TypeScript will not warn you about it.
Type Inference on Fields
When a field has an initializer, TypeScript infers the type. When it does not, the type annotation is mandatory under strict mode:
class User {
name = "Ada";
age: number;
constructor(age: number) {
this.age = age;
}
}The name field is inferred as string from the initializer. The age field has no initializer, so the type annotation is required.
Once a field type is established (by inference or annotation), you cannot assign a different type. For example:
class Point {
x = 0;
}
const pt = new Point();
pt.x = "zero";The field x was inferred as number from x = 0. Trying to assign a string to it produces this error:
Type 'string' is not assignable to type 'number'.When to Use a Class vs an Interface or Type
Classes create objects at runtime. Interfaces and type aliases only describe shapes at compile time:
| Needs | Use |
|---|---|
Create objects with new | Class |
| Describe the shape of data coming from an API | Interface or type alias |
| Share behavior (methods) across instances | Class |
| Define a contract or constraint | Interface |
| Combine existing types | Type alias |
For more on object modeling, see interfaces in TypeScript explained and type aliases in TypeScript explained.
Common Beginner Mistakes
Forgetting this. inside a method. An unqualified name looks for a variable in the outer scope:
class Point {
x = 0;
printX() {
console.log(x);
}
}TypeScript reports this error at compile time, before any JavaScript runs, because printX looks for a variable named x in its enclosing scope and finds nothing there:
Cannot find name 'x'.Fix: write this.x instead of x. TypeScript catches this because it looks for a variable named x in the enclosing scope, and there is none.
Declaring a field without initializing it. Under strictPropertyInitialization, every field must have a value by the time the constructor finishes. Either add an initializer or set it in the constructor.
Calling new on a class that has no constructor. That is fine. If you do not write a constructor, JavaScript provides an empty one. All fields with initializers still get their values.
The next step is to understand how constructors in TypeScript work in more detail, including parameter properties and overloaded constructors.
Rune AI
Key Insights
- A TypeScript class is a JavaScript class with type annotations on fields, constructor parameters, and methods.
- Declare field types before the constructor so TypeScript knows the shape of every instance.
- With strictPropertyInitialization on, every field must have an initializer or be set in the constructor.
- Use the definite assignment assertion (!) only when you are sure a field will be initialized externally.
- All type syntax disappears at compile time. The runtime class is plain JavaScript.
Frequently Asked Questions
Do TypeScript classes work differently than JavaScript classes at runtime?
What happens if I do not annotate a class field type?
Can I use TypeScript classes without strictPropertyInitialization?
Conclusion
TypeScript classes add compile-time type safety to the familiar JavaScript class syntax. You annotate fields with types, TypeScript checks that every field is definitely assigned, and the compiler catches mistakes like missing properties or wrong argument types before your code runs. The result is the same JavaScript class at runtime, but with far fewer bugs.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.