Skip to content
Theory · Classes · lesson 1 of 12

Fields and the constructor

What you'll learn

What you'll learn:

  • declare class fields with types and find a field that was left without a value;
  • close fields with private, protected and # and know which of them works at run time;
  • protect fields with readonly and shorten a constructor with parameter properties;
  • override methods with override and set shared rules with abstract and implements.
A class blueprint, a door ajar with a paper private sign, and a bolted door marked

An architect draws the blueprint of a house: how many rooms it has, where the windows are, how big the kitchen is. A whole block of houses is built from one blueprint, and every house in it is laid out the same way. A class is such a blueprint, and the objects that new creates are the houses built from it.

In JavaScript the blueprint can be incomplete. A field appears in an object when the constructor or a method writes something into this. Misspell a field's name inside a method, and JavaScript quietly creates a new field with the typo in its name.

In TypeScript every field is listed in the class body, and each one has a type. The compiler works like a building inspector: it checks every use of a field against the blueprint. Here is a click counter:

TypeScript
class Counter {
    count: number;
    step = 1;

    constructor(start: number) {
        this.count = start;
    }

    next(): number {
        this.count += this.step;
        return this.count;
    }
}

console.log(new Counter(10).next()); // 11

Let's go through the class line by line:

  1. count: number; declares the field count with the type number. There is no value here; the constructor gives it one.
  2. step = 1; declares a field together with its value. TypeScript infers the type from the value: number.
  3. The constructor takes start: number and puts it into this.count.
  4. The method next(): number adds the step and returns the new value. Its result type is checked as for an ordinary function.
  5. new Counter(10).next() prints 11.

A field with a type

A field declaration looks like a variable declaration, only without let:

  • the field's name: count;
  • a colon and the type: : number, the same annotation as on a variable;
  • an initial value after =: an optional part, like = 1 on the field step.

When there is a value, you may leave the type out: TypeScript infers it. That is why step has no annotation, and a field label = "clicks" would get the type string.

Inside the methods tsc knows the type of every field. It will not let this.count = "many" through, in the same way as it would not allow that assignment to a variable of type number.

Every field gets a value

Strict mode checks one more thing: by the end of the constructor every field must have a value. Let's remove the constructor from the counter:

TypeScript
class Counter {
    count: number; // ✗
    step = 1;
}

console.log(new Counter().step);
TEXT
error TS2564: Property 'count' has no initializer and is not definitely assigned in the constructor.

The compiler says: "the property count has no initial value, and the constructor does not definitely assign it". In JavaScript such a field would be undefined, while its type promises a number. The error catches that mismatch before the program runs.

There are three ways to fix it:

  • give the field a value in its declaration: count = 0;;
  • assign it a value in the constructor, as in the first example;
  • allow the field to be empty: the field count?: number; has the type number | undefined, and you have to check it before use.

The word "definitely" in the message matters. An assignment inside an if does not count: if the condition fails, the field stays empty. Nor does an assignment in a method the constructor calls: tsc looks only at the constructor itself.

A class is also a type

You can write a class's name wherever a type goes: on a variable, on a parameter, on a function's result.

TypeScript
class Counter {
    count = 0;
    step = 1;
}

function show(c: Counter): string {
    return `${c.count} (+${c.step})`;
}

console.log(show(new Counter())); // 0 (+1)
console.log(show({ count: 5, step: 2 })); // 5 (+2)
  1. show takes a parameter of type Counter and reads its fields.
  2. The first call passes an object created with new Counter() and prints 0 (+1).
  3. The second call passes a plain object with no new at all, and tsc lets it through too. The program prints 5 (+2).

Here the blueprint picture stops working. A real inspector would ask which blueprint the house was built from. TypeScript compares only the shape: any object with the fields count and step of the right types fits. This is the same structural typing that interfaces have.

Common mistake

The field was not declared but written straight in the constructor, the way you are used to in JavaScript:

TypeScript
class Counter {
    constructor(start: number) {
        this.count = start; // ✗
    }
}
TEXT
error TS2339: Property 'count' does not exist on type 'Counter'.

In JavaScript a write to this creates the field on the fly. TypeScript checks the blueprint: the class body has no field count, so nothing can be written to it. Add the declaration count: number; above the constructor. The same check catches typos: for this.cuont the compiler says there is no such property and suggests count.

Key points

  • Class fields are declared in the class body with a type, for example count: number;, and a field with a value gets its type inferred.
  • In strict mode every field gets a value in its declaration or in the constructor, otherwise you get error TS2564.
  • You cannot write a field to this that is not among the declared ones: error TS2339.
  • A class's name is also a type, and any object of the same shape fits it.