Skip to main content

Chapter 1: TypeScript Fundamentals - Complete Type-Safe JavaScript Guide

TypeScript is a powerful, statically-typed superset of JavaScript that brings type safety, better tooling, and enhanced developer experience to modern web development. This comprehensive guide will teach you everything you need to know about TypeScript fundamentals and how to leverage its powerful type system.

What is TypeScript?

TypeScript is a programming language developed by Microsoft that:

  • Extends JavaScript: All valid JavaScript code is valid TypeScript
  • Adds Static Typing: Catch errors at compile time, not runtime
  • Improves Developer Experience: Better IDE support, autocomplete, and refactoring
  • Enables Large-Scale Development: Better code organization and maintainability
  • Compiles to JavaScript: Runs anywhere JavaScript runs

Why Learn TypeScript?

TypeScript is becoming essential for modern web development because it:

  • Prevents Runtime Errors: Catch bugs before they reach production
  • Improves Code Quality: Better documentation and self-documenting code
  • Enhances IDE Support: Superior autocomplete, navigation, and refactoring
  • Supports Large Teams: Better collaboration and code maintainability
  • Industry Standard: Used by major companies and frameworks like Angular, React, and Vue

TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale. It's a superset of JavaScript that adds static type definitions, making your code more robust, maintainable, and easier to debug.

Key Characteristics of TypeScript:

  • Static Typing: Type checking at compile time
  • JavaScript Superset: All valid JavaScript is valid TypeScript
  • Compiled Language: Transpiles to JavaScript
  • Enhanced IDE Support: Better autocomplete, refactoring, and error detection
  • Modern JavaScript Features: Access to latest ECMAScript features

Why Use TypeScript?

1. Type Safety

TypeScript catches errors at compile time, preventing runtime errors that could crash your application.

// This will cause a compile-time error
function add(a: number, b: number): number {
return a + b;
}

// Error: Argument of type 'string' is not assignable to parameter of type 'number'
add("5", 10); // ❌ Compile error
add(5, 10); // ✅ Works correctly

Output:

error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

2. Better Developer Experience

Enhanced IDE support with intelligent autocomplete, refactoring tools, and inline documentation.

interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}

// IDE provides autocomplete for User properties
const user: User = {
id: 1,
name: "John Doe",
email: "[email protected]",
isActive: true
};

// IDE shows available properties when typing user.
console.log(user.name); // ✅ Autocomplete suggests: id, name, email, isActive

Output:

John Doe

3. Improved Code Maintainability

Type definitions serve as documentation and make code easier to understand and maintain.

// Clear function signature shows what parameters are expected
function processUserData(
user: User,
options: {
includeInactive: boolean;
sortBy: 'name' | 'email' | 'id';
}
): User[] {
// Implementation details...
return [user];
}

4. Refactoring Safety

TypeScript ensures that changes to your code don't break other parts of your application.

interface Product {
id: number;
title: string;
price: number;
}

// If you change 'title' to 'name' in the interface,
// TypeScript will show all places that need to be updated
const product: Product = {
id: 1,
title: "Laptop", // If changed to 'name', this will show an error
price: 999.99
};

How to Get Started with TypeScript?

1. Installation and Setup

Global Installation

npm install -g typescript

Project Installation

npm install --save-dev typescript
npm install --save-dev @types/node # For Node.js type definitions

Initialize TypeScript Configuration

npx tsc --init

This creates a tsconfig.json file with default configuration.

2. Basic Type Annotations

Primitive Types

// Basic primitive types
let name: string = "John";
let age: number = 25;
let isActive: boolean = true;
let data: null = null;
let value: undefined = undefined;

// Type inference (TypeScript can infer types)
let inferredString = "Hello"; // TypeScript infers this as string
let inferredNumber = 42; // TypeScript infers this as number

console.log(name, age, isActive);
console.log(inferredString, inferredNumber);

Output:

John 25 true
Hello 42

Array Types

// Array of numbers
let numbers: number[] = [1, 2, 3, 4, 5];

// Alternative syntax
let names: Array<string> = ["Alice", "Bob", "Charlie"];

// Mixed arrays (union types)
let mixed: (string | number)[] = ["hello", 42, "world", 100];

console.log(numbers);
console.log(names);
console.log(mixed);

Output:

[1, 2, 3, 4, 5]
['Alice', 'Bob', 'Charlie']
['hello', 42, 'world', 100]

Object Types

// Object type annotation
let person: {
name: string;
age: number;
isEmployed: boolean;
} = {
name: "Jane",
age: 30,
isEmployed: true
};

// Accessing properties
console.log(person.name);
console.log(person.age);
console.log(person.isEmployed);

Output:

Jane
30
true

3. Functions and Type Annotations

Function Parameters and Return Types

// Function with typed parameters and return type
function greet(name: string, age: number): string {
return `Hello, ${name}! You are ${age} years old.`;
}

// Arrow function with types
const calculateArea = (width: number, height: number): number => {
return width * height;
};

// Function with optional parameters
function createUser(name: string, email: string, age?: number): object {
return {
name,
email,
age: age || 0
};
}

// Function with default parameters
function multiply(a: number, b: number = 1): number {
return a * b;
}

console.log(greet("Alice", 25));
console.log(calculateArea(10, 5));
console.log(createUser("Bob", "[email protected]"));
console.log(createUser("Charlie", "[email protected]", 35));
console.log(multiply(5));
console.log(multiply(5, 3));

Output:

Hello, Alice! You are 25 years old.
50
{ name: 'Bob', email: '[email protected]', age: 0 }
{ name: 'Charlie', email: '[email protected]', age: 35 }
5
15

Function Overloads

// Function overloads
function format(value: string): string;
function format(value: number): string;
function format(value: boolean): string;
function format(value: string | number | boolean): string {
if (typeof value === 'string') {
return `String: ${value}`;
} else if (typeof value === 'number') {
return `Number: ${value}`;
} else {
return `Boolean: ${value}`;
}
}

console.log(format("Hello"));
console.log(format(42));
console.log(format(true));

Output:

String: Hello
Number: 42
Boolean: true

4. Union and Intersection Types

Union Types

// Union types allow a variable to be one of several types
let id: string | number;
id = "abc123";
console.log("String ID:", id);

id = 12345;
console.log("Number ID:", id);

// Function with union parameter
function printId(id: string | number): void {
if (typeof id === 'string') {
console.log(`String ID: ${id.toUpperCase()}`);
} else {
console.log(`Number ID: ${id}`);
}
}

printId("user123");
printId(456);

Output:

String ID: abc123
Number ID: 12345
String ID: USER123
Number ID: 456

Intersection Types

// Intersection types combine multiple types
interface Person {
name: string;
age: number;
}

interface Employee {
employeeId: number;
department: string;
}

// Intersection type
type PersonEmployee = Person & Employee;

const personEmployee: PersonEmployee = {
name: "John",
age: 30,
employeeId: 12345,
department: "Engineering"
};

console.log(personEmployee);

Output:

{
name: 'John',
age: 30,
employeeId: 12345,
department: 'Engineering'
}

5. Type Aliases and Interfaces

Type Aliases

// Type alias for primitive types
type UserId = string | number;
type Status = 'active' | 'inactive' | 'pending';

// Type alias for object types
type Point = {
x: number;
y: number;
};

// Using type aliases
let userId: UserId = "user123";
let userStatus: Status = 'active';
let point: Point = { x: 10, y: 20 };

console.log(userId, userStatus, point);

Output:

user123 active { x: 10, y: 20 }

Interfaces

// Interface definition
interface User {
readonly id: number; // Readonly property
name: string;
email: string;
age?: number; // Optional property
getFullInfo(): string; // Method
}

// Interface implementation
class UserImpl implements User {
readonly id: number;
name: string;
email: string;
age?: number;

constructor(id: number, name: string, email: string, age?: number) {
this.id = id;
this.name = name;
this.email = email;
this.age = age;
}

getFullInfo(): string {
return `${this.name} (${this.email}) - Age: ${this.age || 'Not specified'}`;
}
}

// Using the interface
const user = new UserImpl(1, "Alice", "[email protected]", 25);
console.log(user.getFullInfo());
console.log("User ID:", user.id);

Output:

Alice ([email protected]) - Age: 25
User ID: 1

6. Enums

// Numeric enum
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}

// String enum
enum Color {
Red = "red",
Green = "green",
Blue = "blue"
}

// Mixed enum
enum Status {
Pending = 0,
Approved = "approved",
Rejected = "rejected"
}

// Using enums
let currentDirection: Direction = Direction.Up;
let favoriteColor: Color = Color.Blue;
let requestStatus: Status = Status.Approved;

console.log("Direction:", currentDirection, Direction[currentDirection]);
console.log("Color:", favoriteColor);
console.log("Status:", requestStatus);

Output:

Direction: 0 Up
Color: blue
Status: approved

7. TypeScript Configuration

Basic tsconfig.json

{
"compilerOptions": {
"target": "ES2020", // JavaScript version to compile to
"module": "commonjs", // Module system
"lib": ["ES2020"], // Library files to include
"outDir": "./dist", // Output directory
"rootDir": "./src", // Root directory
"strict": true, // Enable strict type checking
"esModuleInterop": true, // Enable ES module interop
"skipLibCheck": true, // Skip type checking of declaration files
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"], // Files to include
"exclude": ["node_modules", "dist"] // Files to exclude
}

8. Compiling TypeScript

Command Line Compilation

# Compile a single file
tsc app.ts

# Compile with watch mode
tsc --watch

# Compile using tsconfig.json
tsc

# Compile and run
tsc && node dist/app.js

Example Compilation Process

// src/app.ts
function welcome(name: string): string {
return `Welcome, ${name}!`;
}

const message = welcome("TypeScript");
console.log(message);

Compilation:

tsc src/app.ts --outDir dist

Generated JavaScript (dist/app.js):

function welcome(name) {
return "Welcome, " + name + "!";
}
var message = welcome("TypeScript");
console.log(message);

Output:

Welcome, TypeScript!

Practical Examples

1. Simple Calculator

// Calculator with type safety
class Calculator {
add(a: number, b: number): number {
return a + b;
}

subtract(a: number, b: number): number {
return a - b;
}

multiply(a: number, b: number): number {
return a * b;
}

divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Division by zero is not allowed");
}
return a / b;
}
}

// Using the calculator
const calc = new Calculator();
console.log("Addition:", calc.add(10, 5));
console.log("Subtraction:", calc.subtract(10, 5));
console.log("Multiplication:", calc.multiply(10, 5));
console.log("Division:", calc.divide(10, 5));

Output:

Addition: 15
Subtraction: 5
Multiplication: 50
Division: 2

2. User Management System

// User management with TypeScript
interface User {
id: number;
username: string;
email: string;
isActive: boolean;
createdAt: Date;
}

class UserManager {
private users: User[] = [];
private nextId: number = 1;

addUser(username: string, email: string): User {
const user: User = {
id: this.nextId++,
username,
email,
isActive: true,
createdAt: new Date()
};

this.users.push(user);
return user;
}

getUserById(id: number): User | undefined {
return this.users.find(user => user.id === id);
}

getAllUsers(): User[] {
return [...this.users]; // Return a copy
}

deactivateUser(id: number): boolean {
const user = this.getUserById(id);
if (user) {
user.isActive = false;
return true;
}
return false;
}
}

// Using the user manager
const userManager = new UserManager();

const user1 = userManager.addUser("alice", "[email protected]");
const user2 = userManager.addUser("bob", "[email protected]");

console.log("All users:", userManager.getAllUsers());
console.log("User by ID:", userManager.getUserById(1));

userManager.deactivateUser(1);
console.log("After deactivation:", userManager.getUserById(1));

Output:

All users: [
{
id: 1,
username: 'alice',
email: '[email protected]',
isActive: true,
createdAt: 2024-01-15T10:30:00.000Z
},
{
id: 2,
username: 'bob',
email: '[email protected]',
isActive: true,
createdAt: 2024-01-15T10:30:00.000Z
}
]
User by ID: {
id: 1,
username: 'alice',
email: '[email protected]',
isActive: true,
createdAt: 2024-01-15T10:30:00.000Z
}
After deactivation: {
id: 1,
username: 'alice',
email: '[email protected]',
isActive: false,
createdAt: 2024-01-15T10:30:00.000Z
}

Common TypeScript Errors and Solutions

1. Type 'any' is not assignable to type 'string'

// ❌ Error: Type 'any' is not assignable to type 'string'
let name: string = someFunction(); // someFunction returns any

// ✅ Solution: Add type assertion or proper typing
let name: string = someFunction() as string;
// Or better: type the function properly

2. Property does not exist on type

// ❌ Error: Property 'length' does not exist on type 'number'
let value: number = 42;
console.log(value.length);

// ✅ Solution: Use correct type
let value: string = "42";
console.log(value.length); // Works correctly

3. Cannot find name

// ❌ Error: Cannot find name 'myVariable'
console.log(myVariable);

// ✅ Solution: Declare the variable
let myVariable: string = "Hello";
console.log(myVariable);

Best Practices for TypeScript Fundamentals

1. Use Explicit Types When Necessary

// Good: Explicit types for function parameters and return values
function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}

// Good: Type inference for simple cases
const name = "John"; // TypeScript infers string
const age = 25; // TypeScript infers number

2. Prefer Interfaces for Object Types

// Good: Use interfaces for object shapes
interface User {
id: number;
name: string;
email: string;
}

// Good: Use type aliases for unions and primitives
type Status = 'active' | 'inactive';
type UserId = string | number;

3. Enable Strict Mode

{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}

4. Use Readonly When Appropriate

interface Config {
readonly apiUrl: string;
readonly version: string;
timeout: number; // Can be modified
}

Conclusion

TypeScript fundamentals provide the foundation for building robust, maintainable applications. By understanding:

  • What TypeScript is and its key characteristics
  • Why it's beneficial for development
  • How to use basic type annotations, functions, and interfaces

You're now equipped to start writing type-safe JavaScript code. The type system helps catch errors early, improves developer experience, and makes code more maintainable.

Next Steps

  • Practice with basic type annotations
  • Experiment with interfaces and type aliases
  • Set up a TypeScript project with proper configuration
  • Move on to Chapter 2: Advanced Types

This tutorial is part of the TypeScript Mastery series by syscook.dev