Project 7.3: Calculator with History
Duration: 2 hours
Project Overview
Build an advanced calculator that tracks all operations in a history, supports undo/redo functionality, and allows saving/loading calculation sessions. This project focuses on using generics, utility types, and implementing design patterns like Command pattern for undo functionality.
Learning Objectives
By completing this project, you will practice:
- Using generics to create reusable data structures
- Implementing utility types for flexible interfaces
- Building an undo/redo system with the Command pattern
- Managing state with a history stack
- Creating a fluent API for chained operations
- Persisting and restoring application state
Requirements
Functional Requirements
- Basic Operations: Addition, subtraction, multiplication, division
- Operation History: Track all calculations performed
- Undo/Redo: Reverse and replay operations
- Memory Functions: Store and recall values
- Session Save/Load: Persist history to file
- Clear Functions: Clear current value, history, or all
User Interface
=== Calculator ===
Current value: 0
1. Enter expression (e.g., + 5, * 3)
2. Show history
3. Undo last operation
4. Redo operation
5. Store in memory
6. Recall from memory
7. Clear current
8. Clear all
9. Save session
10. Load session
11. Exit
Enter choice or expression: + 10
Result: 10
Enter choice or expression: * 5
Result: 50
Enter choice or expression: 3
--- History ---
1. 0 + 10 = 10
2. 10 * 5 = 50
Enter choice or expression: undo
Undone: 10 * 5 = 50
Current value: 10
Data Models
// types.ts
/**
* Supported mathematical operations
*/
type Operation = 'add' | 'subtract' | 'multiply' | 'divide';
/**
* Symbols for operations
*/
type OperationSymbol = '+' | '-' | '*' | '/';
/**
* A single calculation entry
*/
interface CalculationEntry {
id: number;
operation: Operation;
operand: number;
previousValue: number;
resultValue: number;
timestamp: Date;
}
/**
* Calculator state
*/
interface CalculatorState {
currentValue: number;
memory: number | null;
history: CalculationEntry[];
undoneHistory: CalculationEntry[];
}
/**
* Session data for persistence
*/
interface SessionData {
state: CalculatorState;
savedAt: string;
version: string;
}
Project Structure
calculator/
├── src/
│ ├── index.ts # Entry point
│ ├── types.ts # Type definitions
│ ├── calculator.ts # Calculator class
│ ├── history.ts # History management
│ ├── operations.ts # Operation definitions
│ ├── storage.ts # Session persistence
│ └── ui.ts # User interface
├── data/
│ └── session.json # Saved session
├── package.json
└── tsconfig.json
Implementation Guide
Step 1: Define Types
// src/types.ts
export type Operation = 'add' | 'subtract' | 'multiply' | 'divide';
export type OperationSymbol = '+' | '-' | '*' | '/';
export interface CalculationEntry {
id: number;
operation: Operation;
operand: number;
previousValue: number;
resultValue: number;
timestamp: string;
}
export interface CalculatorState {
currentValue: number;
memory: number | null;
history: CalculationEntry[];
undoneHistory: CalculationEntry[];
nextId: number;
}
export interface SessionData {
state: CalculatorState;
savedAt: string;
version: string;
}
// Mapping between symbols and operations
export const symbolToOperation: Record<OperationSymbol, Operation> = {
'+': 'add',
'-': 'subtract',
'*': 'multiply',
'/': 'divide',
};
export const operationToSymbol: Record<Operation, OperationSymbol> = {
add: '+',
subtract: '-',
multiply: '*',
divide: '/',
};
// Initial state factory
export function createInitialState(): CalculatorState {
return {
currentValue: 0,
memory: null,
history: [],
undoneHistory: [],
nextId: 1,
};
}
Step 2: Create Operations Module
// src/operations.ts
import { Operation } from './types';
/**
* Error thrown when an operation fails
*/
export class OperationError extends Error {
constructor(message: string) {
super(message);
this.name = 'OperationError';
}
}
/**
* Performs a mathematical operation
*/
export function performOperation(
operation: Operation,
currentValue: number,
operand: number
): number {
switch (operation) {
case 'add':
return currentValue + operand;
case 'subtract':
return currentValue - operand;
case 'multiply':
return currentValue * operand;
case 'divide':
if (operand === 0) {
throw new OperationError('Cannot divide by zero');
}
return currentValue / operand;
default:
throw new OperationError(`Unknown operation: ${operation}`);
}
}
/**
* Reverses an operation (for undo)
*/
export function reverseOperation(
operation: Operation,
currentValue: number,
operand: number
): number {
switch (operation) {
case 'add':
return currentValue - operand;
case 'subtract':
return currentValue + operand;
case 'multiply':
if (operand === 0) {
return 0; // Edge case: original was 0
}
return currentValue / operand;
case 'divide':
return currentValue * operand;
default:
throw new OperationError(`Unknown operation: ${operation}`);
}
}
/**
* Formats an operation for display
*/
export function formatOperation(
operation: Operation,
previousValue: number,
operand: number,
result: number
): string {
const symbols: Record<Operation, string> = {
add: '+',
subtract: '-',
multiply: '*',
divide: '/',
};
return `${previousValue} ${symbols[operation]} ${operand} = ${result}`;
}
/**
* Rounds a number to avoid floating point issues
*/
export function roundResult(value: number, decimals: number = 10): number {
const factor = Math.pow(10, decimals);
return Math.round(value * factor) / factor;
}
Step 3: Create History Manager
// src/history.ts
import { formatOperation } from './operations';
import { CalculationEntry, Operation } from './types';
/**
* Generic stack implementation for undo/redo
*/
export class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
clear(): void {
this.items = [];
}
size(): number {
return this.items.length;
}
toArray(): T[] {
return [...this.items];
}
static fromArray<T>(items: T[]): Stack<T> {
const stack = new Stack<T>();
items.forEach((item) => stack.push(item));
return stack;
}
}
/**
* Manages calculation history with undo/redo support
*/
export class HistoryManager {
private history: Stack<CalculationEntry>;
private undoneStack: Stack<CalculationEntry>;
private nextId: number;
constructor(
entries: CalculationEntry[] = [],
undoneEntries: CalculationEntry[] = [],
nextId: number = 1
) {
this.history = Stack.fromArray(entries);
this.undoneStack = Stack.fromArray(undoneEntries);
this.nextId = nextId;
}
/**
* Adds a new entry to history
*/
addEntry(
operation: Operation,
operand: number,
previousValue: number,
resultValue: number
): CalculationEntry {
const entry: CalculationEntry = {
id: this.nextId++,
operation,
operand,
previousValue,
resultValue,
timestamp: new Date().toISOString(),
};
this.history.push(entry);
// Clear redo stack when new operation is performed
this.undoneStack.clear();
return entry;
}
/**
* Undoes the last operation
*/
undo(): CalculationEntry | undefined {
const entry = this.history.pop();
if (entry) {
this.undoneStack.push(entry);
}
return entry;
}
/**
* Redoes the last undone operation
*/
redo(): CalculationEntry | undefined {
const entry = this.undoneStack.pop();
if (entry) {
this.history.push(entry);
}
return entry;
}
/**
* Checks if undo is available
*/
canUndo(): boolean {
return !this.history.isEmpty();
}
/**
* Checks if redo is available
*/
canRedo(): boolean {
return !this.undoneStack.isEmpty();
}
/**
* Gets all history entries
*/
getHistory(): CalculationEntry[] {
return this.history.toArray();
}
/**
* Gets undone entries (for state persistence)
*/
getUndoneHistory(): CalculationEntry[] {
return this.undoneStack.toArray();
}
/**
* Gets the current next ID
*/
getNextId(): number {
return this.nextId;
}
/**
* Gets the last entry without removing it
*/
getLastEntry(): CalculationEntry | undefined {
return this.history.peek();
}
/**
* Clears all history
*/
clear(): void {
this.history.clear();
this.undoneStack.clear();
}
/**
* Gets history formatted for display
*/
getFormattedHistory(): string[] {
return this.history.toArray().map((entry, index) => {
const formatted = formatOperation(
entry.operation,
entry.previousValue,
entry.operand,
entry.resultValue
);
return `${index + 1}. ${formatted}`;
});
}
/**
* Gets the number of entries
*/
size(): number {
return this.history.size();
}
}
Step 4: Create Calculator Class
// src/calculator.ts
import { HistoryManager } from './history';
import { OperationError, performOperation, roundResult } from './operations';
import { CalculationEntry, CalculatorState, Operation, createInitialState } from './types';
/**
* Result of a calculation
*/
export interface CalculationResult {
success: boolean;
value: number;
entry?: CalculationEntry;
error?: string;
}
/**
* Advanced calculator with history and undo/redo
*/
export class Calculator {
private currentValue: number;
private memory: number | null;
private historyManager: HistoryManager;
constructor(state?: CalculatorState) {
if (state) {
this.currentValue = state.currentValue;
this.memory = state.memory;
this.historyManager = new HistoryManager(state.history, state.undoneHistory, state.nextId);
} else {
const initial = createInitialState();
this.currentValue = initial.currentValue;
this.memory = initial.memory;
this.historyManager = new HistoryManager();
}
}
/**
* Gets the current value
*/
getValue(): number {
return this.currentValue;
}
/**
* Sets the current value directly
*/
setValue(value: number): void {
this.currentValue = roundResult(value);
}
/**
* Performs a calculation
*/
calculate(operation: Operation, operand: number): CalculationResult {
try {
const previousValue = this.currentValue;
const result = performOperation(operation, previousValue, operand);
const rounded = roundResult(result);
this.currentValue = rounded;
const entry = this.historyManager.addEntry(operation, operand, previousValue, rounded);
return {
success: true,
value: rounded,
entry,
};
} catch (error) {
return {
success: false,
value: this.currentValue,
error: (error as Error).message,
};
}
}
/**
* Convenience methods for operations
*/
add(operand: number): CalculationResult {
return this.calculate('add', operand);
}
subtract(operand: number): CalculationResult {
return this.calculate('subtract', operand);
}
multiply(operand: number): CalculationResult {
return this.calculate('multiply', operand);
}
divide(operand: number): CalculationResult {
return this.calculate('divide', operand);
}
/**
* Undoes the last operation
*/
undo(): CalculationEntry | null {
if (!this.historyManager.canUndo()) {
return null;
}
const entry = this.historyManager.undo();
if (entry) {
this.currentValue = entry.previousValue;
return entry;
}
return null;
}
/**
* Redoes the last undone operation
*/
redo(): CalculationEntry | null {
if (!this.historyManager.canRedo()) {
return null;
}
const entry = this.historyManager.redo();
if (entry) {
this.currentValue = entry.resultValue;
return entry;
}
return null;
}
/**
* Checks if undo is available
*/
canUndo(): boolean {
return this.historyManager.canUndo();
}
/**
* Checks if redo is available
*/
canRedo(): boolean {
return this.historyManager.canRedo();
}
/**
* Stores current value in memory
*/
memoryStore(): void {
this.memory = this.currentValue;
}
/**
* Recalls value from memory
*/
memoryRecall(): number | null {
return this.memory;
}
/**
* Clears memory
*/
memoryClear(): void {
this.memory = null;
}
/**
* Adds current value to memory
*/
memoryAdd(): void {
if (this.memory !== null) {
this.memory = roundResult(this.memory + this.currentValue);
} else {
this.memory = this.currentValue;
}
}
/**
* Subtracts current value from memory
*/
memorySubtract(): void {
if (this.memory !== null) {
this.memory = roundResult(this.memory - this.currentValue);
} else {
this.memory = -this.currentValue;
}
}
/**
* Gets formatted history
*/
getHistory(): string[] {
return this.historyManager.getFormattedHistory();
}
/**
* Gets the number of history entries
*/
getHistoryCount(): number {
return this.historyManager.size();
}
/**
* Clears the current value
*/
clearCurrent(): void {
this.currentValue = 0;
}
/**
* Clears all (value, memory, and history)
*/
clearAll(): void {
this.currentValue = 0;
this.memory = null;
this.historyManager.clear();
}
/**
* Gets the complete state for persistence
*/
getState(): CalculatorState {
return {
currentValue: this.currentValue,
memory: this.memory,
history: this.historyManager.getHistory(),
undoneHistory: this.historyManager.getUndoneHistory(),
nextId: this.historyManager.getNextId(),
};
}
}
Step 5: Create Storage Module
// src/storage.ts
import * as fs from 'fs';
import * as path from 'path';
import { CalculatorState, SessionData } from './types';
const DATA_DIR = path.join(__dirname, '..', 'data');
const SESSION_FILE = path.join(DATA_DIR, 'session.json');
const VERSION = '1.0.0';
/**
* Ensures the data directory exists
*/
function ensureDataDir(): void {
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
}
/**
* Saves calculator state to a session file
*/
export function saveSession(state: CalculatorState): boolean {
try {
ensureDataDir();
const sessionData: SessionData = {
state,
savedAt: new Date().toISOString(),
version: VERSION,
};
const content = JSON.stringify(sessionData, null, 2);
fs.writeFileSync(SESSION_FILE, content, 'utf-8');
return true;
} catch (error) {
console.error('Failed to save session:', (error as Error).message);
return false;
}
}
/**
* Loads calculator state from session file
*/
export function loadSession(): CalculatorState | null {
try {
if (!fs.existsSync(SESSION_FILE)) {
return null;
}
const content = fs.readFileSync(SESSION_FILE, 'utf-8');
const sessionData: SessionData = JSON.parse(content);
// Version check (future-proofing)
if (sessionData.version !== VERSION) {
console.warn('Session version mismatch, some data may not load correctly');
}
return sessionData.state;
} catch (error) {
console.error('Failed to load session:', (error as Error).message);
return null;
}
}
/**
* Checks if a session file exists
*/
export function sessionExists(): boolean {
return fs.existsSync(SESSION_FILE);
}
/**
* Deletes the session file
*/
export function deleteSession(): boolean {
try {
if (fs.existsSync(SESSION_FILE)) {
fs.unlinkSync(SESSION_FILE);
}
return true;
} catch (error) {
console.error('Failed to delete session:', (error as Error).message);
return false;
}
}
Step 6: Create UI Module
// src/ui.ts
import * as readline from 'readline';
import { formatOperation } from './operations';
import { Operation, OperationSymbol, symbolToOperation } from './types';
import { operationToSymbol } from './types';
import { CalculationEntry } from './types';
export function createReadline(): readline.Interface {
return readline.createInterface({
input: process.stdin,
output: process.stdout,
});
}
export function prompt(rl: readline.Interface, question: string): Promise<string> {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer.trim());
});
});
}
/**
* Displays the calculator header
*/
export function showHeader(currentValue: number, memory: number | null): void {
console.log('');
console.log('========================================');
console.log(' CALCULATOR ');
console.log('========================================');
console.log('');
console.log(`Current value: ${currentValue}`);
if (memory !== null) {
console.log(`Memory: ${memory}`);
}
console.log('');
}
/**
* Displays the main menu
*/
export function showMenu(): void {
console.log('Commands:');
console.log(' [expression] Enter calculation (e.g., + 5, * 3, - 2, / 4)');
console.log(' [number] Set current value');
console.log(' history Show calculation history');
console.log(' undo Undo last operation');
console.log(' redo Redo last undone operation');
console.log(' ms Store in memory');
console.log(' mr Recall from memory');
console.log(' mc Clear memory');
console.log(' m+ Add to memory');
console.log(' m- Subtract from memory');
console.log(' clear Clear current value');
console.log(' clearall Clear everything');
console.log(' save Save session');
console.log(' load Load session');
console.log(' help Show this menu');
console.log(' exit Exit calculator');
console.log('');
}
/**
* Parses an expression like "+ 5" or "* 3"
*/
export function parseExpression(input: string): { operation: Operation; operand: number } | null {
const trimmed = input.trim();
// Check for operator at the start
const match = trimmed.match(/^([+\-*/])\s*(-?\d+\.?\d*)$/);
if (match) {
const symbol = match[1] as OperationSymbol;
const operand = parseFloat(match[2]);
if (!isNaN(operand) && symbol in symbolToOperation) {
return {
operation: symbolToOperation[symbol],
operand,
};
}
}
return null;
}
/**
* Checks if input is a number
*/
export function parseNumber(input: string): number | null {
const num = parseFloat(input);
return isNaN(num) ? null : num;
}
/**
* Displays calculation result
*/
export function displayResult(value: number): void {
console.log(`\nResult: ${value}`);
}
/**
* Displays an error message
*/
export function displayError(message: string): void {
console.log(`\nError: ${message}`);
}
/**
* Displays a success message
*/
export function displaySuccess(message: string): void {
console.log(`\n${message}`);
}
/**
* Displays history entries
*/
export function displayHistory(entries: string[]): void {
console.log('\n--- History ---');
if (entries.length === 0) {
console.log('No calculations yet.');
} else {
entries.forEach((entry) => console.log(entry));
}
console.log('');
}
/**
* Displays an undone/redone entry
*/
export function displayUndoRedo(entry: CalculationEntry, action: 'Undone' | 'Redone'): void {
const formatted = formatOperation(
entry.operation,
entry.previousValue,
entry.operand,
entry.resultValue
);
console.log(`\n${action}: ${formatted}`);
}
Step 7: Create Main Application
// src/index.ts
import { Calculator } from './calculator';
import { loadSession, saveSession, sessionExists } from './storage';
import {
createReadline,
displayError,
displayHistory,
displayResult,
displaySuccess,
displayUndoRedo,
parseExpression,
parseNumber,
prompt,
showHeader,
showMenu,
} from './ui';
async function main(): Promise<void> {
const rl = createReadline();
let calculator = new Calculator();
// Check for existing session
if (sessionExists()) {
console.log('\nA saved session was found.');
const loadIt = await prompt(rl, 'Load it? (y/n): ');
if (loadIt.toLowerCase() === 'y') {
const state = loadSession();
if (state) {
calculator = new Calculator(state);
console.log('Session loaded successfully.');
}
}
}
console.log('\nWelcome to the Calculator!');
console.log('Type "help" for available commands.\n');
let running = true;
while (running) {
showHeader(calculator.getValue(), calculator.memoryRecall());
const input = await prompt(rl, 'Enter command or expression: ');
if (!input) continue;
const command = input.toLowerCase();
switch (command) {
case 'help':
showMenu();
break;
case 'history':
displayHistory(calculator.getHistory());
break;
case 'undo':
if (calculator.canUndo()) {
const entry = calculator.undo();
if (entry) {
displayUndoRedo(entry, 'Undone');
displayResult(calculator.getValue());
}
} else {
displayError('Nothing to undo.');
}
break;
case 'redo':
if (calculator.canRedo()) {
const entry = calculator.redo();
if (entry) {
displayUndoRedo(entry, 'Redone');
displayResult(calculator.getValue());
}
} else {
displayError('Nothing to redo.');
}
break;
case 'ms':
calculator.memoryStore();
displaySuccess(`Stored ${calculator.getValue()} in memory.`);
break;
case 'mr':
const recalled = calculator.memoryRecall();
if (recalled !== null) {
calculator.setValue(recalled);
displaySuccess(`Recalled ${recalled} from memory.`);
} else {
displayError('Memory is empty.');
}
break;
case 'mc':
calculator.memoryClear();
displaySuccess('Memory cleared.');
break;
case 'm+':
calculator.memoryAdd();
displaySuccess(`Added to memory. Memory: ${calculator.memoryRecall()}`);
break;
case 'm-':
calculator.memorySubtract();
displaySuccess(`Subtracted from memory. Memory: ${calculator.memoryRecall()}`);
break;
case 'clear':
calculator.clearCurrent();
displaySuccess('Current value cleared.');
break;
case 'clearall':
const confirm = await prompt(rl, 'Clear everything? (y/n): ');
if (confirm.toLowerCase() === 'y') {
calculator.clearAll();
displaySuccess('All cleared.');
}
break;
case 'save':
const saved = saveSession(calculator.getState());
if (saved) {
displaySuccess('Session saved successfully.');
} else {
displayError('Failed to save session.');
}
break;
case 'load':
const state = loadSession();
if (state) {
calculator = new Calculator(state);
displaySuccess('Session loaded successfully.');
} else {
displayError('No session found or failed to load.');
}
break;
case 'exit':
case 'quit':
const saveOnExit = await prompt(rl, 'Save session before exiting? (y/n): ');
if (saveOnExit.toLowerCase() === 'y') {
saveSession(calculator.getState());
console.log('Session saved.');
}
running = false;
console.log('\nGoodbye!');
break;
default:
// Try to parse as expression
const expression = parseExpression(input);
if (expression) {
const result = calculator.calculate(expression.operation, expression.operand);
if (result.success) {
displayResult(result.value);
} else {
displayError(result.error || 'Calculation failed');
}
break;
}
// Try to parse as number
const number = parseNumber(input);
if (number !== null) {
calculator.setValue(number);
displaySuccess(`Value set to ${number}`);
break;
}
displayError('Unknown command. Type "help" for available commands.');
}
}
rl.close();
}
main().catch(console.error);
Configuration Files
// package.json
{
"name": "calculator",
"version": "1.0.0",
"description": "Calculator with history and undo/redo",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"ts-node": "^10.9.0"
}
}
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Sample Output
Welcome to the Calculator!
Type "help" for available commands.
========================================
CALCULATOR
========================================
Current value: 0
Enter command or expression: 10
Value set to 10
========================================
CALCULATOR
========================================
Current value: 10
Enter command or expression: + 5
Result: 15
========================================
CALCULATOR
========================================
Current value: 15
Enter command or expression: * 3
Result: 45
========================================
CALCULATOR
========================================
Current value: 45
Enter command or expression: history
--- History ---
1. 10 + 5 = 15
2. 15 * 3 = 45
========================================
CALCULATOR
========================================
Current value: 45
Enter command or expression: undo
Undone: 15 * 3 = 45
Result: 15
========================================
CALCULATOR
========================================
Current value: 15
Enter command or expression: ms
Stored 15 in memory.
========================================
CALCULATOR
========================================
Current value: 15
Memory: 15
Enter command or expression: save
Session saved successfully.
Extensions
1. Scientific Functions
Add more mathematical operations:
type ScientificOperation = 'sqrt' | 'power' | 'log' | 'sin' | 'cos' | 'tan';
function performScientific(operation: ScientificOperation, value: number, arg?: number): number {
switch (operation) {
case 'sqrt':
return Math.sqrt(value);
case 'power':
return Math.pow(value, arg ?? 2);
case 'log':
return Math.log(value);
case 'sin':
return Math.sin(value);
case 'cos':
return Math.cos(value);
case 'tan':
return Math.tan(value);
}
}
2. Expression Parser
Parse complex expressions:
// Support expressions like "2 + 3 * 4"
function parseComplexExpression(expr: string): number {
// Implement operator precedence
// Use the Shunting Yard algorithm
}
3. Calculation Chains
Support chained operations:
// Fluent API
calculator.add(5).multiply(3).subtract(2).getValue(); // Returns result
4. Export History
Export history to different formats:
function exportToCSV(history: CalculationEntry[]): string {
const header = 'ID,Operation,Operand,Previous,Result,Timestamp';
const rows = history.map(
(e) => `${e.id},${e.operation},${e.operand},${e.previousValue},${e.resultValue},${e.timestamp}`
);
return [header, ...rows].join('\n');
}
5. Multiple Memory Slots
Support multiple memory registers:
interface MemorySlots {
slots: Map<string, number>;
store(name: string, value: number): void;
recall(name: string): number | null;
list(): Array<{ name: string; value: number }>;
}
Key Takeaways
- Generic Stack class: The
Stack<T>class works with any type - Command pattern for undo: Store operations to enable undo/redo
- State management: Centralized state makes persistence easy
- Utility types with Record:
Record<Operation, string>ensures type-safe mappings - Error handling: Custom error classes provide clear error messages
- Separation of concerns: Calculator, history, storage, and UI are independent
Resources
| Resource | Type | Description |
|---|---|---|
| TypeScript Generics | Documentation | Generic types guide |
| Command Pattern | Tutorial | Command pattern explanation |
| Node.js fs module | Documentation | File system operations |
Course Complete
Congratulations on completing Course 2: TypeScript from Scratch!
You have learned:
- JavaScript fundamentals
- TypeScript type system
- Interfaces and type aliases
- Functions and classes
- Generics and utility types
- Module organization
- Building complete applications
You are now ready to move on to Course 3, where you will learn about asynchronous programming and working with external APIs.