Skip to main content

TypeScript Classes

From SCRUM-39: Implement DictionaryAdapter

My Learning Notes

TypeScript class is pretty similar to JavaScript class. It's actually more advanced with more features:

  • Ability to specify the type of the variable
  • Ability to specify that the value will be assigned to the variable using definite assignment (!:)

When to Use Class vs Interface

Interface = "what fields does this object have?" (compile-time only, no JS generated)

Class = "what fields AND what can this object do?" (generates real JS code)

// Interface - just data, no behavior
interface Word {
id: string;
word: string;
definition: string;
}

const myWord: Word = { id: "1", word: "hello", definition: "greeting" };

// Class - has methods with real code
class FreeDictionaryAdapter {
private userId: string;

constructor(userId: string) {
this.userId = userId;
}

async lookup(word: string) {
// actual implementation
}

toString() {
return `Adapter for user ${this.userId}`;
}
}

const adapter = new FreeDictionaryAdapter("123");

In VocabPal:

  • Word, Category, Userinterfaces (just data, no behavior)
  • FreeDictionaryAdapterclass (has lookup() method with real code)

Summary: Use class when you want to encapsulate methods/behavior inside the object.


Access Modifiers

TypeScript has access modifiers like Java:

class Example {
public name: string; // accessible anywhere
private secret: string; // only inside this class
protected inherited: string; // inside class and subclasses
}
ModifierInside ClassSubclassOutside
public
protected
private

TS/JS vs Java Encapsulation

In Java, encapsulation is done by using class with access modifier for each field or method.

In TS/JS, it's 2 layers: by using class and by using module import/export feature.

Modules in TS/JS act like packages in Java. Since modules have import/export as access mechanism, we don't need "default" access modifier like Java.


Definite Assignment Assertion

Use !: when you know a property will be assigned (but TypeScript can't verify):

class OKGreeter {
// Not initialized, but we promise it will be
name!: string;
}

strictPropertyInitialization

By default, tsconfig.json sets "strictPropertyInitialization": false. To enable strict checks:

{
"compilerOptions": {
"strict": true // Bundles strictPropertyInitialization with other checks
}
}

With strict mode:

  • Without strictNullChecks: TS ignores undefined, no property errors
  • With both enabled: TS catches uninitialized = possibly undefined properties

Extends vs Implements

extends - Inheritance

Inherits ALL properties + methods + constructor

class Animal {
name: string;
move() { console.log("Moving"); }
}

class Dog extends Animal {
bark() { console.log("Woof!"); }
}

const dog = new Dog();
dog.name = "Rex"; // inherited
dog.move(); // inherited
dog.bark(); // own method

implements - Contract Fulfillment

Must MATCH interface shape (no inheritance)

interface DictionaryAdapter {
lookup(word: string): Promise<Word | null>;
}

class FreeDictionaryAdapter implements DictionaryAdapter {
async lookup(word: string): Promise<Word | null> {
// MUST implement this method
}
}

Key Difference

  • extends = "I inherit from you" (get code for free)
  • implements = "I promise to have these methods" (must write them yourself)

Interfaces CAN Declare Method Signatures

Interfaces can declare method signatures - they just cannot have the implementation (the actual code).

// ✅ Valid - interface declares the method signature (no body)
interface DictionaryAdapter {
lookup(word: string): Promise<Word | null>;
}

// ✅ The CLASS provides the implementation (with body)
class FreeDictionaryAdapter implements DictionaryAdapter {
async lookup(word: string): Promise<Word | null> {
// actual code goes here
const apiResponse = await fetchWordDefinition(word);
// ...
}
}

Think of it this way:

  • Interface = "Here are the methods you MUST have" (the contract)
  • Class = "Here is HOW those methods actually work" (the implementation)

The implements keyword says "this class promises to have all the methods declared in that interface."


What's Allowed in TypeScript

StatementAllowed?
interface extends class✅ (copies shape)
interface extends interface(s)
class extends class
class implements interface
class implements class✅ (checks structure)

Extends in Java vs TypeScript

Java (nominal typing):

  • Classes and interfaces are separate "family trees"
  • extends = family inheritance (must be same family)
  • You ARE what you're declared as

TypeScript (structural typing):

  • No strict family trees
  • extends = copy the shape/code
  • You ARE what you look like
// TypeScript doesn't care about "family"
class Point { x: number; y: number; }

// Anything with x and y is compatible
const p: Point = { x: 1, y: 2 }; // ✅ plain object works!
// Java cares about family
class Point { int x; int y; }

Point p = new Point(); // ✅
Point p = someRandomObject; // ❌ even if it has x and y

"Family passing down" vs "copying shape" is a good way to remember the difference between Java's inheritance and TypeScript's inheritance.


VocabPal Example: Adapter Pattern

// Interface defines the contract
export interface DictionaryAdapter {
lookup(word: string): Promise<Omit<Word, "id"|"createdAt"|"updatedAt"> | null>;
}

// Class implements the contract
export class FreeDictionaryAdapter implements DictionaryAdapter {
private userId: string;
private sourceUrl?: string;

constructor(userId: string, sourceUrl?: string) {
this.userId = userId;
this.sourceUrl = sourceUrl;
}

async lookup(word: string): Promise<Omit<Word, "id"|"createdAt"|"updatedAt"> | null> {
const apiResponse = await fetchWordDefinition(word);
if (!apiResponse) return null;
return this.transformToWord(apiResponse, word);
}

private transformToWord(apiResponse: any, originalWord: string) {
return {
word: apiResponse.word || originalWord,
definition: apiResponse.meanings?.[0]?.definitions?.[0]?.definition || '',
userId: this.userId,
sourceUrl: this.sourceUrl,
};
}
}

Notes from SCRUM-39

Synced from Jira on 2026-04-04

In Java, an instance of Parent type can be of Child class. Parent X = new Child(). X calls Parent’s fields and has Child’s behaviours/methods. This way we say Child modifies Parent’s methods/behaviour (Polymorphism). To force X to call Child’s fields ( include those not exist in Parent) we need to downcast X : Child X_down = (Child) X. Note that here we need to do type-check for X to ensure X is of Parent type before downcasting otherwise we will get ClassCastException

In JavaScript/TS, X always declared with Parent type, and it calls Child’s fields and methods without the need of downcast. However, this way it can only call fields those are co-exist in both Parent and Child class. Hence, we downcast X : const X_down = X as Child. → At runtime though X is not child, no error is thrown as TS trust the declaration “as Child”

Subclass in TS is structural, it just needs to have Parent’s structure of the class then we can assign const p: ParentLike = new Something()

“Extend” keyword is used as a quick way to copy parent’s structure in a systematic way that it has prototype chain. This helps save the memory because by “extend”-ing, we have parent’s structure without creating a real copy, it’s more like referencing. Chaining also allows the use of “super()” method.


Answer: Interfaces CAN declare method signatures

In TypeScript, an interface can declare method signatures - it just cannot have the implementation (the actual code).

// ✅ Valid - interface declares the method signature (no body)
interface DictionaryAdapter {
lookup(word: string): Promise<Omit<Word, "id"|"createdAt"|"updatedAt">>;
}

// ✅ The CLASS provides the implementation (with body)
class FreeDictionaryAdapter implements DictionaryAdapter {
async lookup(word: string): Promise<Omit<Word, "id"|"createdAt"|"updatedAt">> {
// actual code goes here
const apiResponse = await fetchWordDefinition(word);
// ...
}
}

Think of it this way:

  • Interface = "Here are the methods you MUST have" (the contract)
  • Class = "Here is HOW those methods actually work" (the implementation)

The implements keyword says "this class promises to have all the methods declared in that interface." The adapter pattern relies on interfaces to define contracts that multiple classes can implement.


Notes: how to use “implements” and “extends”?

  • Extends : interface has shape, class has shape and code. Extends is a way to copy either shape or both shape and code. Hence, interface can extend a class to copy the shape of the class, but class cannot extend an interface because interface has no code for the class to copy. In short, these are allowed: interface extends class, class extends class, class extends interface
  • Implements: It’s like a promise to follow a certain shape. This can only be used by class. In short, these are allowed: class implements class, class implements interface

**Note: when to use class and when to use interface? **

Interface = "what fields does this object have?" (compile-time only, no JS generated)

interface Word {
id: string;
word: string;
definition: string;
}

const myWord: Word = { id: "1", word: "hello", definition: "greeting" };

Class = "what fields AND what can this object do?" (generates real JS code)

class FreeDictionaryAdapter {
private userId: string;

constructor(userId: string) {
this.userId = userId;
}

async lookup(word: string) {
// actual implementation
}

toString() {
return `Adapter for user ${this.userId}`;
}
}

const adapter = new FreeDictionaryAdapter("123");

In VocabPal:

  • Word, Category, User → interfaces (just data, no behavior)
  • FreeDictionaryAdapter → class (has lookup() method with real code)

Summary: use class when you want to encapsulate methods/behavior inside the object.


Note: Extends keyword in Java vs Typescript

Java (nominal typing):

  • Classes and interfaces are separate "family trees"
  • extends = family inheritance (must be same family)
  • You ARE what you're declared as

TypeScript (structural typing):

  • No strict family trees
  • extends = copy the shape/code
  • You ARE what you look like
// TypeScript doesn't care about "family"
class Point { x: number; y: number; }

// Anything with x and y is compatible
const p: Point = { x: 1, y: 2 }; // ✅ plain object works

// Java cares about family
class Point { int x; int y; }

// Must be actual Point instance
Point p = new Point(); // ✅
Point p = someRandomObject; // ❌ even if it has x and y

"family passing down" vs "copying shape" is a good way to remember distinguishing between TS’s inheritance and Java inheritance.

This explains why interface extends class in Typescript while it’s not allowed in Java. Java’s interface can only extends other interface(s).

What’s allowed in Java:

  • interface extends interface(s)
  • class extends class

What’s allowed in TS:

  • interface extends class
  • class extends class
  • interface extends interface(s)

Note: Implements keyword in Java vs Typescript

Implements keyword is used exclusively in Java to copy structure of interfaces (it means provide implementation of the interface) . While in TS, implements can be used for checking if the class has the implementation with the right structure (means a class that has the structure of that class or interface)

What allowed in Java:

  • Class implements interface(s)

What allowed in TS:

  • class implements class
  • class implements interface

Code Review Feedback

Katie Nguyen (2026-02-05):

[Code Review - Changes Needed]

Issues Found

1. Missing interface declaration (Major) - The ticket asks for a DictionaryAdapter interface, but you created a class. The adapter pattern requires an interface that defines the contract, and a class that implements it.

Current: class DictionaryAdapter / class FreeDictionaryAdapter extends DictionaryAdapter

Should be: interface DictionaryAdapter / class FreeDictionaryAdapter implements DictionaryAdapter

2. Missing exports (Major) - Neither DictionaryAdapter nor FreeDictionaryAdapter are exported. Other code cannot use them.

3. Extra space in class name (Minor) - Line 24 has two spaces before FreeDictionaryAdapter.

4. No trailing newline (Minor)

What is Good

  • The lookup() method logic is correct
  • Good use of Omit for the return type
  • Error handling for word not found
  • Constructor takes userId and optional sourceUrl

Katie Nguyen (2026-02-05):

[Code Review - APPROVED]

All issues fixed: DictionaryAdapter interface created in types/index.ts, FreeDictionaryAdapter class uses implements keyword, both are exported, extra space removed.

Great job understanding the difference between interface (contract) and class (implementation). Ready to move to Done.

Key Takeaways

  • Use interface for data shapes (no methods)
  • Use class for objects with behavior (methods)
  • extends = inherit code, implements = fulfill contract
  • Interfaces declare method signatures, classes provide implementations
  • TypeScript uses structural typing (shape matters, not family)
  • Access modifiers: public, private, protected
  • Use !: for definite assignment when you know a property will be set