Back to blog

Phase 1: TypeScript Fundamentals - Mastering the Type System

typescriptfundamentalstypesprogrammingjavascript
Phase 1: TypeScript Fundamentals - Mastering the Type System

Welcome to Phase 1

Welcome to the first phase of your TypeScript journey! If you're coming from JavaScript, you're about to add a powerful tool to your development workflow: static type checking. The goal of this phase is to get you comfortable with TypeScript's type system and understand how types can prevent bugs and improve your code quality.

This is not a "what is TypeScript?" tutorial. Instead, we'll focus on practical type system concepts that you'll use daily when building real applications.

Time commitment: 2 weeks, 1-2 hours daily Prerequisite: JavaScript experience (ES6+ features)

What You'll Learn

By the end of Phase 1, you'll be able to:

✅ Set up TypeScript projects from scratch
✅ Understand type inference and annotations
✅ Use interfaces and type aliases effectively
✅ Write generic functions and types
✅ Apply union and intersection types
✅ Use utility types (Partial, Pick, Omit, Record)
✅ Implement type guards and narrowing
✅ Configure TypeScript compiler options
✅ Debug type errors efficiently

Setting Up Your Environment

1. Install Node.js and TypeScript

Install Node.js 22+ LTS:

Install TypeScript globally (optional but useful):

npm install -g typescript

Verify installation:

tsc --version

Should display TypeScript 5.3.0 or higher.

2. Choose Your Editor

VS Code (Recommended):

  • Download: VS Code
  • Best TypeScript support built-in
  • Excellent IntelliSense and type checking

Recommended VS Code Extensions:

  • Error Lens - Inline error messages
  • Pretty TypeScript Errors - Better error formatting
  • TypeScript Importer - Auto-import suggestions

Alternative options:

  • WebStorm - Powerful IDE with excellent TypeScript support
  • Cursor - AI-powered editor with TypeScript intelligence

3. Create Your First TypeScript Project

Initialize a new project:

mkdir typescript-fundamentals
cd typescript-fundamentals
npm init -y
npm install --save-dev typescript @types/node

Create tsconfig.json:

npx tsc --init

Update tsconfig.json with strict settings:

{
  "compilerOptions": {
    "target": "ES2023",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Create your first TypeScript file (src/index.ts):

// src/index.ts
function greet(name: string): string {
  return `Hello, ${name}!`;
}
 
console.log(greet("TypeScript"));

Compile and run:

npx tsc
node dist/index.js

Better: Use tsx for development (no compilation needed):

npm install --save-dev tsx
npx tsx src/index.ts

Note: In production projects, you'll typically use build tools like Vite or webpack to handle TypeScript compilation along with bundling, code splitting, and optimization. Learn more in JavaScript Build Tools & Bundlers Explained.

Understanding TypeScript's Type System

TypeScript is gradually typed: you can add types incrementally. Types exist only at compile time and are erased during compilation to JavaScript.

Type Inference

TypeScript can often infer types automatically:

// Type inference - no annotation needed
let message = "Hello"; // Type: string
let count = 42;        // Type: number
let isActive = true;   // Type: boolean
 
// TypeScript infers return type
function add(a: number, b: number) {
  return a + b; // Return type inferred as number
}
 
// Arrays
let numbers = [1, 2, 3];        // Type: number[]
let mixed = [1, "two", true];   // Type: (string | number | boolean)[]

Best practice: Let TypeScript infer types when they're obvious. Add annotations when they improve clarity.

Basic Type Annotations

// Primitives
let username: string = "Alice";
let age: number = 30;
let isStudent: boolean = false;
let empty: null = null;
let notDefined: undefined = undefined;
 
// Arrays
let numbers: number[] = [1, 2, 3];
let names: Array<string> = ["Alice", "Bob"]; // Generic syntax
 
// Tuples - fixed-length arrays with specific types
let user: [string, number] = ["Alice", 30];
let rgb: [number, number, number] = [255, 0, 128];
 
// Enums
enum Status {
  Pending = "PENDING",
  Approved = "APPROVED",
  Rejected = "REJECTED"
}
 
let orderStatus: Status = Status.Pending;

Object Types

// Inline object type
let user: { name: string; age: number } = {
  name: "Alice",
  age: 30
};
 
// Optional properties
let person: { name: string; age?: number } = {
  name: "Bob"
  // age is optional
};
 
// Readonly properties
let config: { readonly apiKey: string } = {
  apiKey: "secret-key"
};
// config.apiKey = "new-key"; // Error: Cannot assign to 'apiKey'

Interfaces vs Type Aliases

Both can define object shapes, but they have subtle differences.

Interfaces

interface User {
  id: number;
  name: string;
  email: string;
  age?: number; // Optional
  readonly createdAt: Date; // Readonly
}
 
// Interface can be extended
interface Admin extends User {
  role: string;
  permissions: string[];
}
 
const admin: Admin = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
  createdAt: new Date(),
  role: "super-admin",
  permissions: ["read", "write", "delete"]
};
 
// Interfaces can be merged (declaration merging)
interface User {
  lastLogin?: Date;
}
// User now has lastLogin property

Type Aliases

type User = {
  id: number;
  name: string;
  email: string;
};
 
// Can represent primitives, unions, tuples
type ID = string | number;
type Point = [number, number];
type Status = "pending" | "approved" | "rejected";
 
// Type intersection
type Timestamped = {
  createdAt: Date;
  updatedAt: Date;
};
 
type TimestampedUser = User & Timestamped;

When to use which?

  • Use interfaces for object shapes, especially when you might extend them
  • Use type aliases for unions, tuples, primitives, or complex type transformations
  • Both work for most cases—choose one style and be consistent

Functions and Function Types

Function Signatures

// Named function with types
function add(a: number, b: number): number {
  return a + b;
}
 
// Arrow function
const multiply = (a: number, b: number): number => a * b;
 
// Optional parameters
function greet(name: string, greeting?: string): string {
  return `${greeting || "Hello"}, ${name}!`;
}
 
// Default parameters
function createUser(name: string, role: string = "user"): User {
  return { id: Date.now(), name, email: `${name}@example.com` };
}
 
// Rest parameters
function sum(...numbers: number[]): number {
  return numbers.reduce((total, n) => total + n, 0);
}

Function Type Expressions

// Function type
type MathOperation = (a: number, b: number) => number;
 
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
 
// Callback types
function processData(
  data: string[],
  callback: (item: string) => void
): void {
  data.forEach(callback);
}
 
processData(["a", "b", "c"], (item) => console.log(item));

Void and Never

// void - function returns nothing
function log(message: string): void {
  console.log(message);
  // No return statement
}
 
// never - function never returns (throws or infinite loop)
function throwError(message: string): never {
  throw new Error(message);
}
 
function infiniteLoop(): never {
  while (true) {
    // ...
  }
}

Generics

Generics allow you to write reusable, type-safe code that works with multiple types.

Generic Functions

// Generic identity function
function identity<T>(value: T): T {
  return value;
}
 
// Usage - TypeScript infers the type
let num = identity(42);        // T is number
let str = identity("hello");   // T is string
 
// Explicit type argument
let bool = identity<boolean>(true);
 
// Generic with constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
 
const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name");  // Type: string
const age = getProperty(user, "age");    // Type: number
// const invalid = getProperty(user, "email"); // Error: "email" not in User

Generic Interfaces

// Generic interface
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}
 
// Usage
const userResponse: ApiResponse<User> = {
  data: { id: 1, name: "Alice", email: "alice@example.com" },
  status: 200,
  message: "Success"
};
 
const usersResponse: ApiResponse<User[]> = {
  data: [
    { id: 1, name: "Alice", email: "alice@example.com" },
    { id: 2, name: "Bob", email: "bob@example.com" }
  ],
  status: 200,
  message: "Success"
};
 
// Generic with default type
interface Container<T = string> {
  value: T;
}
 
const stringContainer: Container = { value: "hello" }; // T defaults to string
const numberContainer: Container<number> = { value: 42 };

Generic Classes

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;
  }
}
 
// Usage
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2
 
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");

Union and Intersection Types

Union Types (OR)

// Union type - value can be one of several types
type ID = string | number;
 
function printId(id: ID): void {
  console.log(`ID: ${id}`);
}
 
printId(123);       // OK
printId("abc-123"); // OK
 
// Literal union types
type Status = "pending" | "approved" | "rejected";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
 
function setStatus(status: Status): void {
  // status can only be one of the three values
}
 
setStatus("pending");  // OK
// setStatus("unknown"); // Error
 
// Union with different types
type StringOrNumber = string | number;
type Result = { success: true; data: string } | { success: false; error: string };

Intersection Types (AND)

// Intersection type - combines multiple types
type Person = {
  name: string;
  age: number;
};
 
type Employee = {
  employeeId: string;
  department: string;
};
 
type EmployeePerson = Person & Employee;
 
const employee: EmployeePerson = {
  name: "Alice",
  age: 30,
  employeeId: "EMP001",
  department: "Engineering"
};
 
// Practical example
type Timestamped = {
  createdAt: Date;
  updatedAt: Date;
};
 
type AuditLog = {
  userId: string;
  action: string;
};
 
type TimestampedLog = AuditLog & Timestamped;

Type Guards and Narrowing

TypeScript can narrow types based on checks you perform.

typeof Type Guards

function padLeft(value: string, padding: string | number): string {
  if (typeof padding === "number") {
    // TypeScript knows padding is number here
    return " ".repeat(padding) + value;
  }
  // TypeScript knows padding is string here
  return padding + value;
}

Truthiness Narrowing

function printName(name: string | null | undefined): void {
  if (name) {
    // TypeScript knows name is string here
    console.log(name.toUpperCase());
  } else {
    console.log("No name provided");
  }
}

instanceof Type Guards

function processDate(date: Date | string): string {
  if (date instanceof Date) {
    // TypeScript knows date is Date here
    return date.toISOString();
  }
  // TypeScript knows date is string here
  return new Date(date).toISOString();
}

in Operator Narrowing

type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird): void {
  if ("swim" in animal) {
    // TypeScript knows animal is Fish here
    animal.swim();
  } else {
    // TypeScript knows animal is Bird here
    animal.fly();
  }
}

Custom Type Guards

// Type predicate
function isString(value: unknown): value is string {
  return typeof value === "string";
}
 
function processValue(value: unknown): void {
  if (isString(value)) {
    // TypeScript knows value is string here
    console.log(value.toUpperCase());
  }
}
 
// Discriminated unions
type Success = { status: "success"; data: string };
type Error = { status: "error"; message: string };
type Result = Success | Error;
 
function handleResult(result: Result): void {
  if (result.status === "success") {
    // TypeScript knows result is Success here
    console.log(result.data);
  } else {
    // TypeScript knows result is Error here
    console.log(result.message);
  }
}

Utility Types

TypeScript provides built-in utility types for common type transformations.

Partial

interface User {
  id: number;
  name: string;
  email: string;
}
 
// Partial makes all properties optional
type PartialUser = Partial<User>;
// Equivalent to:
// {
//   id?: number;
//   name?: string;
//   email?: string;
// }
 
function updateUser(id: number, updates: Partial<User>): void {
  // Can update any subset of User properties
}
 
updateUser(1, { name: "Alice" }); // OK
updateUser(1, { email: "alice@example.com" }); // OK

Required

interface Config {
  host?: string;
  port?: number;
  debug?: boolean;
}
 
// Required makes all properties required
type RequiredConfig = Required<Config>;
// All properties are now required

Readonly

// Readonly makes all properties readonly
type ReadonlyUser = Readonly<User>;
// Equivalent to:
// {
//   readonly id: number;
//   readonly name: string;
//   readonly email: string;
// }
 
const user: ReadonlyUser = { id: 1, name: "Alice", email: "alice@example.com" };
// user.name = "Bob"; // Error: Cannot assign to 'name'

Pick

// Pick selects specific properties
type UserPreview = Pick<User, "id" | "name">;
// Equivalent to:
// {
//   id: number;
//   name: string;
// }

Omit

// Omit excludes specific properties
type UserWithoutEmail = Omit<User, "email">;
// Equivalent to:
// {
//   id: number;
//   name: string;
// }

Record

// Record creates object type with specific keys and values
type UserRoles = Record<string, string[]>;
// Equivalent to: { [key: string]: string[] }
 
const roles: UserRoles = {
  admin: ["read", "write", "delete"],
  user: ["read"]
};
 
// Record with specific keys
type PageInfo = Record<"home" | "about" | "contact", { title: string; description: string }>;

ReturnType

function createUser(name: string, email: string) {
  return { id: Date.now(), name, email, createdAt: new Date() };
}
 
// Extract return type from function
type User = ReturnType<typeof createUser>;
// Type: { id: number; name: string; email: string; createdAt: Date }

Working with Modules

Exporting Types

// types.ts
export interface User {
  id: number;
  name: string;
  email: string;
}
 
export type UserRole = "admin" | "user" | "guest";
 
export interface ApiResponse<T> {
  data: T;
  status: number;
}

Importing Types

// user-service.ts
import { User, UserRole, ApiResponse } from "./types";
 
function getUser(id: number): ApiResponse<User> {
  // Implementation
}
 
// Type-only imports (removed at runtime)
import type { User } from "./types";

Common Pitfalls and Best Practices

❌ Don't Use any

// ❌ Bad - defeats the purpose of TypeScript
function processData(data: any) {
  return data.value; // No type safety
}
 
// ✅ Good - use proper types or generic
function processData<T extends { value: string }>(data: T) {
  return data.value;
}
 
// ✅ Or use unknown for truly unknown types
function processData(data: unknown) {
  if (typeof data === "object" && data !== null && "value" in data) {
    return (data as { value: string }).value;
  }
}

✅ Enable Strict Mode

Always use "strict": true in tsconfig.json. It enables:

  • strictNullChecks - Prevents null/undefined errors
  • strictFunctionTypes - Stricter function parameter checking
  • strictBindCallApply - Type-safe bind/call/apply
  • noImplicitAny - Requires explicit types
  • And more...

✅ Let TypeScript Infer Types

// ❌ Over-annotated
const numbers: number[] = [1, 2, 3];
const name: string = "Alice";
 
// ✅ Let TypeScript infer
const numbers = [1, 2, 3]; // Type: number[]
const name = "Alice";       // Type: string

✅ Use Const Assertions

// Without const assertion
const colors = ["red", "green", "blue"]; // Type: string[]
 
// With const assertion
const colors = ["red", "green", "blue"] as const; // Type: readonly ["red", "green", "blue"]
 
// Useful for literal types
const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000
} as const;
// Type: { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000 }

TypeScript Configuration

Essential tsconfig.json Options

{
  "compilerOptions": {
    // Language and Environment
    "target": "ES2023",                  // Output JavaScript version
    "lib": ["ES2023"],                   // Available standard library features
 
    // Modules
    "module": "NodeNext",                // Module system
    "moduleResolution": "NodeNext",      // How modules are resolved
    "resolveJsonModule": true,           // Import JSON files
 
    // Emit
    "outDir": "./dist",                  // Output directory
    "declaration": true,                 // Generate .d.ts files
    "sourceMap": true,                   // Generate source maps for debugging
 
    // Type Checking (all enabled by "strict": true)
    "strict": true,                      // Enable all strict checks
    "noUncheckedIndexedAccess": true,    // Add undefined to index signatures
    "noImplicitOverride": true,          // Require override keyword
 
    // Interop Constraints
    "esModuleInterop": true,             // Better CommonJS/ESM interop
    "forceConsistentCasingInFileNames": true,  // Case-sensitive imports
 
    // Skip Lib Check
    "skipLibCheck": true                 // Skip type checking of .d.ts files
  }
}

Summary and Key Takeaways

Congratulations! You've completed Phase 1 of the TypeScript roadmap. Here's what you've mastered:

Type System Basics: Primitives, objects, arrays, tuples, enums
Type Inference: Letting TypeScript infer types automatically
Interfaces & Types: Defining object shapes and complex types
Functions: Type-safe function signatures and callbacks
Generics: Writing reusable, type-safe code
Union & Intersection: Combining types in powerful ways
Type Guards: Narrowing types safely
Utility Types: Transforming types with built-in helpers
Module System: Organizing code with imports/exports
Configuration: Setting up TypeScript projects correctly

What's Next?

You now have a solid foundation in TypeScript's type system. In Phase 2, you'll apply these concepts to build type-safe React applications with React 19 and Next.js 15.

🎯 Continue to Phase 2: Frontend Development →

Additional Resources

For a deeper dive into advanced type system features:

📘 Deep Dive: Advanced TypeScript Types →


Previous: TypeScript Full-Stack Roadmap → Next: Phase 2: Frontend Development →

Happy coding! 🚀

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.