Topic 6.1
Classes with Types
In one line
A class is a blueprint that builds many objects with the same typed fields and methods. TypeScript checks that every field is declared, that every field is given a value before the object is used, and that every method is called with the right arguments.
Think of it like this
A cookie cutter. The cutter is the shape; each cookie you press out is a separate cookie with its own icing. A class is the cutter and every object made from it is a cookie. All cookies have the same shape (the same fields and methods), but one cookie can have blue icing and another pink. TypeScript is the baker who checks that every cookie really has the shape the cutter promises, and has icing before it leaves the kitchen.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Class
- A blueprint that describes the fields and methods every object built from it will have.
- Instance
- One object built from a class with
new. - Field
- A value stored on each object, like
count. Also called a property. - Constructor
- The special function that runs once when you write
newand sets up the new object. - Method
- A function that belongs to a class and works on the object it was called on.
- this
- Inside a method, the object the method was called on.
- Initializer
- A value written right on the field, like
items: string[] = [];. - Definite assignment assertion
- The
!after a field name, a promise to the checker that the field will be set before it is read. - Prototype
- The shared object that holds a class's methods so each instance does not need its own copy.
Step by step
01Declare the fields, then fill them in the constructor
Start with a counter that adds a fixed step each time. Two fields (count and step) are declared with their types. The constructor takes the step, and sets both fields.
Notice increment() has no return type written. TypeScript works it out: it returns this.count, a number. The new keyword builds one instance, and the type of that instance is the class name.
class Counter {
count: number;
step: number;
constructor(step: number) {
this.count = 0;
this.step = step;
}
increment() {
this.count += this.step;
return this.count;
}
}
const c = new Counter(2); // c: Counter02Forget a field and the checker notices
Add an email field but never set it. In plain JavaScript the object would simply lack the value and u.email would be undefined at some later, harder-to-find moment.
TypeScript checks the constructor and reports TS2564 on the field declaration. The message names the field and the problem exactly.
class User {
name: string;
email: string; // error TS2564: has no initializer and is not definitely assigned
constructor(name: string) {
this.name = name;
}
}03Four ways to fix it
Pick the one that is honest. If a sensible default exists, write an initializer. If the value always comes in from outside, take it as a constructor argument. If the value may really be absent, mark it optional with ?. If a framework sets it later, use !.
Each choice changes what the *type* promises to the rest of your code. An optional field has type string | undefined, so every reader must handle the missing case; a ! field claims it is always there.
class Cart {
items: string[] = []; // initializer
note?: string; // may be missing: string | undefined
id!: number; // "trust me": set by load() before use
load() {
this.id = 7;
}
}04Methods, and what happens to this
A normal method uses the this of whoever calls it. Writing b.clickMethod() passes b. Pulling the method out into a variable and calling it plain passes nothing, so this is undefined in a module.
An arrow function stored in a field captures this once, when the object is built, so it can be passed around safely. TypeScript does not warn about the detached call, so you have to know this rule yourself.
05A class is a value and a type
The name Point after new is the constructor, a value you can pass around. The name Point after a colon is the type of one instance. The constructor itself has its own type, typeof Point.
Because TypeScript compares shapes (Topic 3.5 and Topic 6.5), an object literal with the same members is also accepted where a Point is wanted.
Try it yourself
- 1
Fix the missing email
In the User example, change the constructor to
constructor(name: string, email: string)and setthis.email = email. Update the call tonew User("Asha", "a@x.in"). Predict the Types panel before you look.It becomes clean, and the console prints the email instead of
undefined. - 2
Make a field optional
In the User example, change
email: string;toemail?: string;. Predict: does the error go away? What isu.email's type now?The class error disappears because
undefinedis now allowed. Hover with// ^?underu.emailto seestring | undefined. - 3
Call a class like a function
In the last example add
const p2 = Point(5, 6);and read the error before you scroll.TypeScript says the value of type
typeof Pointis not callable and suggestsnew. At runtime JavaScript would throwClass constructor Point cannot be invoked without 'new'.
Code & diagrams
Two fields, one constructor, one method. The ^? shows the instance type is just the class name. The console prints the instance as Counter { ... }.
Console
(nothing was printed)Expected console output
4
Counter { count: 4, step: 2 }TypeScript reports the problem at the field. The program still runs (like tsc, the live editor emits anyway) and prints the undefined that the error predicted. Fix it by passing email into the constructor.
Console
(nothing was printed)Expected console output
undefinedThe first console line is undefined: the ! field exists but nobody has set it yet, and TypeScript trusted you. The last line is an error because note is optional, so its type includes undefined.
Console
(nothing was printed)Expected console output
undefined
7No type errors, yet the last line prints undefined: the detached method has no object. The arrow function field kept its this. (this?. is used so the demo does not crash.)
Console
(nothing was printed)Expected console output
OK
OK
undefinedq is a plain object, not made by new Point, yet it is accepted: TypeScript compares shapes. The type of the class itself (the thing you call new on) is typeof Point.
Console
(nothing was printed)Expected console output
(1, 2) qBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Misspell a field in the constructor
In the Counter example, change this.step = step; to this.stepp = step;.
Myth vs fact
Myth
A TypeScript class is a different thing from a JavaScript class.
Fact
It is the same JavaScript class. TypeScript only adds field types, access modifiers and checks, and erases them before the code runs.
Myth
Declaring count: number; sets it to 0.
Fact
A declaration only tells the checker the field exists and its type. With modern targets it creates the property with the value undefined. Give it an initializer or set it in the constructor.
Myth
The ! after a field makes it safe.
Fact
It only silences the checker. If nothing really sets the field, the first read gives undefined where your type promised a value.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
strictPropertyInitializationneedsstrictNullChecks(both are part ofstrict). The checker follows control flow in the constructor: assigning in both branches of anifcounts, assigning in a helper method called from the constructor does not (use!or an initializer for that). - ▸
useDefineForClassFieldschanges the emit. When it is on (default fortargetES2022 and later),x!: number;becomes a real field set toundefined, and a subclass field redeclaring a parent property can overwrite what the parent constructor set. When it is off, an uninitialised declaration emits nothing at all. - ▸
Methods are shared on the prototype, so a class with 10,000 instances has one copy of each method. An arrow-function field creates one closure per instance: fine for event handlers, wasteful for hot data classes.
Remember this
- 1
The problem this solves: Making many similar objects by hand means repeating the same fields and functions, and the copies drift apart. A class defines them once. A class is a blueprint for objects. You write it once with the fields (the values each object stores), the constructor (a special function that runs when you build an object) and the methods (functions that belong to the object). You build an object with
new Counter(2); that object is called an instance. Classes are plain JavaScript (they arrived in 2015); TypeScript adds types on top and erases them again before the code runs. - 2
TypeScript wants every field declared at the top of the class with its type:
count: number;. In JavaScript you can invent a new property any time by writingthis.count = 0; TypeScript refuses to guess, because a typo likethis.cuontwould silently create a new property. Declaring the fields once gives the checker the full list, so it can complain aboutProperty 'cuont' does not exist(errorTS2339, orTS2551when it can suggest the right name) and offer autocomplete. - 3
The big rule is **
strictPropertyInitialization** (part ofstrict, so it is on in every example on these pages). It says: every field whose type does not allowundefinedmust get a value before the constructor finishes. A new object that has a field typedstringbut holdingundefinedis a lie the type system refuses to tell. The error isTS2564: Property 'email' has no initializer and is not definitely assigned in the constructor. - 4
You have four honest ways to satisfy the rule. Give the field an initializer (
items: string[] = [];). Assign it in the constructor (this.name = name;). Admit it may be missing by making it optional (note?: string, whose type becomesstring | undefined) or by writingundefinedinto the type. Or use the definite assignment assertionid!: number;, which says "trust me, something will set this before anyone reads it" (for example a method called by a framework). The!switches the check off for that one field, so use it only when you can name the code that does the setting. - 5
Methods are functions stored once on the class (on the prototype, the shared object that all instances look up) and not copied into every instance. Inside a method,
thisis the object the method was called on. That is decided at the call site:counter.increment()works, but pulling the method out (const f = counter.increment; f()) loses the object andthisbecomesundefined. An arrow function field (click = () => this.label) is created per instance and always remembers its own object, at the cost of one function per instance. - 6
A class declaration creates two things with one name. A value (the constructor function you call with
new) and a type (the shape of the instances). Inconst c: Counter = new Counter(2)the firstCounteris the type and the second is the value. The type of the constructor itself istypeof Counter. Calling a class withoutnewis an error in TypeScript (TS2348) and also throws at runtime. - 7
One detail worth knowing: with the modern target the live examples use (
target: esnext), TypeScript keeps field declarations as real class fields (useDefineForClassFieldsis on by default for ES2022 and later). A field declared asid!: number;really exists on the object with the valueundefineduntil you set it. Fields are also set after the parent constructor has run, a subtle order you will meet again in Topic 6.3.
Explain it without notes
What does strictPropertyInitialization check, and how can you satisfy it?
What is the difference between a class as a value and a class as a type?
Why can this be undefined inside a method, and how do arrow function fields help?
What does id!: number mean, and what is the risk?
Practice
Write a class Rectangle with width and height (numbers) set from the constructor and a method area(). Create one, log its area, and then try to read rect.color and write down the error.
This class has an error. Fix it in two different ways: once with an initializer and once with an optional field. Say how the type of tags differs between the two.
``ts
class Post {
title: string;
tags: string[];
constructor(title: string) {
this.title = title;
}
}
Trade-offs
- ↔
Classes bundle data and behaviour and give you
instanceofand inheritance. Plain objects plus functions (and types from Phase 3) are lighter and easier to copy and serialise. Many TypeScript codebases use classes for services and long-lived objects, and plain types for data. - ↔
Arrow function fields fix
thisat the price of one function per instance and methods that subclasses cannot override through the prototype.
Done when you can
I can write a class with typed fields, a constructor and a method.
I can explain
strictPropertyInitializationand name the four ways to satisfy it.I can say what
thisis when a method is called normally and when it is detached.I can tell the class as a type from
typeof ClassName.I know that
!only silences the checker and that it is a promise I must keep.