Skip to content
intermediate Phase 6 · TypeScript Introduction

TypeScript Fundamentals

Master TypeScript basics — type annotations, interfaces, type aliases, unions, and the tsconfig.

1h 30m
0 problems
Topic Progress 0%

Why TypeScript and Setup

Why TypeScript and Setup

TypeScript is a statically typed superset of JavaScript that compiles to plain JavaScript. It catches errors at compile time rather than runtime, improving code quality and developer experience with autocompletion and inline documentation.

Installation

npm install -D typescript @types/node ts-node
npx tsc --init

tsconfig.json Essentials

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "jsx": "react-jsx",
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Key Compiler Flags

The strict flag enables all strict type-checking options including strictNullChecks, strictFunctionTypes, and strictBindCallApply. Setting moduleResolution: bundler is essential for modern bundlers like Vite or webpack. The noUncheckedIndexedAccess flag makes array and object access return T | undefined, preventing undefined reference errors. The exactOptionalPropertyTypes flag distinguishes between a missing property and one explicitly set to undefined.

Running TypeScript

# Compile once
npx tsc

# Watch mode
npx tsc --watch

# Run directly with ts-node
npx ts-node src/index.ts

Type Annotations and Interfaces

Type Annotations and Interfaces

Basic Types

TypeScript provides several primitive and composite types for annotating variables:

let count: number = 42;
let name: string = 'Alice';
let isActive: boolean = true;
let items: string[] = ['a', 'b'];
let tuple: [string, number] = ['Alice', 30];

// Union types allow multiple types
let id: string | number = 'abc-123';
id = 42; // also valid

// Literal types restrict to specific values
type Status = 'idle' | 'loading' | 'success' | 'error';
let status: Status = 'loading';

Interfaces vs Type Aliases

Interfaces are ideal for object shapes and support declaration merging. Type aliases are more flexible and can represent unions, intersections, and primitives:

// Interface: extendable, supports declaration merging
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

interface AdminUser extends User {
  role: 'admin';
  permissions: string[];
}

// Declaration merging - add to existing interface
interface User {
  lastLogin?: Date;
}

// Type alias: more flexible, can represent unions
type ApiResponse<T> = {
  data: T;
  meta: {
    page: number;
    total: number;
    hasNext: boolean;
  };
};

// Intersection types combine multiple types
type UserWithPosts = User & {
  posts: Post[];
};

Function Types

TypeScript enforces parameter and return types for functions:

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

const multiply = (a: number, b: number): number => a * b;

// Optional and default parameters
function greet(name: string, greeting = 'Hello'): string {
  return `${greeting}, ${name}`;
}

// Function type signature
type SearchFn = (query: string, limit?: number) => Promise<Result[]>;

// Void return type for side-effect functions
function logMessage(message: string): void {
  console.log(message);
}

Type Guards

Type guards narrow the type of a variable within a conditional block:

function processValue(value: string | number | boolean) {
  if (typeof value === 'string') {
    return value.toUpperCase();  // TS knows it's string
  }
  if (typeof value === 'number') {
    return value.toFixed(2);    // TS knows it's number
  }
  return value ? 'Yes' : 'No'; // TS knows it's boolean
}

// Custom type guard using 'is' keyword
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    'name' in obj &&
    'email' in obj
  );
}

Generics and Utility Types

Generics and Utility Types

Generic Functions

Generics allow you to write functions that work with any type while preserving type information:

function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const num = first([1, 2, 3]);     // number | undefined
const str = first(['a', 'b']);    // string | undefined

// Generic constraints with 'extends'
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = { id: '1', name: 'Alice', email: 'a@b.com', createdAt: new Date() };
getProperty(user, 'name');  // returns string
// getProperty(user, 'age'); // Error: 'age' not in keyof User

Generic Interfaces and Classes

interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  create(item: Omit<T, 'id'>): Promise<T>;
  update(id: string, item: Partial<T>): Promise<T>;
  delete(id: string): Promise<boolean>;
}

class UserRepository implements Repository<User> {
  async findById(id: string): Promise<User | null> {
    return db.users.findById(id);
  }
  async findAll(): Promise<User[]> {
    return db.users.findAll();
  }
  async create(item: Omit<User, 'id'>): Promise<User> {
    return db.users.create({ ...item, id: crypto.randomUUID() });
  }
  async update(id: string, item: Partial<User>): Promise<User> {
    return db.users.update(id, item);
  }
  async delete(id: string): Promise<boolean> {
    return db.users.delete(id);
  }
}

Built-in Utility Types

TypeScript provides several utility types to transform and manipulate types:

// Partial<T> - all properties become optional
type UserPartial = Partial<User>;

// Required<T> - all properties become required
type UserRequired = Required<User>;

// Pick<T, K> - select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;

// Omit<T, K> - exclude specific properties
type CreateUser = Omit<User, 'id' | 'createdAt'>;

// Record<K, V> - create typed maps
const usersById: Record<string, User> = {};

// Readonly<T> - prevent property modifications
const frozenUser: Readonly<User> = { ...user };

// Exclude<T, U> - remove types from a union
type StringOrNumber = string | number | boolean;
type NoBoolean = Exclude<StringOrNumber, boolean>;  // string | number

// Extract<T, U> - extract types from a union
type OnlyString = Extract<StringOrNumber, string>;  // string

// ReturnType<T> - extract function return type
type CreateUserReturn = ReturnType<typeof createUser>;

TypeScript with React

TypeScript with React

Component Props

Define prop types using interfaces for clear component contracts:

interface ButtonProps {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  onClick?: () => void;
}

function Button({ children, variant = 'primary', size = 'md', disabled = false, onClick }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

useState with Generics

const [user, setUser] = useState<User | null>(null);
const [count, setCount] = useState(0);
const [items, setItems] = useState<Item[]>([]);

// Derived state with useMemo
const activeItems = useMemo(() => items.filter(i => i.active), [items]);

// Complex state with interface
interface FormState {
  values: Record<string, string>;
  errors: Record<string, string>;
  isSubmitting: boolean;
}

const [form, setForm] = useState<FormState>({
  values: {},
  errors: {},
  isSubmitting: false,
});

Event Handling

function SearchForm() {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const query = formData.get('query') as string;
    search(query);
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="search" name="query" onChange={handleChange} />
      <button type="submit">Search</button>
    </form>
  );
}

API Data Fetching Hook

interface UseFetchResult<T> {
  data: T | null;
  error: Error | null;
  isLoading: boolean;
}

function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setIsLoading(false));
  }, [url]);

  return { data, error, isLoading };
}

TypeScript with Node.js

TypeScript with Node.js

Typed Express Server

import express, { Request, Response, NextFunction } from 'express';
import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().int().min(13).optional(),
});

type CreateUserInput = z.infer<typeof CreateUserSchema>;

type TypedRequest<T> = Request & { body: T };

async function createUser(
  req: TypedRequest<CreateUserInput>,
  res: Response,
  next: NextFunction
) {
  try {
    const validated = CreateUserSchema.parse(req.body);
    const user = await db.users.create({
      ...validated,
      id: crypto.randomUUID(),
      createdAt: new Date(),
    });
    res.status(201).json(user);
  } catch (error) {
    if (error instanceof z.ZodError) {
      return res.status(400).json({ errors: error.errors });
    }
    next(error);
  }
}

app.post('/api/users', createUser);

Typed Database Layer

import { Pool } from 'pg';

interface DatabaseUser {
  id: string;
  name: string;
  email: string;
  created_at: Date;
}

class UserDatabase {
  constructor(private pool: Pool) {}

  async findById(id: string): Promise<DatabaseUser | null> {
    const { rows } = await this.pool.query<DatabaseUser>(
      'SELECT * FROM users WHERE id = $1',
      [id]
    );
    return rows[0] ?? null;
  }

  async create(input: { name: string; email: string }): Promise<DatabaseUser> {
    const { rows } = await this.pool.query<DatabaseUser>(
      'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
      [input.name, input.email]
    );
    return rows[0];
  }
}

Environment Variables

// env.ts
import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
});

export const env = envSchema.parse(process.env);

Error Handling

class AppError extends Error {
  constructor(
    public statusCode: number,
    public code: string,
    message: string
  ) {
    super(message);
    this.name = 'AppError';
  }
}

function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: { code: err.code, message: err.message }
    });
  }
  console.error(err);
  res.status(500).json({ error: { code: 'INTERNAL', message: 'Server error' } });
}

Quiz

1. What is the difference between an interface and a type alias in TypeScript?

Question 1 options

2. What does the Partial<T> utility type do?

Question 2 options

3. What is the purpose of a type guard in TypeScript?

Question 3 options

Flashcards

Question

What is TypeScript Introduction?

Answer

TypeScript Introduction covers important concepts and best practices.

Question

What is TypeScript Introduction?

Answer

TypeScript Introduction covers important concepts and best practices.

Question

What is TypeScript Introduction?

Answer

TypeScript Introduction covers important concepts and best practices.

Revision Notes

Key Takeaways

  • 1. TypeScript adds static type checking to JavaScript, catching errors at compile time before they reach production
  • 2. Interfaces are preferred for object shapes and support declaration merging; type aliases are more flexible for unions and intersections
  • 3. The `strict: true` compiler option enables comprehensive type checking including null checks and strict function types
  • 4. Generics allow you to write reusable, type-safe code that works with any type while preserving type information
  • 5. Utility types like Partial, Required, Pick, Omit, and Record help transform and manipulate existing types efficiently
  • 6. Type guards narrow union types to specific types within conditional blocks, enabling safe type-specific operations

Interview Tips

  • Explain when you would use `interface` versus `type` and why—interfaces for OOP patterns, type aliases for complex compositions
  • Demonstrate understanding of generics by writing a generic Repository pattern or API response handler
  • Discuss how TypeScript improves developer experience through autocompletion, inline documentation, and refactoring safety
  • Be prepared to explain type guards and how they help narrow union types at runtime
  • Know common utility types and when to apply them—Partial for updates, Pick/Omit for API payloads, Record for maps
  • Explain the trade-offs of strict mode: more errors caught upfront but potentially more verbose code

Cheat Sheet

0

Basic types: string, number, boolean, null, undefined, symbol, bigint, void, never, unknown, any

1

Arrays: string[] or Array<string>. Tuples: [string, number]. Enums: enum Color { Red, Green }

2

Interfaces: interface User { id: string; name: string; }. Type aliases: type Status = 'a' | 'b'

3

Functions: function add(a: number, b: number): number. Arrow: const fn = (x: number): string => ...

4

Generics: function first<T>(arr: T[]): T | undefined. Constraints: <T extends Foo>

5

Utility types: Partial, Required, Pick<T,K>, Omit<T,K>, Record<K,V>, Readonly, ReturnType

6

Type guards: typeof x === 'string', x instanceof Foo, custom function isFoo(x: T): x is Foo

7

React: useState<Type>(), useRef<Type>(), event types like React.ChangeEvent<HTMLInputElement>