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.

8 min read

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:

typescripttypescript
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.

texttext
0 0

The 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."

typescripttypescript
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:

typescripttypescript
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.

texttext
1

When 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:

typescripttypescript
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.

texttext
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.

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
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.

texttext
50

The scale method is different: it modifies the instance instead of returning a value. Calling rect.scale(2) doubles both fields in place:

typescripttypescript
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.

texttext
20 10

Inside 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

FeatureFieldMethod
PurposeHolds dataPerforms an action
Type annotationx: number(n: number): void
Can be readonlyYesMethods cannot be readonly (use get instead)
Initialized inDeclaration or constructorDefined once on the prototype
Access withthis.xthis.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:

typescripttypescript
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:

typescripttypescript
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:

typescripttypescript
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:

texttext
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:

NeedsUse
Create objects with newClass
Describe the shape of data coming from an APIInterface or type alias
Share behavior (methods) across instancesClass
Define a contract or constraintInterface
Combine existing typesType 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:

typescripttypescript
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:

texttext
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

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.
RunePowered by Rune AI

Frequently Asked Questions

Do TypeScript classes work differently than JavaScript classes at runtime?

No. TypeScript classes compile to the same JavaScript class syntax. All type annotations, access modifiers, and readonly are removed during compilation. The runtime behavior is identical to JavaScript classes.

What happens if I do not annotate a class field type?

If strict mode is off, the field gets an implicit any type. If strict mode is on and there is no initializer or constructor assignment, TypeScript reports an error because it cannot infer the type.

Can I use TypeScript classes without strictPropertyInitialization?

Yes, but it is not recommended. Without it, uninitialized fields silently become undefined, leading to runtime bugs. Keep strict mode on and either add an initializer, set the field in the constructor, or use the definite assignment assertion (!) when you are sure external code will set the value.

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.