Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Classes & OOP
TypeScript adds powerful features to JavaScript classes: access modifiers, abstract classes, and more. It makes object-oriented programming feel natural and type-safe. While JavaScript classes are essentially syntactic sugar over prototypal inheritance, TypeScript transforms them into proper OOP constructs with compile-time visibility enforcement, abstract contracts, and type-safe generics. If you come from Java or C#, TypeScript classes will feel familiar. If you come from pure JavaScript, they will feel like a major upgrade in safety and expressiveness.1. Class Basics
Basic Class Structure
Parameter Properties (Shorthand)
TypeScript can automatically create and assign properties from constructor parameters. This is one of the most loved TypeScript features because it eliminates the tedious boilerplate of declaring a property, adding a constructor parameter, and assigning one to the other:2. Access Modifiers
Control visibility of class members. Access modifiers are the compiler-enforced equivalent of “do not touch” signs. They tell other developers (and the compiler) which parts of a class are part of its public API and which are internal implementation details. This matters enormously for maintainability: if a property isprivate, you can change or remove it without worrying about breaking consumers.
public (Default)
Accessible from anywhere. This is the default in TypeScript, just like JavaScript.private
Only accessible within the class.protected
Accessible within the class and subclasses.Private Fields (ES2022)
True private fields using# syntax.
3. Readonly Properties
Properties that can only be set in the constructor.Combining with Parameter Properties
4. Getters and Setters
Define computed properties with validation.5. Static Members
Properties and methods on the class itself, not instances.Singleton Pattern
6. Inheritance
Extend classes to create specialized versions.Method Overriding with override
7. Abstract Classes
Define templates for subclasses. Cannot be instantiated directly. Abstract classes are a “contract with benefits” — like an interface, they define what subclasses must implement, but unlike an interface, they can also provide shared implementation code. This is the sweet spot between “pure interface” (no shared code) and “concrete base class” (forces a specific implementation).8. Implementing Interfaces
Classes can implement one or more interfaces. Theimplements keyword is a compile-time contract: it guarantees that the class provides every property and method the interface requires. If you add a new method to the interface, every implementing class that is missing it immediately gets a compiler error. This is one of the most powerful patterns for enforcing consistency across a codebase.
Interface vs Abstract Class
| Feature | Interface | Abstract Class |
|---|---|---|
| Multiple inheritance | ✅ Yes | ❌ No |
| Implementation code | ❌ No | ✅ Yes |
| Properties with values | ❌ No | ✅ Yes |
| Constructor | ❌ No | ✅ Yes |
| Access modifiers | ❌ No | ✅ Yes |
9. Generic Classes
Classes that work with multiple types.Generic Constraints in Classes
10. Decorators
Decorators are a way to add metadata or modify class behavior declaratively using@ syntax. They are heavily used in frameworks like NestJS, Angular, and TypeORM. TypeScript has had experimental support for years, and the TC39 proposal recently reached Stage 3, meaning the syntax is stabilizing. Decorators shine when you want to add cross-cutting concerns (logging, validation, caching, authorization) without polluting business logic.
Class Decorator
Decorator Factory
Method Decorator
Property Decorator
11. Practical Example: Service Layer
Summary
| Concept | Example |
|---|---|
| Class Definition | class User { name: string } |
| Constructor | constructor(name: string) {} |
| Parameter Property | constructor(public name: string) {} |
| Access Modifiers | public, private, protected |
| Private Fields | #privateField |
| Readonly | readonly id: number |
| Getters/Setters | get value(), set value(v) |
| Static Members | static PI = 3.14 |
| Inheritance | class Dog extends Animal |
| Abstract Class | abstract class Shape |
| Implement Interface | class Doc implements Printable |
| Generic Class | class Stack<T> |
| Decorators | @Logger class User |
Interview Deep-Dive
Q: Compare TypeScript's 'private' keyword versus JavaScript's '#' private fields. When would you choose one over the other?
Q: Compare TypeScript's 'private' keyword versus JavaScript's '#' private fields. When would you choose one over the other?
privatekeyword (TypeScript-only): Enforced only at compile time. The compiled JavaScript has no concept of private — the property is a regular public property on the object. If someone casts toanyor accesses the object from plain JavaScript, the “private” field is fully accessible.(account as any).balancebypasses the protection entirely. However,privateintegrates naturally with TypeScript’s type system, works with access modifier parameter properties (constructor(private balance: number)), and produces clear error messages.#private fields (JavaScript-native): Enforced at runtime by the JavaScript engine.#balanceis truly inaccessible from outside the class — no cast, no escape hatch, no workaround.account.#balanceis a syntax error even in plain JavaScript. The engine uses a WeakMap-like mechanism internally to make the field genuinely unreachable.- When to choose
private: When you are writing a fully TypeScript codebase where every consumer goes through the type system. It is simpler syntax, integrates with parameter properties, and the IDE experience is slightly better (autocomplete correctly hides private members). Most application code falls here. - When to choose
#: When you are writing a library consumed by JavaScript users, when you need security guarantees (the field cannot be accessed even by malicious or buggy code), or when the object will be serialized/deserialized and you need the private field to survive the round trip without appearing inJSON.stringify()output.#fields are excluded fromfor...in,Object.keys(), andJSON.stringify(). - The gotcha with
#: Private fields are per-class, not per-instance. A method inclass BankAccountcan accessother.#balanceifotheris also aBankAccountinstance. This is consistent with how private fields work in Java and C++ but surprises JavaScript developers.
# private fields with TypeScript’s parameter properties shorthand?No. constructor(#balance: number) is a syntax error. Parameter properties only work with TypeScript’s access modifiers (public, private, protected, readonly). If you want # private fields, you must declare them explicitly in the class body and assign in the constructor. This is one of the practical friction points that keeps many teams on private even though # is technically superior for encapsulation.Q: When should you use an abstract class versus an interface in TypeScript? A candidate who says 'they are the same' is missing critical distinctions.
Q: When should you use an abstract class versus an interface in TypeScript? A candidate who says 'they are the same' is missing critical distinctions.
- Abstract classes provide implementation sharing: An abstract class can have concrete methods with real code that subclasses inherit.
abstract class Shape { describe(): string { return 'A ' + this.color + ' shape'; } abstract getArea(): number; }—describe()has a shared implementation,getArea()must be implemented by each subclass. Interfaces cannot provide any implementation. - Interfaces support multiple inheritance: A class can implement multiple interfaces (
class Doc implements Printable, Serializable, Loggable), but it can only extend one abstract class. If you need to compose behaviors from multiple sources, interfaces are the only option. - Abstract classes have runtime presence: They compile to JavaScript classes. You can use
instanceofon them:if (shape instanceof Shape)works. Interfaces are erased — they do not exist at runtime. You cannotinstanceofcheck an interface. - Abstract classes carry constructor constraints: If
abstract class Repositoryhas a constructor that takes aDatabaseConnection, every subclass must callsuper(connection). This enforces initialization requirements. Interfaces have no constructors. - My decision framework: Use an abstract class when you have shared implementation code and a clear “is-a” hierarchy (a
CircleIS aShape). Use an interface when you are defining a capability contract (“can be printed,” “can be serialized”) that unrelated classes might independently satisfy. In practice, lean toward interfaces for dependency injection boundaries (your service depends oninterface UserRepository, notabstract class BaseRepository) because interfaces are lighter and more flexible for testing.
interface Repository<T> { find, create, update, delete }), then create an abstract class that partially implements it (abstract class BaseRepository<T> implements Repository<T> with shared logic for caching or logging), and finally concrete classes that extend the abstract class (class UserRepository extends BaseRepository<User>). Consuming code depends on the interface, enabling mock implementations for testing. The abstract class provides DRY implementation for production classes. This layered approach gives you the best of both worlds: clean contracts at the boundary and shared implementation internally.Q: Explain TypeScript decorators -- how do they work, what are they used for in practice, and what is their current status in the language?
Q: Explain TypeScript decorators -- how do they work, what are they used for in practice, and what is their current status in the language?
- How they work: A decorator is a function that receives the thing it decorates (a class constructor, a method descriptor, or a property descriptor) and can modify or replace it.
@Logger class User {}callsLogger(User)at class definition time. A method decorator like@Log add(a, b)receives the target object, the property name, and the property descriptor, and can wrap the original method with additional behavior (logging, validation, caching). - Practical uses: Dependency injection (Angular’s
@Injectable()), route definition (NestJS’s@Get('/users')), ORM column mapping (TypeORM’s@Column()), validation (@Required,@MinLength(3)), and cross-cutting concerns like logging, caching, and rate limiting. Decorators separate infrastructure concerns from business logic. - The status complication: TypeScript has two decorator implementations. The “legacy” implementation (
experimentalDecorators: truein tsconfig) follows an older TC39 proposal and is what Angular and NestJS use today. The “modern” implementation (TypeScript 5.0+) follows the TC39 Stage 3 proposal with different semantics (decorators receive a context object instead of the three-argumenttarget, propertyKey, descriptor). These two are not compatible. If you are starting a new project without a framework mandate, use the Stage 3 decorators. If you use Angular or NestJS, you must use the legacy decorators until those frameworks migrate. - The gotcha: Decorator factories (decorators that take arguments) return the actual decorator function.
@Logger('INFO')first callsLogger('INFO'), which returns a function, and that function is then called with the class constructor. Missing the factory pattern is a common bug:@Logger('INFO')works, but@Logger(without arguments when it expects them) passes the class constructor as the first argument instead of the prefix string, producing confusing behavior.
descriptor.value (the original function) and replaces it: descriptor.value = function(...args) { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result = original.apply(this, args); cache.set(key, result); return result; }. The this context must be forwarded with apply, not lost through a bare function call. The cache lifetime should be considered — a naive implementation leaks memory. In production, you would add TTL expiration or LRU eviction.Q: The Singleton pattern uses a private constructor in TypeScript. Walk me through the implementation and explain when Singletons are appropriate versus harmful.
Q: The Singleton pattern uses a private constructor in TypeScript. Walk me through the implementation and explain when Singletons are appropriate versus harmful.
private constructor() to prevent external instantiation and a static method to control access to the single instance.- Implementation:
private constructor()makesnew Database()a compile error outside the class.static getInstance()lazily creates the instance on first call and returns the cached instance on subsequent calls.private static instance: Databaseholds the reference. All consumers callDatabase.getInstance()and receive the same object. - When it is appropriate: Database connection pools, configuration managers, and logger instances — resources that are expensive to create, shared across the application, and where having multiple instances would cause problems (multiple connection pools exhausting database connections, conflicting configuration states). In JavaScript specifically, module-level singletons are common because Node.js caches modules —
export const db = new Database()creates a singleton implicitly because the module is only loaded once. - When it is harmful: When it creates hidden dependencies and makes testing difficult. If
UserService.getInstance()internally callsDatabase.getInstance(), you cannot testUserServicewithout a real database. The Singleton hides the dependency — it is not in the constructor signature, so you cannot inject a mock. This is the primary argument against Singletons in modern architecture. - The modern alternative: Dependency injection. Instead of
UserServicereaching forDatabase.getInstance(), the constructor takesDatabaseas a parameter:constructor(private db: Database). In production, a DI container provides the real database. In tests, you inject a mock. The “single instance” guarantee moves from the class itself to the DI container’s configuration (register as singleton scope). This gives you the same runtime behavior with full testability.
private is TypeScript-only and erased at runtime. In the compiled JavaScript, the constructor is a regular public constructor, and new Database() works fine from JavaScript code. To enforce the Singleton at runtime, you would need to check inside the constructor: if (Database.instance) throw new Error('Use getInstance()'), or use # private fields (which have runtime enforcement). This is another case where TypeScript’s compile-time guarantees do not automatically translate to runtime safety — important if your class is consumed by JavaScript code or if you are concerned about reflection-based access.