Skip to content

About

Hands-on TypeScript learning repo: one runnable file per concept, from types, interfaces and generics to classes, utility types and async/await.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

TypeScript Learning Journey

TypeScript Node.js

A hands-on repository where I learn TypeScript from the ground up. Each topic lives in its own small, runnable file under src/, so concepts stay focused and easy to revisit.

Overview

TypeScript is JavaScript with a static type system. It catches mistakes at compile time, improves editor tooling, and makes large codebases easier to maintain.

This repository covers the language fundamentals through to asynchronous code and best practices. Every file is compiled with the TypeScript compiler (tsc) and executed with Node.js.

Table of Contents

Learning Roadmap

The topics build on each other, so the suggested order is:

Setup  →  Primitive types  →  Functions  →  Objects & interfaces  →  Type aliases
   →  Union & intersection  →  Literal types  →  Assertions & guards  →  Classes
   →  Generics  →  Utility types  →  Enums  →  Promises & async/await  →  Best practices

Project Structure

typescript/
├── src/                      # TypeScript source files (one topic per file)
│   ├── basics.ts             # Setup and first program
│   ├── basic_types.ts        # Primitive types, arrays, tuples
│   ├── functions.ts          # Typed parameters, return types, optional params
│   ├── objects_interfaces.ts # Object types and interfaces
│   ├── type_alices.ts        # Type aliases
│   ├── type_assertions.ts    # Type assertions and narrowing
│   ├── classes.ts            # Classes, access modifiers, inheritance
│   ├── generics.ts           # Generic functions, interfaces, constraints
│   ├── utility_types.ts      # Built-in utility types
│   ├── enums.ts              # Numeric and string enums
│   ├── async_await.ts        # Promises and async/await
│   └── best_practices.ts     # Recommended patterns
├── dist/                     # Compiled JavaScript output (generated)
├── node_modules/             # Installed dependencies (generated)
├── .gitignore
├── package.json
├── package-lock.json
└── tsconfig.json             # Compiler configuration

dist/ and node_modules/ are generated folders and should stay out of version control (see .gitignore).

Getting Started

Prerequisites

  • Node.js (LTS recommended) and npm
  • A code editor with TypeScript support, such as VS Code

Installation

Install the compiler globally, or per project if you prefer to pin a version:

# Global install
npm install -g typescript

# Verify the installation
tsc --version
# Or install it only for this project
npm install --save-dev typescript
npx tsc --version

Initialize the project and generate a TypeScript config:

npm init -y     # creates package.json
npx tsc --init  # creates tsconfig.json

Compiler Configuration

tsc --init generates a tsconfig.json with many options. These are the ones that matter most when starting out:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
Option Purpose
rootDir / outDir Where TypeScript source lives and where compiled JavaScript is written
target The JavaScript version the output compiles down to
module The module system used in the output
strict Enables strict type checking. Keep this true; turning it off defeats most of the point of TypeScript
skipLibCheck Skips type checking of declaration files to speed up compilation
include / exclude Which files the compiler picks up and which it ignores

Compile and Run

# Compile every file in src/ into dist/
tsc

# Run a compiled file with Node
node dist/basics.js
node dist/classes.js

Each src/<name>.ts file compiles to dist/<name>.js.

Handy variations:

tsc --watch   # Recompile automatically whenever a file changes
tsc --noEmit  # Type-check only, without writing any JavaScript

Topics Covered

1. Primitive Types

File: src/basic_types.ts

let username: string = "dev";
let age: number = 25;
let isActive: boolean = true;
let scores: number[] = [1, 2, 3, 4]; // array of numbers
let tags: string[] = ["ts", "node"]; // array of strings

// let age: number = "twenty"; // Error: Type 'string' is not assignable to type 'number'

// A tuple is a fixed-length array where each position has its own type
let entry: [string, number] = ["item", 42];

TypeScript infers types from initial values, so annotations are often optional. Assigning the wrong type is a compile error, not a runtime surprise.

2. Functions

File: src/functions.ts

// Typed parameters and an explicit return type
function add(a: number, b: number): number {
  return a + b;
}

// Optional parameter (?) and default value
function greet(
  name: string,
  greeting: string = "Hello",
  suffix?: string,
): string {
  return `${greeting}, ${name}${suffix ?? "!"}`;
}

// Arrow function with a function type
const multiply: (x: number, y: number) => number = (x, y) => x * y;

// void: the function returns nothing
function logMessage(message: string): void {
  console.log(message);
}

3. Objects, Interfaces, and Type Aliases

Files: src/objects_interfaces.ts, src/type_alices.ts

// Interface: describes the shape of an object
interface User {
  id: number;
  name: string;
  email?: string; // optional property
  readonly createdAt: Date; // cannot be reassigned
}

const user: User = { id: 1, name: "Sam", createdAt: new Date() };

// Interfaces can extend other interfaces
interface Admin extends User {
  permissions: string[];
}

// Type alias: a name for any type
type ID = number | string;
type Point = { x: number; y: number };

Use an interface for object contracts and a type alias when you need unions, tuples, or other flexible combinations.

4. Union and Intersection Types

// Union (|): a value can be one of several types
type Result = string | number;

function format(value: Result): string {
  return typeof value === "string" ? value.toUpperCase() : value.toFixed(2);
}

// Intersection (&): a value must satisfy all combined types
type Timestamped = { createdAt: Date };
type Named = { name: string };
type Record = Named & Timestamped; // must have BOTH name and createdAt

5. Literal Types

// A literal type restricts a value to exact options
type Role = "admin" | "editor" | "viewer";
type Direction = "up" | "down" | "left" | "right";

let role: Role = "admin";
// role = "guest"; // Error: '"guest"' is not assignable to type 'Role'

function move(direction: Direction): void {
  console.log(`Moving ${direction}`);
}

Literal unions are a lightweight, type-safe alternative to magic strings.

6. Type Assertions and Type Guards

File: src/type_assertions.ts

// Type assertion: you tell the compiler the type (no runtime check!)
const input = document.getElementById("name") as HTMLInputElement;

// Type guard: check the type at runtime so TypeScript can narrow it
function process(value: unknown): string {
  if (typeof value === "string") {
    return value.toUpperCase(); // value is a string here
  }
  return "Invalid value";
}

// Custom type guard using a type predicate (value is Fish)
type Fish = { swim: () => void };
type Bird = { fly: () => void };

function isFish(animal: Fish | Bird): animal is Fish {
  return "swim" in animal; // the 'in' operator also narrows types
}

Prefer type guards over assertions. An assertion silences the compiler, while a guard proves the type.

7. Classes

File: src/classes.ts

class Employee {
  // Parameter properties declare and assign fields in one step
  constructor(
    public name: string,
    private department: string,
    protected readonly id: number,
  ) {}

  describe(): string {
    return `${this.name} works in ${this.department}`;
  }
}

// Inheritance
class Manager extends Employee {
  constructor(
    name: string,
    department: string,
    id: number,
    private team: number,
  ) {
    super(name, department, id);
  }

  override describe(): string {
    return `${super.describe()} and manages ${this.team} people`;
  }
}

// Abstract class: a base that cannot be instantiated directly
abstract class Shape {
  abstract area(): number;
}

class Rectangle extends Shape {
  constructor(
    private width: number,
    private height: number,
  ) {
    super();
  }
  area(): number {
    return this.width * this.height;
  }
}

Access modifiers: public (default), private (this class only), protected (this class and subclasses).

8. Generics

File: src/generics.ts

// A generic function: T is decided by the caller
function identity<T>(value: T): T {
  return value;
}
identity<string>("hello"); // explicit
identity(42); // inferred as number

// Constraint: T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

// Generic interface
interface ApiResponse<T> {
  data: T;
  success: boolean;
}

const response: ApiResponse<string[]> = { data: ["a", "b"], success: true };

Generics let you write reusable code without giving up type safety.

9. Utility Types

File: src/utility_types.ts

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

type ProductDraft = Partial<Product>; // all properties optional
type ProductStrict = Required<Product>; // all properties required
type ProductLocked = Readonly<Product>; // no reassignment
type ProductPreview = Pick<Product, "id" | "title">; // keep only these keys
type ProductInput = Omit<Product, "id">; // remove these keys
type PriceList = Record<string, number>; // object with string keys → number values

Utility types transform existing types, so you avoid redefining similar shapes by hand.

10. Enums

File: src/enums.ts

// Numeric enum: values auto-increment from 0
enum Status {
  Pending, // 0
  Active, // 1
  Archived, // 2
}

// String enum: readable values, easy to debug
enum Color {
  Red = "RED",
  Blue = "BLUE",
}

const current: Status = Status.Active;
console.log(`This shape is ${Color.Blue}`);

For simple cases, a literal union ("red" | "blue") can be a lighter alternative to an enum.

11. Promises and async/await

File: src/async_await.ts

// A function returning a Promise of a specific type
function fetchUser(id: number): Promise<{ id: number; name: string }> {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, name: "Sam" }), 500);
  });
}

// async/await makes asynchronous code read like synchronous code
async function main(): Promise<void> {
  try {
    const user = await fetchUser(1);
    console.log(user.name);
  } catch (error) {
    console.error("Something went wrong:", error);
  }
}

// Run several promises in parallel
async function loadMany(): Promise<void> {
  const users = await Promise.all([fetchUser(1), fetchUser(2)]);
  console.log(users.length); // 2
}

main();

An async function always returns a Promise, and await pauses until it resolves.

Best Practices

File: src/best_practices.ts

// ❌ Avoid 'any': it disables type checking
function badProcess(value: any) {
  return value.toUpperCase(); // crashes at runtime if value isn't a string
}

// ✅ Prefer 'unknown' and check the type before using it
function goodProcess(value: unknown): string {
  if (typeof value === "string") {
    return value.toUpperCase();
  }
  return "Invalid value";
}

// ✅ Interface for object contracts, type for unions and combinations
interface Account {
  id: number;
  name: string;
}
type AccountRole = "admin" | "editor" | "viewer";
type AdminAccount = Account & { role: "admin" };

Checklist of habits to keep:

  • Keep "strict": true in tsconfig.json.
  • Use unknown instead of any, and narrow before use.
  • Let TypeScript infer simple types; annotate function parameters and return types.
  • Prefer type guards over type assertions.
  • Use readonly and Readonly<T> for data that should not change.
  • Use literal unions for fixed sets of values.
  • Handle errors in async code with try/catch.

Key Takeaways

  • Types are checked at compile time and erased in the compiled JavaScript.
  • tsc compiles src/ into dist/; Node runs the compiled output.
  • interface describes object shapes; type handles unions, intersections, and aliases.
  • Narrowing (typeof, in, custom predicates) is the safe way to work with uncertain types.
  • Generics and utility types reduce duplication while keeping type safety.
  • strict mode and avoiding any are the biggest wins for code quality.

Progress Checklist

  • TypeScript setup and tsconfig.json
  • Primitive types, arrays, and tuples
  • Functions
  • Objects and interfaces
  • Type aliases
  • Union and intersection types
  • Literal types
  • Type assertions and type guards
  • Classes
  • Generics
  • Utility types
  • Enums
  • Promises and async/await
  • Best practices

Next Steps

  • Explore advanced types: conditional types, mapped types, and template literal types
  • Learn modules and imports/exports across multiple files
  • Add a linter (ESLint with typescript-eslint) and a formatter (Prettier)
  • Write tests for the examples (for example with Jest or Vitest)
  • Add npm scripts such as build and start to package.json
  • Use TypeScript with a framework: Node.js/Express on the backend or React on the frontend
  • Build a small project that combines everything learned here

Resources

About

Hands-on TypeScript learning repo: one runnable file per concept, from types, interfaces and generics to classes, utility types and async/await.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages