TypeScript - Complete Guide to Modern Type-Safe JavaScript
What is TypeScript and Why Has It Become Essential?
TypeScript is a programming language developed by Microsoft that extends JavaScript with static type checking. Every valid JavaScript code is valid TypeScript, but TypeScript adds an optional type system that catches errors at compile time rather than runtime.
In 2025, TypeScript has become the de facto standard for professional JavaScript development. Major frameworks like Angular, Vue 3, Next.js, and virtually every serious library are built with TypeScript. Understanding TypeScript is no longer optional—it's essential for modern web development.
Why Developers Choose TypeScript
Catch Bugs Before They Reach Production
// JavaScript - runtime error
function greet(name) {
return 'Hello, ' + name.toUppercase() // typo - toUpperCase
}
greet('world') // Error in production!
// TypeScript - error in your editor
function greet(name: string): string {
return 'Hello, ' + name.toUppercase() // ❌ Property 'toUppercase' does not exist
}Intelligent Autocomplete
interface User {
id: number
name: string
email: string
role: 'admin' | 'user'
}
const user: User = getUser(1)
user. // Autocomplete shows: id, name, email, roleFearless Refactoring
Change a function signature or rename a property, and TypeScript shows every place in your codebase that needs updating.
Core Types and Type System
Primitive Types
// Basic types
let name: string = 'John'
let age: number = 25
let isActive: boolean = true
let nothing: null = null
let notDefined: undefined = undefined
// Type inference - TypeScript infers the type
let city = 'London' // type: string
let count = 42 // type: number
// Arrays
let numbers: number[] = [1, 2, 3]
let names: Array<string> = ['Alice', 'Bob']
// Tuple - fixed length array with known types
let person: [string, number] = ['John', 25]Object Types and Interfaces
// Interface - describes object shape
interface User {
id: number
name: string
email: string
age?: number // optional property
readonly createdAt: Date // cannot be modified
}
// Type alias - similar but more flexible
type Point = {
x: number
y: number
}
// Extending interfaces
interface Admin extends User {
permissions: string[]
}
// Intersection types
type AdminUser = User & { permissions: string[] }Union and Literal Types
// Union types - value can be one of several types
type Status = 'pending' | 'active' | 'completed'
type Result = string | number | null
function formatValue(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase()
}
return value.toFixed(2)
}
// Discriminated unions - powerful pattern matching
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number }
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2
case 'rectangle':
return shape.width * shape.height
case 'triangle':
return (shape.base * shape.height) / 2
}
}Special Types
// any - disables type checking (avoid!)
let anything: any = 'string'
anything = 42 // OK but dangerous
// unknown - safer alternative to any
let value: unknown = 'hello'
// value.toUpperCase() // ❌ Error - must be narrowed first
if (typeof value === 'string') {
value.toUpperCase() // ✅ OK after type guard
}
// never - represents values that never occur
function throwError(message: string): never {
throw new Error(message)
}
// void - function returns nothing
function logMessage(msg: string): void {
console.log(msg)
}Functions in TypeScript
Function Types
// Function type annotation
function add(a: number, b: number): number {
return a + b
}
// Arrow function
const multiply = (a: number, b: number): number => a * b
// Function type alias
type MathOperation = (a: number, b: number) => number
const divide: MathOperation = (a, b) => a / b
// Optional and default parameters
function greet(name: string, greeting: string = 'Hello'): string {
return `${greeting}, ${name}!`
}
// Rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((acc, n) => acc + n, 0)
}Function Overloads
// Multiple function signatures
function parse(input: string): object
function parse(input: object): string
function parse(input: string | object): string | object {
if (typeof input === 'string') {
return JSON.parse(input)
}
return JSON.stringify(input)
}
const obj = parse('{"name":"John"}') // returns object
const str = parse({ name: 'John' }) // returns stringGenerics - The Power of Type Parameters
Basic Generics
// Generic function
function identity<T>(value: T): T {
return value
}
const num = identity(42) // type: number
const str = identity('hello') // type: string
// Generic interface
interface Box<T> {
value: T
getValue(): T
}
const numberBox: Box<number> = {
value: 42,
getValue() {
return this.value
},
}
// Generic constraints
interface HasLength {
length: number
}
function getLength<T extends HasLength>(item: T): number {
return item.length
}
getLength('hello') // ✅ string has length
getLength([1, 2, 3]) // ✅ array has length
// getLength(42) // ❌ number doesn't have lengthAdvanced Generic Patterns
// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second]
}
// Generic with default type
interface ApiResponse<T = unknown> {
data: T
status: number
message: string
}
// keyof operator
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: 'John', age: 30 }
const name = getProperty(user, 'name') // type: string
const age = getProperty(user, 'age') // type: numberUtility Types
TypeScript provides powerful built-in utility types:
interface User {
id: number
name: string
email: string
password: string
}
// Partial - all properties optional
type PartialUser = Partial<User>
// { id?: number; name?: string; email?: string; password?: string }
// Required - all properties required
type RequiredUser = Required<PartialUser>
// Pick - select specific properties
type UserCredentials = Pick<User, 'email' | 'password'>
// { email: string; password: string }
// Omit - exclude specific properties
type PublicUser = Omit<User, 'password'>
// { id: number; name: string; email: string }
// Record - construct object type
type UserRoles = Record<string, User>
// { [key: string]: User }
// Readonly - all properties readonly
type ReadonlyUser = Readonly<User>
// ReturnType - extract function return type
function createUser() {
return { id: 1, name: 'John' }
}
type CreateUserReturn = ReturnType<typeof createUser>
// { id: number; name: string }
// Parameters - extract function parameters
type CreateUserParams = Parameters<typeof createUser>
// Awaited - unwrap Promise type
type UserPromise = Promise<User>
type ResolvedUser = Awaited<UserPromise> // UserType Guards and Narrowing
// typeof guard
function processValue(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase() // TypeScript knows it's string
}
return value.toFixed(2) // TypeScript knows it's number
}
// instanceof guard
class Dog {
bark() {
return 'Woof!'
}
}
class Cat {
meow() {
return 'Meow!'
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
return animal.bark()
}
return animal.meow()
}
// in operator guard
interface Bird {
fly(): void
}
interface Fish {
swim(): void
}
function move(animal: Bird | Fish) {
if ('fly' in animal) {
animal.fly()
} else {
animal.swim()
}
}
// Custom type guard
function isString(value: unknown): value is string {
return typeof value === 'string'
}
function processUnknown(value: unknown) {
if (isString(value)) {
return value.toUpperCase() // TypeScript knows it's string
}
}Classes and Object-Oriented TypeScript
// Class with access modifiers
class User {
public name: string
private password: string
protected email: string
readonly id: number
constructor(name: string, email: string, password: string) {
this.id = Math.random()
this.name = name
this.email = email
this.password = password
}
// Shorthand constructor
// constructor(
// public name: string,
// protected email: string,
// private password: string,
// readonly id = Math.random()
// ) {}
validatePassword(input: string): boolean {
return this.password === input
}
}
// Abstract classes
abstract class Shape {
abstract getArea(): number
describe(): string {
return `This shape has area ${this.getArea()}`
}
}
class Circle extends Shape {
constructor(private radius: number) {
super()
}
getArea(): number {
return Math.PI * this.radius ** 2
}
}
// Implementing interfaces
interface Serializable {
serialize(): string
}
class Product implements Serializable {
constructor(
public name: string,
public price: number
) {}
serialize(): string {
return JSON.stringify(this)
}
}TypeScript with React
Component Props
// Props interface
interface ButtonProps {
label: string
onClick: () => void
variant?: 'primary' | 'secondary'
disabled?: boolean
children?: React.ReactNode
}
// Functional component
const Button: React.FC<ButtonProps> = ({
label,
onClick,
variant = 'primary',
disabled = false,
children,
}) => {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{children || label}
</button>
)
}
// Props with generics
interface ListProps<T> {
items: T[]
renderItem: (item: T) => React.ReactNode
keyExtractor: (item: T) => string
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
)
}Hooks with TypeScript
// useState
const [count, setCount] = useState<number>(0)
const [user, setUser] = useState<User | null>(null)
// useRef
const inputRef = useRef<HTMLInputElement>(null)
const valueRef = useRef<number>(0) // mutable ref
// useReducer
interface State {
count: number
loading: boolean
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'setLoading'; payload: boolean }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 }
case 'decrement':
return { ...state, count: state.count - 1 }
case 'setLoading':
return { ...state, loading: action.payload }
}
}
const [state, dispatch] = useReducer(reducer, { count: 0, loading: false })
// Custom hook
function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
})
const setValue = (value: T) => {
setStoredValue(value)
localStorage.setItem(key, JSON.stringify(value))
}
return [storedValue, setValue]
}Configuration with tsconfig.json
{
"compilerOptions": {
// Target and module
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
// Strict mode (recommended)
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// Module interop
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
// Output
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
// Path aliases
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"]
},
// Other
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"jsx": "preserve"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Best Practices
Do's
// ✅ Use interfaces for object shapes
interface User {
name: string
email: string
}
// ✅ Prefer type inference when obvious
const count = 42 // not const count: number = 42
// ✅ Use union types for function parameters
function format(value: string | number): string {
return String(value)
}
// ✅ Use strict null checks
function getUser(id: number): User | null {
// explicitly handle null case
}
// ✅ Use readonly for immutable data
interface Config {
readonly apiUrl: string
readonly timeout: number
}Don'ts
// ❌ Avoid 'any' - use 'unknown' if type is truly unknown
function process(data: any) {} // Bad
function process(data: unknown) {} // Better
// ❌ Don't use type assertions without reason
const user = data as User // Dangerous
const user = isUser(data) ? data : null // Safer with type guard
// ❌ Don't over-annotate - let TypeScript infer
const name: string = 'John' // Unnecessary
const name = 'John' // TypeScript knows it's string
// ❌ Don't use enums in most cases
enum Status { Active, Inactive } // Verbose
type Status = 'active' | 'inactive' // Simpler, better tree-shakingTypeScript 5.x Modern Features
// const type parameters (TS 5.0)
function createTuple<const T extends readonly unknown[]>(items: T): T {
return items
}
const tuple = createTuple([1, 2, 'three']) // readonly [1, 2, "three"]
// Decorators (TS 5.0+)
function logged(target: any, context: ClassMethodDecoratorContext) {
return function (...args: any[]) {
console.log(`Calling ${String(context.name)}`)
return target.apply(this, args)
}
}
class Calculator {
@logged
add(a: number, b: number) {
return a + b
}
}
// satisfies operator (TS 4.9+)
const config = {
endpoint: '/api',
timeout: 5000,
} satisfies Record<string, string | number>
// Type is preserved as { endpoint: string; timeout: number }
// but validated against Record<string, string | number>
// using declarations for resource management (TS 5.2)
function processFile() {
using file = openFile('data.txt')
// file is automatically disposed when scope exits
return file.read()
}Summary
TypeScript transforms JavaScript development by adding a powerful type system that catches errors early, enables better tooling, and makes code more maintainable. Key takeaways:
- Type Safety - Catch errors at compile time, not runtime
- Better Developer Experience - Autocomplete, refactoring, documentation
- Gradual Adoption - Start with JavaScript, add types incrementally
- Ecosystem - Every major framework and library supports TypeScript
- Modern Features - Generics, utility types, and advanced type manipulation
TypeScript is no longer optional for professional JavaScript development—it's the standard that enables teams to build reliable, scalable applications with confidence.