Classes and interfaces
A class describes a shape and produces objects. TypeScript adds three access levels, a way to require that a member exists, and a way to check a class against an interface. All of it is checked while compiling, and almost all of it disappears from the output.
Fields: if you declare one, it must exist
A field declaration is a promise about every instance. Under strict mode the compiler checks that you keep it.
What a field declaration promises
When you write name: string inside a class, you are telling the compiler that every object created from this class has a name, and it is a string. Code elsewhere is then allowed to call shop.name.toUpperCase() without checking for undefined.
That promise has to be kept, or the guarantee is worthless. The flag that enforces it is strictPropertyInitialization, one of the checks turned on by strict. If a declared field is never assigned, the compiler stops you before the code runs.
Two mistakes this check catches
Assigning the field from another method. If the constructor calls this.init() and init assigns the field, you still get ts(2564). The compiler does not follow assignments across methods, because it cannot tell whether that method always runs. Assign the field in the constructor itself.
Reaching for ! too early. boss!: string is a definite assignment assertion: it moves the guarantee from the compiler to you. The check stops reporting, but nothing is verified. If the value is really undefined, the failure happens at runtime. Use it only where something outside the class is known to assign the field, such as a framework that injects it.
A class declaration creates two things
class Order {} creates a type named Order and a value named Order. In a type position, Order means one instance of the class. The class object itself — the thing you write new in front of — has the type typeof Order. So let a: Order holds an instance, while let b: typeof Order = Order holds the class and lets you call new b(). An interface creates only a type, never a value.
Methods need no new rules. A method is a function that lives inside a class, and its parameters and return type are annotated exactly as chapter 02 described. The new material starts in the next section: saying which code is allowed to read a member.
Three access levels: public, protected, private
Each level says which code may read the member. All three are checked only while compiling.
public means any code may read the member. It is the default, so writing no modifier is the same as writing public. protected narrows that to the class and any class that extends it. private narrows it further, to the class body itself. In code:
const shop = new MilkTeaShop();shop.vaultCode;
A private member is readable only inside the class that declares it. A subclass does not count as inside, and neither does outside code.
🏦 private member · read from Outside (on an instance created with new)
Property 'vaultCode' is private and only accessible within class 'MilkTeaShop'. ts(2341)and its subclasses, the private one does not. Both checks happen only while compiling.In a subclass constructor, super() comes first
If a subclass writes its own constructor, it must call super(...) before it touches this. Forgetting gives ts(2377): Constructors for derived classes must contain a 'super' call. This rule comes from JavaScript itself; TypeScript only reports it earlier.
One consequence is easy to miss. The subclass's own field initializers run after super() returns. So if a base-class constructor reads a field that the subclass initializes, it reads undefined. Anything the base constructor needs should be passed to it as an argument.
private is a compile-time check; #field is a runtime one
private hides nothing once the code runs. This section shows exactly what survives compilation.
Chapter 01 said that types are erased when TypeScript compiles. Access modifiers are part of the type system, so they are erased too. private vaultCode becomes an ordinary property in the output, readable by anyone. JavaScript has its own private fields, written with a #, and those are a different mechanism. Compare the two:
Notice line 7 on the left. s["vaultCode"] compiles with no error at all — no cast is needed. TypeScript allows bracket access to a private member on purpose, as an escape hatch for tests and older code. And a plain JavaScript file that imports this class sees nothing unusual: it reads s.vaultCode directly.
class Shop {
private vaultCode = "8848";
}class Shop {
vaultCode = "8848";
}shop["vaultCode"]🔓 "8848" — no error, no castshop["vaultCode"] in the source: bracket access to a private member is a deliberate escape hatch, so no cast is needed. At runtime JSON.stringify(shop), DevTools, and any plain JavaScript file all see the value.No runtime cost, clear error messages, and it takes part in type compatibility (that is what §06 is about). Use it to mark which members are internal, so that other people editing the code do not reach into them by accident. That covers most of what encapsulation is for.
A JavaScript language feature, not a TypeScript one. It survives compilation and stays unreachable from outside the class while the program runs. TypeScript still type-checks it fully. Use it when code you do not control must not be able to reach the field, which mostly means libraries.
Common mistake: treating private as a security feature
A password or a token stored in a private field is not protected. One JSON.stringify(shop) prints it. So does the browser's DevTools, and so does any log line that serializes the object. #field does keep the value out of those places, but it is still plain text in memory and it is still in your bundle if you hard-coded it. Real secrets belong on a server, not in a field of either kind.
Four conveniences: parameter properties, readonly, get/set, static
None of these is a new idea. They are four shortcuts that make a class shorter to write.
Parameter properties save the most typing. Put an access modifier in front of a constructor parameter, and that one line declares the field, receives the argument, and assigns it:
Parameter properties emit code, so they cannot just be erased
This is the one feature in this chapter with no JavaScript equivalent. Everything else here is either erased or already JavaScript, but a parameter property makes the compiler generate a this.db = db statement that is not in your source. Node's built-in TypeScript support (22.18 and later) only strips types, so it rejects this syntax. The erasableSyntaxOnly flag reports it as ts(1294) if you want the compiler to warn you first. Use parameter properties freely with a normal build step; avoid them if Node must run your .ts files directly.
readonly blocks assignment after initialization. get / set let a pair of methods be used with property syntax. static puts a member on the class itself instead of on each instance. All three in one example:
readonly is a compile-time check like private, and it is narrower than it looks: it blocks assignment to the property, not changes to the object the property points at. With readonly tags: string[], the line o.tags = [] is an error but o.tags.push("x") is allowed. To stop changes at runtime you need Object.freeze.A getter and its setter may have different types
Since TypeScript 4.3 the setter may accept a wider type than the getter returns, as long as the getter's type is assignable to the setter's. That is useful when you want to accept several input forms but always hand back one: set sugar(v: number | string) together with get sugar(): number lets callers write o.sugar = "30" while o.sugar still reads as a number.
abstract and implements: an unfinished class, and a shape check
One says a subclass must finish the job. The other says a class must match an interface.
An abstract class is an unfinished class. Some members are implemented; the ones marked abstract have only a signature. You cannot create an instance of it, and a concrete subclass must implement every abstract member before it can be used:
implements is a check, not an inheritance. An interface lists the members a class must have; implements asks the compiler to verify that the class has them. It adds nothing to the class. A checkout counter does not care which payment service is behind it, only that the object can pay and refund:
pay(amount: number): Promise<string>refund(txId: string): Promise<void>
The value of the interface shows up at the call site. MilkTeaShop takes any object that matches PaymentProvider, so swapping the payment service changes nothing inside the shop:
Three details that are easy to get wrong
1. implements does not change the type of the class. It adds no members and it does not annotate anything for you. If you write pay(amount) { … } with no type, amount does not become number; it is an implicit any, which is an error under strict (ts(7006)). Write the parameter types yourself; the compiler then compares what you wrote against the interface.
2. abstract class vs interface. An interface only describes a shape and is erased at compile time — at runtime the name does not exist. An abstract class is a real class: it exists at runtime, it can hold implemented methods and state, and a subclass inherits them. Use an abstract class when subclasses should inherit working code; use an interface when you only want to describe a shape.
3. One extends, many implements. class Shop extends Building implements Payable, Refundable { } is valid. A class has exactly one base class, but it can be checked against any number of interfaces.
Back to chapter 04: classes compare by shape, except for private
The last missing piece of structural typing.
Chapter 04 said that TypeScript compares types by their shape, not by their name. Classes are no exception: two unrelated classes with the same members are interchangeable. There is one exception, and it appears as soon as a class has a private or protected member. Step through it:
const cup: PaperCup = new PlasticCup(); // ✓private stock lines and the assignment compiles. Or keep them and move the declaration to a shared base class that both cups extend — then there is only one declaration, and the two types are compatible again.The complete rule for structural typing
Types are compared by shape, except that two types with private or protected members are compatible only if those members come from the same declaration (inherited from a shared base class counts). This is also how libraries fake nominal typing: give a class one private member, and no same-shaped stranger can be used in its place.
Practice
Four tasks in the TypeScript Playground: trigger the access errors, sign a contract, read the compiled output, and finish an abstract class.
Quiz
Nine questions on what the compiler checks, and on what survives compilation.
class Shop { private vaultCode = "8848" } — after it is compiled to JavaScript, what happens to vaultCode?
Which sentence states the difference between protected and private correctly?
Under strict, name: string; reports ts(2564): declared but never assigned. Which change does not make the error go away?
You want a class that cannot be instantiated directly, and whose missing members a subclass must implement. Which keyword goes in front of class?
In class WeChatPay implements PaymentProvider, what does implements actually do?
class CashPay implements PaymentProvider { pay(amount) { … } } — amount has no annotation. What is its type?
About the parameter property constructor(private db: Database) {}, which statements are correct? (Choose all that apply.)
PaperCup and PlasticCup have identical members, and each declares its own private stock = 0. What does const cup: PaperCup = new PlasticCup() do?
You need a field that code outside the class cannot read while the program runs. Which one do you use?
- A declared field must actually be assigned: initialize it where you declare it, assign it in the constructor, or take responsibility with
!. strictPropertyInitialization enforces this, and it does not follow assignments made in other methods. - Three access levels:
publicis the default,protectedadds subclasses,privateis the class body only. All three are compile-time checks. privateis erased. In the compiled output it is an ordinary property, and even in TypeScripts["field"]reads it without a cast. For a field that is unreachable at runtime, use JavaScript's#field.- A parameter property replaces four lines with one, but it is the only feature here that generates code. A runtime that only strips types, such as Node running
.tsfiles directly, rejects it. abstractmarks a class that cannot be instantiated and members a subclass must implement.implementsonly checks the class against an interface: it adds no members and infers no parameter types. One class can be checked against several interfaces.- Classes are compared by shape, with one exception: once a class has a
privateorprotectedmember, those members must come from the same declaration for the two types to be compatible.