You are viewing a preview of this lesson. Sign in to start learning
Back to TypeScript

TypeScript Foundations

Master the basics of TypeScript, including setup, compilation, and understanding how TypeScript enhances JavaScript with static typing and type safety.

TypeScript Foundations

Master TypeScript's core concepts with free flashcards and spaced repetition to reinforce your learning. This lesson covers type annotations, interfaces, functions, generics, and type inference—essential foundations for building type-safe JavaScript applications. Whether you're new to TypeScript or transitioning from JavaScript, understanding these fundamentals will transform how you write code.

Welcome to TypeScript 💻

TypeScript is a strongly typed superset of JavaScript that compiles to plain JavaScript. Think of it as JavaScript with superpowers—it catches errors during development instead of runtime, provides intelligent code completion, and makes refactoring safer. Created by Microsoft in 2012, TypeScript has become the standard for large-scale JavaScript applications, powering frameworks like Angular, and being embraced by React and Vue communities.

💡 Why TypeScript matters: Studies show that TypeScript can prevent up to 15% of common JavaScript bugs before they reach production. Companies like Airbnb, Slack, and Google rely on TypeScript for their critical codebases.

Core Concepts in Detail

1. Type Annotations 🏷️

Type annotations explicitly declare what type a variable, parameter, or return value should be. While JavaScript is dynamically typed (types are determined at runtime), TypeScript is statically typed (types are checked at compile time).

Basic type annotations:

let username: string = "Alice";
let age: number = 30;
let isActive: boolean = true;
let items: number[] = [1, 2, 3];
let tuple: [string, number] = ["hello", 42];

🌍 Real-world analogy: Type annotations are like labeled containers in a warehouse. Just as you wouldn't store liquids in a box labeled "electronics," TypeScript won't let you store a number in a variable labeled as a string.

Primitive types:

TypeDescriptionExample
stringText data"hello", 'world'
numberAll numbers (int and float)42, 3.14
booleanTrue or falsetrue, false
nullIntentional absencenull
undefinedNot yet assignedundefined
anyOpt out of type checkingAny value (avoid when possible!)
voidNo return valueFunctions that don't return

💡 Tip: The any type defeats the purpose of TypeScript. Use it only when absolutely necessary, such as when migrating JavaScript code gradually.

2. Interfaces 📋

An interface defines the shape of an object—what properties it has and what types those properties are. Interfaces are TypeScript's way of enforcing contracts in your code.

interface User {
  id: number;
  name: string;
  email: string;
  age?: number;  // Optional property (note the ?)
  readonly role: string;  // Cannot be modified after creation
}

const user: User = {
  id: 1,
  name: "Bob",
  email: "bob@example.com",
  role: "admin"
  // age is optional, so we can omit it
};

Key interface features:

  • Optional properties: Use ? to mark properties that may not exist
  • Readonly properties: Use readonly to prevent modification after creation
  • Index signatures: Allow dynamic property names
interface StringDictionary {
  [key: string]: string;  // Any string key maps to string value
}

const translations: StringDictionary = {
  hello: "hola",
  goodbye: "adiós"
};

🤔 Did you know? Unlike classes, interfaces have zero runtime cost. They exist only at compile time and are completely removed from the JavaScript output.

3. Functions ⚙️

TypeScript allows you to specify types for function parameters and return values, making function contracts explicit and enforceable.

Basic function typing:

function add(a: number, b: number): number {
  return a + b;
}

const multiply = (x: number, y: number): number => {
  return x * y;
};

// Function with no return value
function logMessage(message: string): void {
  console.log(message);
}

Optional and default parameters:

function greet(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

greet("Alice");  // "Hello, Alice!"
greet("Bob", "Hi");  // "Hi, Bob!"

function buildName(firstName: string, lastName?: string): string {
  return lastName ? `${firstName} ${lastName}` : firstName;
}

Rest parameters:

function sum(...numbers: number[]): number {
  return numbers.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3, 4);  // 10

💡 Pro tip: Always specify return types explicitly, even when TypeScript can infer them. This catches errors where you accidentally return the wrong type.

4. Type Inference 🧠

TypeScript is smart enough to automatically determine types in many situations. This is called type inference, and it keeps your code clean while maintaining type safety.

// TypeScript infers these types automatically
let message = "Hello";  // inferred as string
let count = 42;  // inferred as number
let active = true;  // inferred as boolean

const numbers = [1, 2, 3];  // inferred as number[]
const mixed = [1, "two", true];  // inferred as (string | number | boolean)[]

Function return type inference:

// Return type is inferred as number
function square(x: number) {
  return x * x;
}

// Return type is inferred as string
const formatName = (first: string, last: string) => {
  return `${first} ${last}`;
};

🌍 Real-world analogy: Type inference is like autocorrect on your phone. You don't have to spell everything perfectly—the system figures out what you mean from context.

5. Union and Literal Types 🔀

Union types allow a value to be one of several types, connected with the pipe (|) operator:

let id: string | number;
id = "abc123";  // Valid
id = 42;  // Also valid
id = true;  // Error: boolean is not assignable

function formatValue(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase();
  } else {
    return value.toFixed(2);
  }
}

Literal types restrict values to specific constants:

let direction: "north" | "south" | "east" | "west";
direction = "north";  // Valid
direction = "up";  // Error: not one of the allowed values

type Status = "pending" | "approved" | "rejected";

function setStatus(status: Status): void {
  console.log(`Status: ${status}`);
}

6. Generics 🎯

Generics allow you to write reusable code that works with multiple types while maintaining type safety. They're like templates or placeholders for types.

// Generic function
function identity<T>(arg: T): T {
  return arg;
}

const num = identity<number>(42);  // T is number
const str = identity<string>("hello");  // T is string
const auto = identity(true);  // T is inferred as boolean

// Generic interface
interface Box<T> {
  content: T;
}

const numberBox: Box<number> = { content: 123 };
const stringBox: Box<string> = { content: "gift" };

// Generic array operations
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

const first = firstElement([1, 2, 3]);  // number | undefined
const firstLetter = firstElement(["a", "b"]);  // string | undefined

💡 Mnemonic: Think of generics as "type variables"—just like x can represent any number, T can represent any type.

7. Type Aliases 📝

Type aliases create custom names for types, making complex types easier to read and reuse:

type Point = {
  x: number;
  y: number;
};

type ID = string | number;

type UserRole = "admin" | "user" | "guest";

type Callback = (data: string) => void;

function processData(callback: Callback): void {
  callback("processed");
}

Type aliases vs interfaces:

FeatureType AliasInterface
Define object shape✅ Yes✅ Yes
Can be extended⚠️ Via intersection (&)✅ Via extends
Union types✅ Yes❌ No
Primitive types✅ Yes❌ No
Declaration merging❌ No✅ Yes

Examples with Detailed Explanations

Example 1: Building a Type-Safe Shopping Cart 🛒

interface Product {
  id: number;
  name: string;
  price: number;
  category: "electronics" | "clothing" | "food";
}

interface CartItem {
  product: Product;
  quantity: number;
}

class ShoppingCart {
  private items: CartItem[] = [];

  addItem(product: Product, quantity: number): void {
    const existingItem = this.items.find(item => item.product.id === product.id);
    
    if (existingItem) {
      existingItem.quantity += quantity;
    } else {
      this.items.push({ product, quantity });
    }
  }

  getTotal(): number {
    return this.items.reduce(
      (total, item) => total + (item.product.price * item.quantity),
      0
    );
  }

  getItemsByCategory(category: Product["category"]): CartItem[] {
    return this.items.filter(item => item.product.category === category);
  }
}

const laptop: Product = {
  id: 1,
  name: "Laptop",
  price: 999.99,
  category: "electronics"
};

const cart = new ShoppingCart();
cart.addItem(laptop, 1);
console.log(cart.getTotal());  // 999.99

Why this works: Interfaces define clear contracts for Product and CartItem. The literal type for category prevents typos. TypeScript ensures you can't add invalid products or mix up quantities and prices.

Example 2: Generic API Response Handler 🌐

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

interface User {
  id: number;
  username: string;
  email: string;
}

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  const json = await response.json();
  
  return {
    data: json,
    status: response.status,
    message: response.ok ? "Success" : "Error"
  };
}

// Usage
async function getUser(id: number): Promise<User | null> {
  const response = await fetchData<User>(`/api/users/${id}`);
  
  if (response.status === 200) {
    return response.data;  // TypeScript knows this is User
  }
  
  return null;
}

Why this works: The generic <T> allows fetchData to work with any data type while maintaining type safety. When you call fetchData<User>, TypeScript knows response.data is a User object, giving you autocomplete and type checking.

Example 3: Type Guards and Narrowing 🔍

type Circle = {
  kind: "circle";
  radius: number;
};

type Rectangle = {
  kind: "rectangle";
  width: number;
  height: number;
};

type Shape = Circle | Rectangle;

function getArea(shape: Shape): number {
  // Type guard using discriminant property
  if (shape.kind === "circle") {
    // TypeScript knows shape is Circle here
    return Math.PI * shape.radius ** 2;
  } else {
    // TypeScript knows shape is Rectangle here
    return shape.width * shape.height;
  }
}

// Custom type guard
function isCircle(shape: Shape): shape is Circle {
  return shape.kind === "circle";
}

const myShape: Shape = { kind: "circle", radius: 5 };

if (isCircle(myShape)) {
  console.log(myShape.radius);  // TypeScript knows radius exists
}

Why this works: The kind property acts as a discriminant, allowing TypeScript to narrow the union type. Inside each if block, TypeScript knows exactly which type you're working with, preventing errors like accessing radius on a Rectangle.

Example 4: Utility Types in Action 🛠️

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  age: number;
}

// Partial: Makes all properties optional
type UserUpdate = Partial<User>;

function updateUser(id: number, updates: UserUpdate): void {
  // Can update only some properties
}

updateUser(1, { email: "new@email.com" });  // Valid

// Pick: Select specific properties
type UserPublic = Pick<User, "id" | "name" | "email">;

const publicUser: UserPublic = {
  id: 1,
  name: "Alice",
  email: "alice@example.com"
  // password and age are not included
};

// Omit: Exclude specific properties
type UserWithoutPassword = Omit<User, "password">;

// Readonly: Makes all properties immutable
type ImmutableUser = Readonly<User>;

const user: ImmutableUser = {
  id: 1,
  name: "Bob",
  email: "bob@example.com",
  password: "secret",
  age: 30
};

// user.name = "Alice";  // Error: Cannot assign to 'name' because it is read-only

// Record: Create object type with specific keys
type UserRoles = Record<"admin" | "user" | "guest", string[]>;

const permissions: UserRoles = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"]
};

Why this works: TypeScript's built-in utility types transform existing types without rewriting code. Partial is perfect for update operations, Pick and Omit control what's exposed in APIs, and Readonly prevents accidental mutations.

Common Mistakes ⚠️

1. Overusing any

// ❌ Bad: Defeats TypeScript's purpose
function processData(data: any): any {
  return data.value;
}

// ✅ Good: Use specific types or generics
function processData<T extends { value: unknown }>(data: T): unknown {
  return data.value;
}

2. Forgetting optional chaining

interface User {
  address?: {
    street: string;
  };
}

// ❌ Bad: Runtime error if address is undefined
const street = user.address.street;

// ✅ Good: Safe navigation
const street = user.address?.street;

3. Not handling null/undefined

// ❌ Bad: Assumes array has elements
function getFirst(arr: number[]): number {
  return arr[0];  // Could be undefined!
}

// ✅ Good: Account for empty arrays
function getFirst(arr: number[]): number | undefined {
  return arr[0];
}

4. Incorrect type assertions

// ❌ Bad: Lying to TypeScript
const value = getSomeValue() as string;  // Might not be a string!

// ✅ Good: Validate before asserting
const value = getSomeValue();
if (typeof value === "string") {
  // Now safe to use as string
}

5. Ignoring discriminated unions

type Result = { success: true; data: string } | { success: false; error: string };

// ❌ Bad: Accessing properties without checking
function handleResult(result: Result): void {
  console.log(result.data);  // Error: Property 'data' might not exist
}

// ✅ Good: Use discriminant property
function handleResult(result: Result): void {
  if (result.success) {
    console.log(result.data);  // TypeScript knows data exists
  } else {
    console.log(result.error);  // TypeScript knows error exists
  }
}

Key Takeaways 🎯

Type annotations make your code self-documenting and catch errors early

Interfaces define clear contracts for object shapes and improve code organization

Type inference reduces boilerplate while maintaining safety—let TypeScript do the work

Union types and literal types provide flexibility with guardrails

Generics enable reusable, type-safe code across different data types

Avoid any whenever possible—it's an escape hatch, not a solution

Type guards and discriminated unions help TypeScript narrow types safely

Utility types transform existing types without duplication

🧠 Memory device: Remember "I-F-G-U-T" for TypeScript foundations:

  • Interfaces define structure
  • Functions need parameter/return types
  • Generics make code reusable
  • Union types allow flexibility
  • Type inference keeps code clean

📚 Further Study

  1. TypeScript Official Handbook: https://www.typescriptlang.org/docs/handbook/intro.html - Comprehensive guide from the creators
  2. TypeScript Deep Dive: https://basarat.gitbook.io/typescript/ - Free book covering advanced patterns
  3. Execute Program TypeScript Course: https://www.executeprogram.com/courses/typescript - Interactive exercises with immediate feedback

📋 Quick Reference Card

Basic Typesstring, number, boolean, any, void
Arraysnumber[] or Array<number>
Tuple[string, number]
Unionstring | number
Literal"yes" | "no"
Optionalname?: string
Readonlyreadonly id: number
Function(x: number) => number
GenericArray<T>, Promise<T>
Type Aliastype Name = string;
Interfaceinterface User { id: number; }
Type Guardtypeof x === "string"
Assertionvalue as string
Utility TypesPartial<T>, Pick<T, K>, Omit<T, K>

Practice Questions

Test your understanding with these questions:

Q1: Complete the type alias for a callback function: ```typescript type Callback = {{1}}; ``` where the callback takes a string parameter and returns void.
A: (data: string) => void
Q2: Write a TypeScript function signature that takes two numbers and returns their sum. Include parameter types and return type annotation.
A: !AI