TypeScript for JavaScript Developers — Complete Beginners Guide 2026

Sanjeev SharmaSanjeev Sharma
9 min read

Advertisement

Introduction

Why This Matters

TypeScript is no longer an optional enhancement — it is the default for professional JavaScript development in 2026. The entire React, Next.js, Node.js, and Angular ecosystems have gone TypeScript-first. TypeScript catches an estimated 15% of all runtime bugs at compile time according to Microsoft research, and it transforms your IDE from a text editor into an intelligent coding assistant with real-time error detection, automated refactoring, and accurate autocomplete. If you know JavaScript, you are 80% of the way there — this guide covers the remaining 20%.

What TypeScript Actually Is

TypeScript is a strict syntactical superset of JavaScript. Every valid JavaScript file is a valid TypeScript file. TypeScript adds optional type annotations that the compiler checks at build time, then strips them away — the output is clean, standard JavaScript that runs anywhere.

your-file.ts → TypeScript compiler (tsc) → your-file.js

           type checking happens here
           (errors stop the build)
           types stripped in output

Setup in 60 Seconds

# In any Node.js project
npm install --save-dev typescript @types/node
npx tsc --init   # generates tsconfig.json with sensible defaults
 
# Run TypeScript directly during development
npm install --save-dev ts-node tsx
npx tsx your-file.ts
 
# For a Next.js project — TypeScript is built in, just rename .js to .tsx

A minimal tsconfig.json for modern projects:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

Always enable "strict": true — it activates the full suite of type checks that make TypeScript actually useful.

Basic Type Annotations

// Primitive types
let username:  string  = 'Alice'
let age:       number  = 30
let isActive:  boolean = true
let score:     bigint  = 1000n
 
// TypeScript infers types from the assigned value — annotation is optional
let inferred = 'Hello'   // TypeScript infers: string
inferred = 42            // ❌ Error: Type 'number' is not assignable to type 'string'
 
// Special types
let flexible: any       = 'anything'   // ❌ Avoid — disables type checking
let flexible2: unknown  = 'anything'   // ✅ Safe — must narrow before use
let noop: void          = undefined    // for functions that return nothing
let unreachable: never                 // for values that should never exist

Functions

TypeScript requires parameter types; return types can be inferred but explicit annotations improve readability:

// Parameter and return type annotation
function greet(name: string, greeting: string = 'Hello'): string {
  return `${greeting}, ${name}!`
}
 
// Optional parameter
function createUser(name: string, email?: string): object {
  return { name, email: email ?? null }
}
 
// Arrow function
const multiply = (a: number, b: number): number => a * b
 
// Rest parameters
function sum(...numbers: number[]): number {
  return numbers.reduce((acc, n) => acc + n, 0)
}
 
// Function type
type Transformer = (input: string) => string
const upper: Transformer = (s) => s.toUpperCase()
 
// Overloads — for functions with different call signatures
function format(value: string): string
function format(value: number): string
function format(value: string | number): string {
  return typeof value === 'string' ? value.trim() : value.toFixed(2)
}

Interfaces and Type Aliases

// Interface — preferred for object shapes and classes
interface User {
  readonly id:   number    // can't be changed after creation
  name:          string
  email:         string
  age?:          number    // optional
  role:          'admin' | 'user' | 'moderator'
}
 
// Extend an interface
interface AdminUser extends User {
  permissions: string[]
  lastLogin:   Date
}
 
// Type alias — great for unions, primitives, and complex compositions
type ID        = string | number
type Status    = 'active' | 'inactive' | 'pending'
type Nullable<T> = T | null
 
// Intersection — combine multiple types
type FullProfile = User & { preferences: Record<string, string> }
 
// When to use which:
// - Interface: object shapes, classes, extendable contracts
// - Type alias: unions, primitives, utility compositions, function types

Union, Intersection, and Literal Types

// Union — one of several possible types
type Input = string | number | boolean
 
// Literal types — specific allowed values
type Theme     = 'light' | 'dark' | 'system'
type HttpVerb  = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
type Port      = 80 | 443 | 3000 | 8080
 
function setTheme(theme: Theme): void {
  document.body.dataset.theme = theme
}
 
setTheme('light')    // ✅
setTheme('purple')   // ❌ Error at compile time!
 
// Discriminated union — the key pattern for complex state
type ApiState<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error';   error: string }
 
function renderUser(state: ApiState<User>) {
  switch (state.status) {
    case 'loading': return 'Loading...'
    case 'success': return state.data.name    // TypeScript knows data exists
    case 'error':   return state.error        // TypeScript knows error exists
  }
}

Generics — Reusable, Type-Safe Components

Generics let you write a function or class once and have it work correctly with any type:

// Generic function
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0]
}
 
const firstNum  = getFirst([1, 2, 3])       // type: number | undefined
const firstName = getFirst(['a', 'b', 'c']) // type: string | undefined
 
// Generic with constraint — T must have an 'id' property
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id)
}
 
// Generic interface — for API responses
interface ApiResponse<T> {
  data:    T
  status:  number
  message: string
  meta?: {
    total:   number
    page:    number
    perPage: number
  }
}
 
// Generic class
class Repository<T extends { id: number }> {
  private items: T[] = []
 
  add(item: T): void {
    this.items.push(item)
  }
 
  findById(id: number): T | undefined {
    return this.items.find(i => i.id === id)
  }
 
  findAll(): T[] {
    return [...this.items]
  }
}
 
const users = new Repository<User>()
users.add({ id: 1, name: 'Alice', email: 'a@b.com', role: 'user' })

Utility Types — TypeScript's Built-in Power Tools

TypeScript ships with a rich library of generic utility types that transform existing types:

interface User {
  id:       number
  name:     string
  email:    string
  password: string
  role:     'admin' | 'user'
}
 
// Partial — make all properties optional (useful for update payloads)
type UpdateUser = Partial<User>
// { id?: number; name?: string; email?: string; ... }
 
// Required — make all properties required
type StrictUser = Required<User>
 
// Pick — select specific properties
type PublicUser = Pick<User, 'id' | 'name' | 'email'>
 
// Omit — exclude specific properties
type SafeUser = Omit<User, 'password'>
 
// Record — dictionary with typed keys and values
type UserMap = Record<number, User>
 
// Readonly — prevent mutation
type ImmutableUser = Readonly<User>
 
// ReturnType — extract a function's return type
function fetchUser(id: number): Promise<User> { ... }
type FetchResult = Awaited<ReturnType<typeof fetchUser>>  // User
 
// Parameters — extract a function's parameter types
type FetchParams = Parameters<typeof fetchUser>  // [number]

Type Narrowing — TypeScript's Superpower

TypeScript tracks your runtime checks and adjusts the type inside each branch:

function processInput(input: string | number | null) {
  // Guard for null
  if (input === null) {
    console.log('No input')
    return
  }
 
  // Now TypeScript knows input is string | number
  if (typeof input === 'string') {
    // Here TypeScript knows: string
    return input.toUpperCase()
  }
 
  // Here TypeScript knows: number
  return input * 2
}
 
// instanceof narrowing
function handleEvent(event: MouseEvent | KeyboardEvent) {
  if (event instanceof KeyboardEvent) {
    console.log('Key pressed:', event.key)    // TypeScript knows event.key exists
  } else {
    console.log('Click at:', event.clientX)   // TypeScript knows event.clientX exists
  }
}
 
// Custom type guard
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'name' in value
  )
}

Real-World Pattern: Typed Service Layer

// Domain types
interface Product {
  id:        number
  name:      string
  price:     number
  category:  string
  inStock:   boolean
}
 
type CreateProduct = Omit<Product, 'id'>
type UpdateProduct = Partial<CreateProduct>
type ProductFilter = Pick<Product, 'category' | 'inStock'>
 
// Service with full type safety
class ProductService {
  private products: Product[] = []
 
  create(input: CreateProduct): Product {
    const product: Product = { id: Date.now(), ...input }
    this.products.push(product)
    return product
  }
 
  update(id: number, changes: UpdateProduct): Product | null {
    const idx = this.products.findIndex(p => p.id === id)
    if (idx === -1) return null
    this.products[idx] = { ...this.products[idx], ...changes }
    return this.products[idx]
  }
 
  filter(criteria: Partial<ProductFilter>): Product[] {
    return this.products.filter(p =>
      (criteria.category === undefined || p.category === criteria.category) &&
      (criteria.inStock   === undefined || p.inStock   === criteria.inStock)
    )
  }
}

Common Mistakes

  • Using any as an escape hatch — it defeats type checking entirely; use unknown with a type guard instead.
  • Not enabling "strict": true — the most valuable checks (null safety, implicit any) are only active in strict mode.
  • Choosing interface vs type arbitrarily — use interface for object shapes you will extend; use type for unions and utility compositions.
  • Forgetting to handle undefined when using optional chaining in TypeScript — noUncheckedIndexedAccess helps catch these.
  • Using as type assertions to silence errors instead of fixing the underlying type mismatch.

Best Practices

  • Enable strict: true in tsconfig.json from day one — retrofitting it into an existing codebase is painful.
  • Use unknown instead of any for values from external sources (API responses, user input); write a type guard to narrow it.
  • Prefer interface for anything that forms a public API contract — interfaces produce better error messages and support declaration merging.
  • Leverage utility types (Partial, Omit, Pick, Record) aggressively — they keep your types DRY.
  • Use discriminated unions for modelling state (loading/success/error) — TypeScript can exhaustively check all cases.
  • Annotate return types on public functions explicitly — it documents intent and catches accidental type widening.

Key Takeaways

  • TypeScript is a strict superset of JavaScript — all valid JavaScript is valid TypeScript, and TypeScript compiles down to plain JavaScript.
  • Enabling "strict": true in tsconfig.json is mandatory to get the full benefit of TypeScript's type system.
  • Interfaces are preferred for object shapes and class contracts; type aliases are preferred for unions, primitives, and utility compositions.
  • Generics allow you to write reusable functions, classes, and interfaces that preserve type information across different concrete types.
  • TypeScript narrows types inside if, switch, and instanceof blocks — this is called control flow analysis.
  • Built-in utility types like Partial, Omit, Pick, Record, and Readonly transform existing types without duplication.
  • Use unknown instead of any for values of uncertain type — it forces you to narrow before use, keeping type safety intact.
  • Discriminated unions with a shared status or kind literal property are the cleanest pattern for modelling complex state machines.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading