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.
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.
- TypeScript Learning Journey
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
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/andnode_modules/are generated folders and should stay out of version control (see.gitignore).
- Node.js (LTS recommended) and npm
- A code editor with TypeScript support, such as VS Code
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 --versionInitialize the project and generate a TypeScript config:
npm init -y # creates package.json
npx tsc --init # creates tsconfig.jsontsc --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 every file in src/ into dist/
tsc
# Run a compiled file with Node
node dist/basics.js
node dist/classes.jsEach 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 JavaScriptFile: 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.
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);
}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.
// 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// 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.
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.
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).
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.
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 valuesUtility types transform existing types, so you avoid redefining similar shapes by hand.
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.
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.
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": trueintsconfig.json. - Use
unknowninstead ofany, and narrow before use. - Let TypeScript infer simple types; annotate function parameters and return types.
- Prefer type guards over type assertions.
- Use
readonlyandReadonly<T>for data that should not change. - Use literal unions for fixed sets of values.
- Handle errors in async code with
try/catch.
- Types are checked at compile time and erased in the compiled JavaScript.
tsccompilessrc/intodist/; Node runs the compiled output.interfacedescribes object shapes;typehandles 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.
strictmode and avoidinganyare the biggest wins for code quality.
- 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
- 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
buildandstarttopackage.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