DEV Community

Alex Chen
Alex Chen

Posted on

TypeScript Basics: Why and How to Get Started (2026)

TypeScript Basics: Why and How to Get Started (2026)

TypeScript isn't just "JavaScript with types." It's a different way of thinking about code.

Why TypeScript in 2026?

The #1 reason I use TypeScript:
Catches bugs BEFORE they reach production.

Real examples from my projects:
→ "Cannot read property 'map' of undefined" → TS catches at compile time
→ Passed string where number expected → TS refuses to compile
→ Misspelled property name → TS tells you immediately
→ Forgot to handle null return → TS warns you

The tradeoff:
+ Fewer runtime bugs
+ Better IDE autocomplete
+ Self-documenting code
- Slightly more verbose
- Build step required
- Learning curve for advanced types
Enter fullscreen mode Exit fullscreen mode

Getting Started

Installation & Setup

# Initialize a new project
npm init -y
npm install typescript --save-dev
npx tsc --init

# Or create an Express + TypeScript project quickly
npm init -y
npm install express
npm install -D typescript @types/express @types/node ts-node nodemon
npx tsc --init
Enter fullscreen mode Exit fullscreen mode

tsconfig.json Essentials

{
  "compilerOptions": {
    "target": "ES2022",                // Output JS version
    "module": "NodeNext",              // Modern module resolution
    "moduleResolution": "NodeNext",
    "outDir": "./dist",                // Compiled output directory
    "rootDir": "./src",               // Source directory
    "strict": true,                   // Enable all strict checks (IMPORTANT!)
    "esModuleInterop": true,          // Interop with CommonJS
    "skipLibCheck": true,             // Skip type checking .d.ts files (faster)
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,              // Generate .d.ts files for libraries
    "sourceMap": true,                // Generate source maps for debugging
    "noUnusedLocals": true,           // Error on unused variables
    "noUnusedParameters": true,       // Error on unused parameters
    "noImplicitReturns": true,        // Error if function might not return value
    "noFallthroughCasesInSwitch": true // Error on switch fallthrough
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
Enter fullscreen mode Exit fullscreen mode

package.json Scripts

{
  "scripts": {
    "build": "tsc",
    "dev": "ts-node-esm src/server.ts",
    "dev:watch": "nodemon --exec ts-node-esm src/server.ts",
    "start": "node dist/server.js",
    "typecheck": "tsc --noEmit"
  }
}
Enter fullscreen mode Exit fullscreen mode

Core Type System

Basic Types

// Primitive types
let isActive: boolean = true;
let count: number = 42;
let name: string = "Alice";
let nothing: null = null;
let notDefined: undefined = undefined;

// bigint and symbol (rarely used but exist)
let bigNum: bigint = 9007199254740991n;
let sym: symbol = Symbol("unique");

// Arrays
const numbers: number[] = [1, 2, 3];
const strings: Array<string> = ["a", "b", "c"];

// Tuples (fixed-length arrays with known types)
let user: [string, number] = ["Alice", 30];
// user = [30, "Alice"]; // ERROR — wrong order!

// Any (avoid when possible!)
let risky: any = "could be anything";
risky = 42;          // No error
risky.foo();         // No error — defeats the purpose of TS!

// Unknown (safer alternative to any)
let safer: unknown = "could be anything";
// safer.foo();       // ERROR — must check type first
if (typeof safer === "string") {
  console.log(safer.toUpperCase()); // OK — narrowed to string
}

// Void (functions that don't return)
function log(message: string): void {
  console.log(message);
  // No return needed
}

// Never (functions that never complete)
function fail(message: string): never {
  throw new Error(message); // Never returns
}
Enter fullscreen mode Exit fullscreen mode

Objects & Interfaces

// Interface — best for object shapes
interface User {
  id: string;
  email: string;
  name: string;
  role: 'user' | 'admin' | 'moderator';
  createdAt: Date;
  updatedAt?: Date;     // Optional field (?)
  readonly internalId: number; // Read-only
}

// Usage
const user: User = {
  id: 'usr_123',
  email: 'alice@example.com',
  name: 'Alice',
  role: 'admin',
  createdAt: new Date(),
  internalId: 9001,
};

// user.internalId = 9999; // ERROR — readonly!
// user.role = 'superadmin'; // ERROR — not in union type!

// Type alias — more flexible than interface
type ID = string;
type Status = 'pending' | 'active' | 'inactive' | 'suspended';
type UserWithStatus = User & { status: Status }; // Intersection

// When to use which:
// → Interface: object shapes, classes implementing them
// → Type alias: unions, intersections, computed types, primitives
Enter fullscreen mode Exit fullscreen mode

Functions

// Full type annotation
function add(a: number, b: number): number {
  return a + b;
}

// Return type inferred (preferred when obvious)
function greet(name: string) {
  return `Hello, ${name}!`; // Inferred as string
}

// Default parameters
function createUser(name: string, role: string = 'user') {
  return { name, role };
}

// Optional parameters
function findUser(id: string, includeDeleted?: boolean) {
  // includeDeleted is boolean | undefined
}

// Rest parameters
function sum(...numbers: number[]): number {
  return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4, 5); // 15

// Function as type
type FilterFn<T> = (item: T, index: number) => boolean;

function filter<T>(items: T[], predicate: FilterFn<T>): T[] {
  return items.filter(predicate);
}

// Callback typing
function fetchData(
  url: string,
  onSuccess: (data: unknown) => void,
  onError?: (error: Error) => void
): void { /* ... */ }

// Async functions always return Promise<T>
async function getUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  return response.json(); // Inferred as Promise<User>
}
Enter fullscreen mode Exit fullscreen mode

Practical Examples

Express Server with TypeScript

import express, { Request, Response, NextFunction } from 'express';
import helmet from 'helmet';
import cors from 'cors';

// Extend Express Request to include custom properties
declare global {
  namespace Express {
    interface Request {
      user?: AuthenticatedUser;
      requestId?: string;
    }
  }
}

interface AuthenticatedUser {
  id: string;
  email: string;
  role: 'user' | 'admin';
}

const app = express();

app.use(helmet());
app.use(cors());
app.use(express.json());

// Typed middleware
function auth(req: Request, _res: Response, next: NextFunction) {
  const header = req.headers.authorization;
  if (!header?.startsWith('Bearer ')) {
    throw new UnauthorizedError('Missing token');
  }

  const payload = verifyToken(header.slice(7));
  req.user = payload; // Works because we extended the Request type
  next();
}

// Typed route handlers
app.get('/api/users/:id', auth, async (req: Request, res: Response) => {
  const user = await userService.findById(req.params.id);
  res.json(user);
});

// Generic API response helper
interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: { code: string; message: string };
}

function ok<T>(res: Response, data: T, status = 200) {
  return res.status(status).json<ApiResponse<T>>({ success: true, data });
}

function fail(res: Response, code: string, message: string, status = 400) {
  return res.status(status).json<ApiResponse<null>>({
    success: false,
    error: { code, message },
  });
}

// Usage
ok(res, user);                    // infers data type from user
fail(res, 'NOT_FOUND', 'User');   // data is null
Enter fullscreen mode Exit fullscreen mode

Database Models

// Type-safe database operations
interface BaseEntity {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}

interface User extends BaseEntity {
  email: string;
  name: string;
  passwordHash: string;
  role: UserRole;
}

type UserRole = 'user' | 'admin' | 'moderator';

interface CreateUserData {
  email: string;
  name: string;
  password: string;
  role?: UserRole; // defaults to 'user'
}

class UserService {
  async create(data: CreateUserData): Promise<User> {
    const passwordHash = await hashPassword(data.password);

    const user = await db.query(
      'INSERT INTO users (email, name, password_hash, role) VALUES ($1, $2, $3, $4) RETURNING *',
      [data.email, data.name, passwordHash, data.role || 'user']
    );

    return user.rows[0]; // Type-safe access
  }

  async findById(id: string): Promise<User | null> {
    const result = await db.query('SELECT * FROM users WHERE id = $1', [id]);
    return result.rows[0] ?? null; // Proper null handling
  }
}
Enter fullscreen mode Exit fullscreen mode

Utility Types (Built-in)

// Partial<T> — make all fields optional
interface Todo { title: string; done: boolean; priority: number; }
function updateTodo(id: string, updates: Partial<Todo>) {
  // updates can be any subset of Todo fields
}
updateTodo('1', { done: true }); // Only update done field

// Required<T> — make all fields required (inverse of Partial)
// Omit<T, K> — remove specific keys
type TodoInput = Omit<Todo, 'id' | 'createdAt' | 'updatedAt'>;

// Pick<T, K> — keep only specific keys
type TodoPreview = Pick<Todo, 'title' | 'done'>;

// Readonly<T> — make all fields read-only
const config: Readonly<AppConfig> = { ... };

// Record<K, V> — key-value mapping
type RolePermissions = Record<UserRole, string[]>;
const permissions: RolePermissions = {
  admin: ['read', 'write', 'delete'],
  user: ['read'],
  moderator: ['read', 'write'],
};

// Extract type from array
const statuses = ['pending', 'active', 'inactive'] as const;
type Status = typeof statuses[number]; // 'pending' | 'active' | 'inactive'

// Awaited<T> — unwrap Promise type
type UserData = Awaited<ReturnType<typeof userService.findById>>;
// Same as User | null
Enter fullscreen mode Exit fullscreen mode

Error Handling with TypeScript

// Custom error class hierarchy
class AppError extends Error {
  constructor(
    public statusCode: number,
    public code: string,
    message: string,
    public details?: unknown
  ) {
    super(message);
    this.name = this.constructor.name;
  }
}

class NotFoundError extends AppError {
  constructor(resource: string) {
    super(404, 'NOT_FOUND', `${resource} not found`);
  }
}

class ValidationError extends AppError {
  constructor(public fields: Array<{ field: string; issue: string }>) {
    super(422, 'VALIDATION_ERROR', 'Validation failed', fields);
  }
}

// Type-narrowing error handler
function isError(error: unknown): error is Error {
  return error instanceof Error;
}

function isAppError(error: unknown): error is AppError {
  return error instanceof AppError;
}

// Safe error handler middleware
function errorHandler(err: unknown, req: Request, res: Response, next: NextFunction) {
  if (isAppError(err)) {
    return res.status(err.statusCode).json({
      error: { code: err.code, message: err.message, details: err.details }
    });
  }

  if (isError(err)) {
    console.error('Unexpected error:', err.stack);
    return res.status(500).json({
      error: { code: 'INTERNAL_ERROR', message: 'An internal error occurred' }
    });
  }

  // Handle non-Error throws (strings, numbers, etc.)
  console.error('Unknown error:', err);
  res.status(500).json({ error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' } });
}
Enter fullscreen mode Exit fullscreen mode

Migration Strategy: JS → TypeScript

Don't rewrite everything at once!

Step 1: Add tsconfig.json with strict: false initially
Step 2: Rename .js files to .ts one by one (start with entry point)
Step 3: Fix errors file by file
Step 4: Enable strict mode gradually:
  → noImplicitAny first
  → strictNullChecks next
  → noUnusedLocals/Params
  → Finally: strict: true

Each file you convert makes your codebase safer.
Even partial TypeScript is better than none.
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Concept Syntax
Basic annotation let x: number = 5
Array number[] or Array<number>
Optional field?: string or `string \
Union {% raw %}`string \
Literal type {% raw %}`'success' \
Interface {% raw %}interface Name { ... }
Type alias type Name = { ... }
Generic function fn<T>(arg: T): T
Partial Partial<User>
Omit/Pick Omit<User, 'id'>
Readonly Readonly<Config>
Assert type value as string
Non-null assert value! (use sparingly!)
Satisfies const obj = {...} satisfies Shape

Are you team JavaScript or team TypeScript?

Follow @armorbreak for more practical developer guides.

Top comments (0)