TypeScript classes are JavaScript classes (Classes) with the same fields, getters, static blocks, #private members and extends chains. What TypeScript adds is a set of modifiers that the checker enforces and the emitter mostly erases. Under strict (the default since TypeScript 6.0) every field must also be initialized in its declaration or the constructor; write name!: string when a framework assigns it later.
| Addition | Example | Checked by | Emitted JavaScript |
|---|---|---|---|
| public / protected / private | private balance: number | Checker only | Ordinary property |
| readonly | readonly id: string | Checker only | Ordinary property |
| Parameter property | constructor(private r: number) | Checker | Field plus this.r = r |
| abstract | abstract area(): number | Checker | Member removed |
| implements | class A implements B | Checker | Removed |
| override | override print() | Checker | Removed |
Parameter properties, access modifiers and #private
Prefix a constructor parameter with public, private, protected or readonly and TypeScript declares the field and assigns it for you, private members are visible only inside the declaring class, protected members also inside subclasses. The JavaScript # prefix is different: it is enforced by the engine at run time.
class Account {
#pin = "0000"; // JavaScript private field
constructor(
public readonly owner: string, // parameter properties
private balance: number,
protected currency = "EUR",
) {}
deposit(amount: number) { return (this.balance += amount); }
}
const acct = new Account("Ada", 100);
console.log(acct.deposit(50), acct["balance"]); // bracket escape hatch compiles
acct.owner = "Eve";
acct.balance = 1e6;
acct.#pin;account.ts(14,6): error TS2540: Cannot assign to 'owner' because it is a read-only property. account.ts(15,6): error TS2341: Property 'balance' is private and only accessible within class 'Account'. account.ts(16,6): error TS18013: Property '#pin' is not accessible outside class 'Account' because it has a private identifier.
The emitted constructor is plain JavaScript: field declarations owner; balance; currency; and this.owner = owner assignments. Without the three error lines it logs 150 150. Keep the last line and Node.js 2,131 refuses the file: SyntaxError: Private field '#pin' must be declared in an enclosing class.
| private balance | #balance | |
|---|---|---|
| Enforced | Compile time only | Run time, by the engine |
| Bracket access obj["balance"] | Allowed | Impossible |
| Seen by JSON.stringify, Object.keys | Yes | No |
| Same name redeclared in a subclass | Error TS2415 | Allowed, separate slot |
Prefer #private for real encapsulation in libraries. TypeScript's private remains handy for parameter properties and for tests that must peek inside. Remember from Installing and Running that parameter properties generate code, so erasableSyntaxOnly and Node.js type stripping reject them.
Abstract classes, implements and override
An abstract class cannot be instantiated and may declare abstract members without a body; every concrete subclass must implement them. JavaScript has no such keyword (Abstract Classes fakes it with a new.target check), so TypeScript deletes abstract members on emit. implements checks that a class satisfies one or more interfaces, and override, made mandatory by noImplicitOverride, catches methods that stop overriding anything after a base class is renamed.
interface Printable {
print(prefix: string): string;
}
abstract class Shape implements Printable {
abstract area(): number; // no body: subclasses must implement
print(prefix: string) { return `${prefix}${this.area().toFixed(2)}`; }
}
class Circle extends Shape {
constructor(private r: number) { super(); }
area() { return Math.PI * this.r ** 2; }
override print(prefix: string) { return `Circle ${super.print(prefix)}`; }
}
console.log(new Circle(1).print("area="));
class Square extends Shape {}
new Shape();
class Label implements Printable {
print(prefix) { return prefix; }
}
class Ring extends Circle {
area() { return 0; }
}shapes.ts(18,7): error TS2515: Non-abstract class 'Square' does not implement inherited abstract member area from class 'Shape'. shapes.ts(19,1): error TS2511: Cannot create an instance of an abstract class. shapes.ts(21,9): error TS7006: Parameter 'prefix' implicitly has an 'any' type. shapes.ts(24,3): error TS4114: This member must have an 'override' modifier because it overrides a member in the base class 'Circle'.
Compiled with --noImplicitOverride. The valid part logs Circle area=3.14. The Label error is the classic surprise: implements is only a check and does not give method parameters their types, so you still annotate them.