TypeScript for JavaScript Developers — Complete Beginners Guide 2026
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 outputSetup 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 .tsxA 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 existFunctions
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 typesUnion, 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
anyas an escape hatch — it defeats type checking entirely; useunknownwith a type guard instead. - Not enabling
"strict": true— the most valuable checks (null safety, implicit any) are only active in strict mode. - Choosing
interfacevstypearbitrarily — useinterfacefor object shapes you will extend; usetypefor unions and utility compositions. - Forgetting to handle
undefinedwhen using optional chaining in TypeScript —noUncheckedIndexedAccesshelps catch these. - Using
astype assertions to silence errors instead of fixing the underlying type mismatch.
Best Practices
- Enable
strict: truein tsconfig.json from day one — retrofitting it into an existing codebase is painful. - Use
unknowninstead ofanyfor values from external sources (API responses, user input); write a type guard to narrow it. - Prefer
interfacefor 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": truein 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, andinstanceofblocks — this is called control flow analysis. - Built-in utility types like
Partial,Omit,Pick,Record, andReadonlytransform existing types without duplication. - Use
unknowninstead ofanyfor values of uncertain type — it forces you to narrow before use, keeping type safety intact. - Discriminated unions with a shared
statusorkindliteral property are the cleanest pattern for modelling complex state machines.
Advertisement