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:
- Download: Node.js Official Site
- Verify:
node --versionshould show v22.x.x or higher
Install TypeScript globally (optional but useful):
npm install -g typescriptVerify installation:
tsc --versionShould 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/nodeCreate tsconfig.json:
npx tsc --initUpdate 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.jsBetter: Use tsx for development (no compilation needed):
npm install --save-dev tsx
npx tsx src/index.tsNote: 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 propertyType 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 UserGeneric 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" }); // OKRequired
interface Config {
host?: string;
port?: number;
debug?: boolean;
}
// Required makes all properties required
type RequiredConfig = Required<Config>;
// All properties are now requiredReadonly
// 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 errorsstrictFunctionTypes- Stricter function parameter checkingstrictBindCallApply- Type-safe bind/call/applynoImplicitAny- 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
- TypeScript Handbook
- TypeScript Deep Dive
- Type Challenges - Practice advanced types
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.