TypeScript 19 ๐ท Classes โ Properties, Methods, and Constructors
TypeScript classes start as JavaScript classes โ properties, methods, and constructors โ and add a type layer on top. Every property, method parameter, and return value can be typed. The compiler checks that the class is used correctly โ that properties are initialized, methods are called with the right arguments, and instances match the class’s declared shape. Classes are how you model objects with both state and behavior, and TypeScript makes them safer without changing how JavaScript works.
Key point: A class declares a shape โ properties it has, methods it defines, and access rules. TypeScript checks that properties are initialized (under strictPropertyInitialization), that methods use their parameters correctly, and that instances are constructed with the right arguments. The class is still a JavaScript class at runtime; the types are only at compile time.
Declaring a class
A TypeScript class looks like a JavaScript class, with types added.
class User {
id: number;
name: string;
email: string;
constructor(id: number, name: string, email: string) {
this.id = id;
this.name = name;
this.email = email;
}
greet(): string {
return `Hello, ${this.name}!`;
}
}
What’s declared:
id,name,emailโ properties with typesconstructor(id, name, email)โ parameters typedgreet(): stringโ method with return type
Creating an instance:
const alice = new User(1, 'Alice', 'alice@example.com');
alice.greet(); // 'Hello, Alice!'
alice.id; // number
alice.missing; // โ property doesn't exist
The compiler checks:
- Constructor called with 3 arguments of the right types
- Properties exist and have the declared types
- Methods exist and are called correctly
Property initialization: Under strictPropertyInitialization (part of strict), every property must be initialized โ either at declaration or in the constructor.
class User {
id: number; // โ
assigned in constructor
name: string; // โ
assigned in constructor
email: string = 'none'; // โ
default
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
Without assignment, the compiler errors:
class Bad {
id: number; // โ property 'id' has no initializer
}
Why this matters: Uninitialized properties are undefined at runtime โ a common source of bugs. TypeScript forces you to initialize every property, or explicitly declare it as possibly undefined.
Why classes still matter: Modern TypeScript often favors plain objects and functions, but classes still shine when you need state with behavior, private state, inheritance, or interface implementation. Classes are the right tool for modeling entities with identity and methods. The type system makes them safer without changing their runtime behavior.
Properties
Properties declare the shape of instances. They can be initialized inline, in the constructor, or with defaults.
Inline initialization:
class Config {
host = 'localhost';
port = 8080;
debug = false;
}
Types are inferred from the defaults: host: string, port: number, debug: boolean.
Explicit types:
class User {
id: number = 0;
name: string = '';
tags: string[] = [];
}
Explicit types are useful when the default doesn’t match the final type, or for documentation.
Constructor initialization:
class User {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
The most common pattern โ the constructor sets properties from parameters.
Optional properties:
class User {
id: number;
nickname?: string; // may be undefined
constructor(id: number) {
this.id = id;
}
}
? means the property may be absent. It’s typed string | undefined.
Definite assignment assertion โ !:
class User {
id!: number; // assigned elsewhere (e.g., by a framework)
}
The ! tells TypeScript “trust me, this will be set before use.” Use sparingly โ it silences a real check.
Readonly properties:
class User {
readonly id: number;
name: string;
constructor(id: number, name: string) {
this.id = id; // โ
assignable in constructor
this.name = name;
}
changeId(): void {
// this.id = 2; // โ readonly after construction
}
}
readonly allows assignment only in the constructor. After that, the property can’t be reassigned.
Static properties:
class User {
static count = 0;
static readonly MAX = 100;
constructor() {
User.count++;
}
}
User.count; // 0, then increments
User.MAX; // 100
Static properties live on the class, not instances. They’re shared across all instances.
Why properties are typed: The type tells you what shape an instance has. The compiler catches missing initialization, wrong types, and access to nonexistent properties. It’s the object shape you’ve seen with interfaces, but for classes.
Why
readonlymatters: Immutable properties are safer โ no accidental reassignment, no shared mutable state. For IDs, creation timestamps, and configuration,readonlydocuments intent and prevents bugs. It’s shallow: nested objects can still be mutated unless you make them readonly too.
Methods
Methods are functions on instances. They have access to this.
class Calculator {
value = 0;
add(n: number): this {
this.value += n;
return this;
}
subtract(n: number): this {
this.value -= n;
return this;
}
get(): number {
return this.value;
}
}
new Calculator().add(5).subtract(2).get(); // 3
Typed parameters and returns:
class User {
name = '';
greet(title: string): string {
return `Hello, ${title} ${this.name}`;
}
}
title must be a string; greet returns a string. The compiler enforces both.
The this return type: Returning this preserves the subclass type through method chains.
class Base {
setName(name: string): this {
return this;
}
}
class Child extends Base {
setAge(age: number): this {
return this;
}
}
new Child().setName('x').setAge(1); // โ
works
If setName returned Base, .setAge would fail because Base doesn’t have it. this keeps the chain going.
Optional and default parameters:
class Formatter {
format(value: string, prefix?: string): string {
return prefix ? `${prefix}${value}` : value;
}
pad(value: string, length = 10): string {
return value.padEnd(length);
}
}
Same rules as regular functions.
Static methods:
class User {
static create(name: string): User {
return new User(name);
}
constructor(public name: string) {}
}
User.create('Alice');
Static methods live on the class, not instances. Factory methods are a common pattern.
Overloading methods:
class Parser {
parse(input: string): object;
parse(input: number): number;
parse(input: string | number): object | number {
return typeof input === 'string' ? {} : input;
}
}
Same overload syntax as functions.
Private methods: Prefix with private to hide from outside the class.
class User {
private validate(): boolean {
return this.name.length > 0;
}
save(): void {
if (!this.validate()) throw new Error('invalid');
}
}
Private methods can be called only from within the class.
Why method typing matters: Method signatures are the class’s API. The compiler checks every call โ right arguments, right return type โ and prevents typos and wrong usage. That’s the same benefit as interfaces, but with implementation attached.
Why
thisreturn type is useful: It preserves the subclass type through chains, so fluent interfaces work naturally across inheritance. It’s a small feature that makes builder patterns and fluent APIs clean.
Constructors
The constructor initializes instances. TypeScript adds types to parameters and supports several convenient patterns.
Basic constructor:
class User {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
Parameter properties โ the shorthand:
class User {
constructor(
public id: number,
public name: string,
private email: string
) {}
}
public, private, protected, and readonly on constructor parameters do two things:
- Declare a property
- Assign it automatically from the parameter
The example above is equivalent to declaring id, name, email as properties and assigning them in the constructor. It’s the standard shorthand for simple cases.
Optional constructor parameters:
class User {
constructor(
public id: number,
public nickname?: string
) {}
}
new User(1); // โ
new User(1, 'Al'); // โ
Default constructor parameters:
class Config {
constructor(
public host = 'localhost',
public port = 8080
) {}
}
new Config(); // host=localhost, port=8080
Calling super โ inheritance:
class Animal {
constructor(public name: string) {}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // must call super first
}
}
In a derived class, super() must be called before accessing this.
Constructor return type: Constructors can’t declare a return type. They implicitly return the instance.
class User {
// constructor(): User { } // โ not allowed
constructor() {} // โ
}
Private constructors: A private constructor prevents new outside the class โ useful for singletons or factory-only classes.
class Singleton {
private static instance: Singleton | null = null;
private constructor() {}
static getInstance(): Singleton {
if (!this.instance) this.instance = new Singleton();
return this.instance;
}
}
Singleton.getInstance(); // โ
new Singleton(); // โ constructor is private
Why constructor typing matters: It’s the entry point to creating instances. TypeScript checks that new is called with the right arguments, that required parameters are provided, and that super is called appropriately in derived classes.
Why parameter properties are idiomatic: They eliminate boilerplate.
constructor(private userService: UserService)declares the property and assigns it in one line. It’s the most common pattern for dependency injection in Angular and NestJS โ and it’s how you’ll see constructors written most of the time.
Access modifiers
TypeScript adds public, private, protected, and readonly to control access.
public โ the default:
class User {
public name: string = '';
// equivalent to:
name: string = '';
}
Public members are accessible everywhere. That’s the default, so you usually don’t write public.
private โ class only:
class User {
private secret = 'hidden';
reveal(): string {
return this.secret; // โ
inside class
}
}
const u = new User();
u.secret; // โ private
Private members are accessible only from within the class body โ not subclasses, not outside.
protected โ class and subclasses:
class Animal {
protected name: string = '';
getName(): string {
return this.name;
}
}
class Dog extends Animal {
bark(): string {
return `${this.name} barks`; // โ
subclass can access
}
}
new Animal().name; // โ protected
Protected members are accessible from the class and any subclass.
readonly:
class User {
readonly id: number;
constructor(id: number) {
this.id = id;
}
}
Readonly members can be assigned only in the constructor.
Combining modifiers:
class Config {
private readonly secret: string = 'x';
protected readonly version: number = 1;
public readonly name: string = 'config';
}
Modifiers compose: private readonly means both restrictions apply.
private is compile-time only: Unlike JavaScript’s #private fields, TypeScript’s private is erased at runtime. It’s a compile-time check, not a runtime guarantee.
class User {
private secret = 'x';
}
const u = new User();
(u as any).secret; // โ
compiles โ bypasses check
The private keyword prevents access from TypeScript code, but JavaScript can still reach the property.
JavaScript’s #private โ runtime private:
class User {
#secret = 'x';
reveal(): string {
return this.#secret;
}
}
const u = new User();
u.#secret; // โ syntax error, not just type error
# fields are actually private at runtime โ no bypass. TypeScript supports them natively.
Comparison:
| Feature | private | #private |
|---|---|---|
| Runtime enforced | โ | โ |
Accessible via as any | โ | โ |
| TypeScript support | โ | โ |
| Use | Conventional | True encapsulation |
Why access modifiers matter: They express intent and catch mistakes. A private method can’t be called from outside; a protected method is available to subclasses. The compiler enforces the rules. It’s the same idea as interfaces โ establishing a contract about what’s part of the public API.
Why
#privateis the modern choice: It’s actually private at runtime. TypeScript’sprivateis a compile-time convention โ anyone can bypass it withas any.#fields are enforced by the JavaScript engine. For real encapsulation, use#. For convention and tooling,privateis fine.
Constructors and dependency injection
The constructor is where dependencies enter a class โ the pattern Angular and NestJS rely on.
Constructor injection:
class UserService {
constructor(private http: HttpClient) {}
getUser(id: number): Promise<User> {
return this.http.get(`/users/${id}`);
}
}
The parameter property private http: HttpClient declares and assigns the dependency. The class doesn’t create the HttpClient โ it receives one.
Why this pattern: Testable, flexible, explicit. You can pass a real HttpClient in production and a mock in tests. The class doesn’t know or care which โ it just uses what it was given.
Multiple dependencies:
class OrderService {
constructor(
private http: HttpClient,
private logger: Logger,
private config: Config
) {}
async placeOrder(order: Order): Promise<void> {
this.logger.info('Placing order');
await this.http.post('/orders', order);
}
}
Framework integration: In Angular, decorators like @Injectable() handle the wiring. In NestJS, @Injectable() and constructor injection are the standard pattern. In plain TypeScript, you pass the dependencies manually.
Without a framework:
const http = new HttpClient();
const logger = new Logger();
const orderService = new OrderService(http, logger, config);
With a DI container: Frameworks like InversifyJS provide the container that instantiates and wires classes.
Why constructor injection is idiomatic: It’s explicit, testable, and works everywhere. No global state, no service locator, no magic โ just parameters. The class declares what it needs, and whoever creates it provides those dependencies.
Why DI is important in large codebases: When classes create their own dependencies (
this.http = new HttpClient()), they’re coupled to specific implementations and hard to test. Constructor injection decouples them โ the class depends on an interface, and the caller decides what to pass. That’s the foundation of testable, maintainable code.
A full example
A class hierarchy modeling users and admins.
// ============================================
// BASE CLASS
// ============================================
class User {
protected readonly id: number;
public name: string;
private email: string;
constructor(id: number, name: string, email: string) {
this.id = id;
this.name = name;
this.email = email;
}
getId(): number {
return this.id;
}
getEmail(): string {
return this.email;
}
greet(): string {
return `Hello, ${this.name}!`;
}
static create(name: string, email: string): User {
return new User(Math.floor(Math.random() * 1000), name, email);
}
}
// ============================================
// SUBCLASS
// ============================================
class Admin extends User {
private permissions: Set<string> = new Set();
constructor(id: number, name: string, email: string) {
super(id, name, email);
}
grant(permission: string): this {
this.permissions.add(permission);
return this;
}
can(permission: string): boolean {
return this.permissions.has(permission);
}
override greet(): string {
return `Admin ${this.name}`;
}
}
// ============================================
// USAGE
// ============================================
const alice = User.create('Alice', 'alice@example.com');
console.log(alice.greet());
console.log(alice.getId());
const admin = new Admin(2, 'Bob', 'bob@example.com')
.grant('write')
.grant('delete');
console.log(admin.greet()); // Admin Bob
console.log(admin.can('write')); // true
console.log(admin.can('read')); // false
What this shows:
protected readonly idโ accessible to subclasses, immutable after constructionprivate emailโ accessible only inside the class- Parameter properties (
public name) โ declare and assign in one line - Static
createโ factory method overrideโ explicit override of the base method- Fluent chain โ
grantreturnsthis
Why this shape: It’s the essential class toolkit โ properties, methods, constructor parameters, access modifiers, inheritance, static methods. Real classes use all of these. The example is small enough to read at once but exercises every concept.
Complete Example Session
# ============================================
# PART 1: BASIC CLASS
# ============================================
cat > basics.ts << 'EOF'
class User {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
greet(): string {
return `Hello, ${this.name}!`;
}
}
const alice = new User(1, 'Alice');
console.log(alice.greet());
console.log(alice.id, alice.name);
EOF
npx tsc --noEmit basics.ts
# (no errors)
# ============================================
# PART 2: TRIGGER TYPE ERRORS
# ============================================
cat > errors.ts << 'EOF'
class User {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
const a = new User(1, 'Alice');
// new User('1', 'Alice'); // โ wrong arg type
// new User(1); // โ missing arg
console.log(a.missing); // โ property doesn't exist
EOF
npx tsc --noEmit errors.ts
# [ errors.ts:15:13 - Property 'missing' does not exist on type 'User'. ]
rm errors.ts
# ============================================
# PART 3: ACCESS MODIFIERS
# ============================================
cat > access.ts << 'EOF'
class User {
public name: string;
private email: string;
protected readonly id: number;
constructor(id: number, name: string, email: string) {
this.id = id;
this.name = name;
this.email = email;
}
getEmail(): string {
return this.email;
}
}
class Admin extends User {
role = 'admin';
show(): string {
return `Admin #${this.id}`; // โ
protected
}
}
const a = new User(1, 'Alice', 'a@x.com');
// a.email; // โ private
// a.id; // โ protected
console.log(a.name, a.getEmail());
const admin = new Admin(2, 'Bob', 'b@x.com');
console.log(admin.show());
EOF
npx tsc --noEmit access.ts
# (no errors)
# ============================================
# PART 4: PARAMETER PROPERTIES
# ============================================
cat > params.ts << 'EOF'
class UserService {
constructor(
public id: number,
public name: string,
private email: string
) {}
getEmail(): string {
return this.email;
}
}
const s = new UserService(1, 'Alice', 'a@x.com');
console.log(s.id, s.name, s.getEmail());
EOF
npx tsc --noEmit params.ts
# (no errors)
# ============================================
# PART 5: INHERITANCE
# ============================================
cat > inherit.ts << 'EOF'
class Animal {
constructor(public name: string) {}
speak(): string {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name);
}
override speak(): string {
return `${this.name} barks`;
}
}
const d = new Dog('Rex', 'Labrador');
console.log(d.speak(), d.breed);
EOF
npx tsc --noEmit inherit.ts
# (no errors)
# ============================================
# PART 6: STATIC AND PRIVATE CONSTRUCTOR
# ============================================
cat > static.ts << 'EOF'
class Singleton {
private static instance: Singleton | null = null;
static count = 0;
private constructor(public id: number) {
Singleton.count++;
}
static getInstance(): Singleton {
if (!this.instance) this.instance = new Singleton(1);
return this.instance;
}
}
const a = Singleton.getInstance();
const b = Singleton.getInstance();
console.log(a === b); // true
console.log(Singleton.count); // 1
EOF
npx tsc --noEmit static.ts
# (no errors)
# ============================================
# PART 7: COMPILE AND RUN
# ============================================
npx tsc basics.ts access.ts params.ts inherit.ts static.ts
node basics.js
# [ Hello, Alice! ]
# [ 1 Alice ]
node access.js
# [ Alice a@x.com ]
# [ Admin #2 ]
node params.js
# [ 1 Alice a@x.com ]
node inherit.js
# [ Rex barks Labrador ]
node static.js
# [ true ]
# [ 1 ]
Quick Reference
Class Syntax
| Part | Example |
|---|---|
| Property | name: string = '' |
| Method | greet(): string { } |
| Constructor | constructor(id: number) { } |
| Static | static count = 0 |
| Readonly | readonly id: number |
| Optional | nickname?: string |
| Definite | id!: number |
Access Modifiers
| Modifier | Access |
|---|---|
public | Anywhere (default) |
private | Class only |
protected | Class + subclasses |
readonly | Assignable in constructor |
#private | Runtime private |
Constructor Parameter Properties
| Syntax | Effect |
|---|---|
constructor(public x: T) | Property + assign |
constructor(private x: T) | Private property + assign |
constructor(protected x: T) | Protected + assign |
constructor(readonly x: T) | Readonly + assign |
constructor(x: T) | Parameter only |
Property Initialization
| Form | Meaning |
|---|---|
name = 'x' | Inline default |
name: string | Must assign in constructor |
name?: string | Optional |
name!: string | Definite assignment |
readonly name = 'x' | Immutable |
this Return Type
| Return | Preserves subclass |
|---|---|
this | โ |
ClassName | โ loses subclass |
void | โ no chaining |
Static Members
| Form | Access |
|---|---|
static count = 0 | Class.count |
static create(): T | Class.create() |
static readonly MAX = 100 | Class.MAX |
static { } | Static block |
Inheritance
| Keyword | Use |
|---|---|
extends | Subclass |
super(...) | Call parent constructor |
super.method() | Call parent method |
override | Explicitly override |
Modifiers vs #
private | #private | |
|---|---|---|
| Runtime enforced | โ | โ |
Bypass via as any | โ | โ |
| TypeScript support | โ | โ |
| Use | Convention | Real privacy |
Comparison with Interfaces
| Aspect | Class | Interface |
|---|---|---|
| Runtime | โ exists | โ erased |
| Implementation | โ | โ |
| Constructor | โ | โ |
| State | โ | โ (shape only) |
| Implements | โ | N/A |
Comparison with Plain Objects
| Aspect | Class | Plain object |
|---|---|---|
| Methods | โ | Functions only |
| Private state | โ | โ |
| Inheritance | โ | โ (composition) |
| Serialization | โ ๏ธ loses class | โ |
| Simple cases | Overkill | โ |
Constructor Types
| Type | Use |
|---|---|
| Regular | General |
| Parameter properties | DI, simple cases |
| Private | Singleton, factory-only |
| Protected | Abstract-like |
| Overloaded | Rare |
Common Patterns
| Pattern | Example |
|---|---|
| DI | constructor(private http: HttpClient) |
| Factory | static create(...) |
| Singleton | private constructor |
| Fluent | method(): this |
| Abstract | abstract class |
| Immutable | readonly + constructor |
Method Overloading
| Step | Code |
|---|---|
| Overload 1 | parse(s: string): object; |
| Overload 2 | parse(n: number): number; |
| Implementation | parse(x: string | number) { } |
Best Practices
โ Do This:
// Type properties explicitly
class User {
id: number = 0;
name: string = '';
} // โ
// Initialize all properties
class User {
id: number;
constructor(id: number) { this.id = id; }
} // โ
// Use parameter properties for simple cases
class Service {
constructor(private http: HttpClient) {}
} // โ
// Use readonly for immutable fields
class User {
readonly id: number;
constructor(id: number) { this.id = id; }
} // โ
// Use private for internal state
class Counter {
private count = 0;
increment(): void { this.count++; }
} // โ
// Use this return for fluent methods
class Builder {
add(x: number): this { return this; }
} // โ
// Use static factory methods when helpful
class User {
static create(name: string): User { return new User(name); }
private constructor(public name: string) {}
} // โ
// Use #private for real encapsulation
class Secret {
#value = 'x';
get() { return this.#value; }
} // โ
// Prefer composition over deep inheritance
class Logger { log(msg: string) {} }
class Service {
constructor(private logger: Logger) {}
} // โ
โ Don’t Do This:
// Don't leave properties uninitialized
class User {
id: number; // โ strict error // โ
}
// Don't use `!` casually
class User {
id!: number; // โ ๏ธ only if truly assigned elsewhere // โ ๏ธ
}
// Don't use `any` for properties
class User {
data: any; // โ
}
// Don't mutate readonly after construction
class User {
readonly id = 1;
change() { this.id = 2; } // โ // โ
}
// Don't call `this` before `super` in subclass
class D extends B {
constructor() {
this.x = 1; // โ super must be called first // โ
super();
}
}
// Don't overuse inheritance
class A extends B extends C extends D { } // โ ๏ธ deep hierarchy
// Don't expose internal state directly
class Cache {
data = {}; // โ ๏ธ use private // โ ๏ธ
}
// Don't use `private` for real secrecy
class Secret {
private password = 'x'; // โ ๏ธ bypassable with as any // โ ๏ธ
}
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Uninitialized property | Strict error | Initialize or use ! |
this before super | Runtime error | Call super first |
Missing override | Silent override | Add override |
private thought as runtime | Bypassable via as any | Use # for real privacy |
| Deep inheritance | Fragile | Prefer composition |
! on unassigned property | Runtime undefined | Initialize properly |
| Wrong constructor args | Compile error | Match the signature |
| Static state shared | Surprising mutations | Be careful with static |
| Overriding constructor signature | Confusing | Follow Liskov |
| Type-only import | Value needed at runtime | Import the class properly |
Real-World Examples
1. Basic class
class User {
constructor(public name: string) {}
}
2. With methods
class User {
name = '';
greet(): string { return `Hi, ${this.name}`; }
}
3. Readonly property
class User {
readonly id: number;
constructor(id: number) { this.id = id; }
}
4. Private property
class User {
private secret = 'x';
get() { return this.secret; }
}
5. Protected property
class Base {
protected value = 0;
}
class Child extends Base {
show() { return this.value; }
}
6. Parameter properties
class Service {
constructor(private http: HttpClient, public name: string) {}
}
7. Static factory
class User {
static create(name: string): User { return new User(name); }
private constructor(public name: string) {}
}
8. Static property
class Counter {
static count = 0;
}
9. Fluent chain
class Builder {
values: number[] = [];
add(n: number): this { this.values.push(n); return this; }
}
10. Inheritance
class Animal {
constructor(public name: string) {}
}
class Dog extends Animal {
constructor(name: string, public breed: string) { super(name); }
}
11. Override method
class A { speak(): string { return 'A'; } }
class B extends A {
override speak(): string { return 'B'; }
}
12. Abstract class
abstract class Shape {
abstract area(): number;
}
class Circle extends Shape {
constructor(private r: number) { super(); }
area() { return Math.PI * this.r ** 2; }
}
13. Optional constructor param
class User {
constructor(public id: number, public nickname?: string) {}
}
14. Default constructor param
class Config {
constructor(public host = 'localhost', public port = 8080) {}
}
15. Private constructor (singleton)
class Singleton {
private static instance: Singleton;
private constructor() {}
static get(): Singleton {
return this.instance ??= new Singleton();
}
}
16. #private field
class Secret {
#value = 'x';
get() { return this.#value; }
}
17. Method overloading
class Parser {
parse(s: string): object;
parse(n: number): number;
parse(x: string | number) { return typeof x === 'string' ? {} : x; }
}
18. Implement interface
interface Comparable<T> {
compareTo(other: T): number;
}
class Version implements Comparable<Version> {
constructor(public v: string) {}
compareTo(other: Version) { return this.v.localeCompare(other.v); }
}
19. Dependency injection
class UserService {
constructor(private http: HttpClient, private logger: Logger) {}
}
20. Getters and setters
class Temp {
private _c = 0;
get celsius() { return this._c; }
set celsius(v: number) { this._c = v; }
get fahrenheit() { return this._c * 9/5 + 32; }
}
Visual: Class Anatomy
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class User { โ
โ โ
โ // properties โ
โ id: number โ
โ name: string = '' โ
โ readonly createdAt: Date โ
โ private secret: string โ
โ โ
โ // static โ
โ static count = 0 โ
โ โ
โ // constructor โ
โ constructor(id: number, name: string) { โ
โ this.id = id โ
โ this.name = name โ
โ } โ
โ โ
โ // methods โ
โ greet(): string { } โ
โ private validate(): boolean { } โ
โ static create(name: string): User { } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Access Modifiers
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ public โ
โ โ anywhere โ
โ โ default โ
โ โ
โ protected โ
โ โ class + subclasses โ
โ โ
โ private โ
โ โ class only โ
โ โ
โ #private โ
โ โ class only, runtime enforced โ
โ โ
โ readonly โ
โ โ constructor only โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Outside Subclass Same class โ
โ โโโโโโโโ โโโโโโโโ โโโโโโโโโโ โ
โ public โ
โ
โ
โ
โ protectedโ โ
โ
โ
โ private โ โ โ
โ
โ #private โ โ โ
โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Parameter Properties
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Longhand โ
โ โ
โ class User { โ
โ name: string; โ
โ email: string; โ
โ โ
โ constructor(name: string, email: string) {โ
โ this.name = name; โ
โ this.email = email; โ
โ } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Shorthand โ
โ โ
โ class User { โ
โ constructor( โ
โ public name: string, โ
โ public email: string โ
โ ) {} โ
โ } โ
โ โ
โ Same result โ less code โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Inheritance Chain
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class Animal { โ
โ constructor(public name: string) {} โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โฒ
โ extends
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class Dog extends Animal { โ
โ constructor(name: string, breed: string) {โ
โ super(name); // โ must call first โ
โ } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Rules: โ
โ โข super() must be called before this access โ
โ โข only one parent class โ
โ โข methods can be overridden โ
โ โข use `override` keyword explicitly โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Static vs Instance
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Static โ on the class โ
โ โ
โ User.count โ class property โ
โ User.create() โ class method โ
โ โ
โ Shared across all instances โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Instance โ on the object โ
โ โ
โ user.name โ instance property โ
โ user.greet() โ instance method โ
โ โ
โ Each instance has its own โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: DI via Constructor
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class UserService { โ
โ constructor( โ
โ private http: HttpClient, โ
โ private logger: Logger โ
โ ) {} โ
โ โ
โ async getUser(id: number) { โ
โ this.logger.info('fetching'); โ
โ return this.http.get(`/users/${id}`); โ
โ } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ caller provides
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ const http = new HttpClient(); โ
โ const logger = new Logger(); โ
โ const service = new UserService(http, logger)โ
โ โ
โ โ easy to swap for tests โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: this Return Type
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class Builder { โ
โ add(x: number): this { return this; } โ
โ } โ
โ โ
โ class FluentBuilder extends Builder { โ
โ name(s: string): this { return this; } โ
โ } โ
โ โ
โ new FluentBuilder() โ
โ .add(1) โ
โ .name('x') โ works โ `this` preserved โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ If return type was `Builder`: โ
โ โ
โ new FluentBuilder() โ
โ .add(1) โ
โ .name('x') โ name not on Builder โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Class vs Interface
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ interface โ shape only โ
โ โ
โ interface User { โ
โ id: number; โ
โ name: string; โ
โ greet(): string; โ
โ } โ
โ โ
โ โข exists at type level โ
โ โข no implementation โ
โ โข erased at runtime โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class โ shape + implementation โ
โ โ
โ class User { โ
โ id = 0; โ
โ name = ''; โ
โ greet() { return `Hi ${this.name}`; } โ
โ } โ
โ โ
โ โข exists at runtime โ
โ โข has implementation โ
โ โข has constructor, static, private โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: private vs #private
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TypeScript `private` โ
โ โ
โ class A { private x = 1; } โ
โ โ
โ const a = new A(); โ
โ a.x; โ compile error โ
โ (a as any).x; โ
bypasses check โ
โ โ
โ โ compile-time only โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JavaScript `#private` โ
โ โ
โ class A { #x = 1; } โ
โ โ
โ const a = new A(); โ
โ a.#x; โ syntax error โ
โ (a as any).#x; โ still fails โ
โ โ
โ โ runtime enforced โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Class Design Checklist
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Before writing a class: โ
โ โ
โ [ ] Needs state + behavior? โ
โ [ ] Needs private state? โ
โ [ ] Needs inheritance? โ
โ [ ] Will be serialized? โ
โ [ ] Needs DI? โ
โ [ ] Better as a plain object? โ
โ โ
โ If "better as plain object" โ skip class. โ
โ Otherwise โ class with typed properties. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Concept | Meaning |
|---|---|
| Class | Blueprint for objects with state and behavior |
| Property | Typed field on instances |
| Method | Typed function on instances |
| Constructor | Initializes instances |
readonly | Assignable only in constructor |
static | On the class, not instances |
public | Accessible anywhere (default) |
private | Class only (compile-time) |
protected | Class and subclasses |
#private | Runtime-enforced private |
| Parameter property | Constructor shorthand for property + assign |
this return | Preserves subclass in chains |
override | Explicit override of base method |
super | Call parent constructor or method |
Key takeaways:
- Classes bundle state and behavior โ properties, methods, and a constructor
- Every property and method can be typed โ the compiler checks them
- Under
strictPropertyInitialization, every property must be initialized at declaration or in the constructor - Parameter properties โ
constructor(private http: HttpClient)โ declare and assign in one line readonlyallows assignment only in the constructor;private/protectedcontrol access#privateis real runtime privacy; TypeScript’sprivateis compile-time onlystaticmembers live on the class, not instances โ factory methods and shared statesuper()must be called beforethisin a subclassoverridemakes overriding explicit โ catches typosthisreturn type preserves the subclass through fluent chains- Constructor injection is the standard DI pattern โ testable and explicit
- Prefer composition over deep inheritance โ pass dependencies instead of building tall hierarchies
Remember: Classes in TypeScript are JavaScript classes with a type layer. Type your properties, methods, and constructor parameters โ the compiler catches wrong initialization, wrong arguments, and wrong usage. Use access modifiers to control who can do what, and parameter properties for concise DI. Reach for a class when you need state with behavior, private state, or inheritance. Otherwise, a plain object and functions will do.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!