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