TypeScript is the most in-demand typed language in frontend and full-stack development. If you know JavaScript, learning TypeScript doesn't mean starting over — it means building on what you already know. This roadmap takes you from zero TypeScript to production-ready in the most direct path possible.
Before You Start: What TypeScript Actually Is
TypeScript is JavaScript with a type system. Every valid JavaScript file is valid TypeScript. The learning curve isn't about unlearning JavaScript — it's about adding a layer of annotations that the compiler uses to catch errors before your code runs.
The fundamental promise: catch at compile time what would otherwise blow up at runtime.
// JavaScript — this is valid, until it isn't
function getUserName(user) {
return user.name.toUpperCase()
}
getUserName(null) // ❌ TypeError at runtime
// TypeScript — caught at compile time
function getUserName(user: { name: string }): string {
return user.name.toUpperCase()
}
getUserName(null) // ❌ Error: Argument of type 'null' is not assignable
Stage 1: Foundation (Week 1-2)
Set Up the Environment
# Install TypeScript
npm install -g typescript
# Check version
tsc --version
# Initialize a project
mkdir ts-practice && cd ts-practice
npm init -y
npx tsc --init
The tsconfig.json generated by tsc --init is your compiler configuration. The defaults are reasonable for learning. As you progress, you'll want "strict": true — enable it early.
Core Type Syntax
Primitive types:
let name: string = "Alice"
let age: number = 30
let active: boolean = true
let nothing: null = null
let notDefined: undefined = undefined
Type inference — TypeScript infers types when you initialize variables. You don't need to annotate everything:
let name = "Alice" // TypeScript infers: string
let age = 30 // TypeScript infers: number
Arrays and objects:
let numbers: number[] = [1, 2, 3]
let names: Array<string> = ["Alice", "Bob"]
let user: { name: string; age: number } = {
name: "Alice",
age: 30
}
Functions:
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}`
}
Union Types and Type Narrowing
// Union — can be one of multiple types
type ID = string | number
function processId(id: ID) {
if (typeof id === "string") {
return id.toUpperCase() // TypeScript knows id is string here
}
return id.toFixed(0) // TypeScript knows id is number here
}
Type narrowing — using conditions to help TypeScript understand which type you're working with inside a branch — is one of the most important TypeScript concepts. Master it early.
Interfaces and Type Aliases
// Interface — for object shapes
interface User {
id: number
name: string
email: string
role?: "admin" | "user" // optional property
}
// Type alias — more flexible, works for unions/intersections too
type Status = "active" | "inactive" | "pending"
type AdminUser = User & { permissions: string[] } // intersection
When to use which: interfaces are better for object shapes that might be extended. Type aliases are better for unions, intersections, and complex types.
Stage 1 Milestone: You can add types to existing JavaScript functions and objects without type errors.
Stage 2: Intermediate Concepts (Week 3-4)
Generics
Generics let you write code that works with multiple types while preserving type safety:
// Without generics — loses type information
function first(arr: any[]): any {
return arr[0]
}
// With generics — preserves type information
function first<T>(arr: T[]): T {
return arr[0]
}
const num = first([1, 2, 3]) // TypeScript knows: number
const str = first(["a", "b"]) // TypeScript knows: string
Generics 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") // TypeScript knows: string
const age = getProperty(user, "age") // TypeScript knows: number
// getProperty(user, "email") // ❌ Error: not a key of user
Utility Types
TypeScript ships with built-in utility types that transform existing types:
interface User {
id: number
name: string
email: string
password: string
}
// Partial — makes all properties optional
type UpdateUser = Partial<User>
// Required — makes all properties required
type FullUser = Required<User>
// Pick — select specific properties
type PublicUser = Pick<User, "id" | "name" | "email">
// Omit — exclude specific properties
type SafeUser = Omit<User, "password">
// Readonly — makes all properties read-only
type ImmutableUser = Readonly<User>
// Record — creates an object type with specific keys/values
type UserMap = Record<string, User>
These save you from writing redundant type definitions. Learn them before you start copy-pasting type definitions.
Discriminated Unions
One of TypeScript's most powerful patterns for modeling state:
type LoadingState = { status: "loading" }
type SuccessState = { status: "success"; data: User[] }
type ErrorState = { status: "error"; error: string }
type AppState = LoadingState | SuccessState | ErrorState
function render(state: AppState) {
switch (state.status) {
case "loading":
return "Loading..."
case "success":
return state.data.map(u => u.name) // TypeScript knows: data exists
case "error":
return `Error: ${state.error}` // TypeScript knows: error exists
}
}
This pattern eliminates runtime errors from accessing properties that don't exist on the current variant.
Type Assertions and unknown
// Type assertion — you tell TypeScript what the type is
const input = document.getElementById("username") as HTMLInputElement
const value = input.value
// unknown — safer than any
function processInput(value: unknown) {
if (typeof value === "string") {
return value.toUpperCase() // safe — TypeScript confirmed it's a string
}
throw new Error("Expected string")
}
Rule: prefer unknown over any. any disables type checking entirely. unknown forces you to narrow before using.
Stage 2 Milestone: You can type React components, API responses, and utility functions. You're using generics for reusable code.
Stage 3: Advanced TypeScript (Week 5-8)
Conditional Types
type IsArray<T> = T extends any[] ? true : false
type A = IsArray<number[]> // true
type B = IsArray<string> // false
More practically:
type UnpackPromise<T> = T extends Promise<infer U> ? U : T
type Resolved = UnpackPromise<Promise<string>> // string
type NotPromise = UnpackPromise<number> // number
Template Literal Types
type EventName = "click" | "focus" | "blur"
type EventHandler = `on${Capitalize<EventName>}`
// "onClick" | "onFocus" | "onBlur"
type CSSProperty = `margin-${"top" | "bottom" | "left" | "right"}`
// "margin-top" | "margin-bottom" | "margin-left" | "margin-right"
Mapped Types
// Make all properties of T optional and nullable
type NullablePartial<T> = {
[K in keyof T]?: T[K] | null
}
// Create a validation schema from a type
type ValidationSchema<T> = {
[K in keyof T]: (value: T[K]) => boolean
}
Declaration Files
When using JavaScript libraries without TypeScript types, you may need to write .d.ts declaration files:
// types/my-library.d.ts
declare module "my-library" {
export function doThing(input: string): Promise<{ result: string }>
export const VERSION: string
}
Most major libraries now ship with TypeScript types or have community definitions at @types/library-name.
Stage 3 Milestone: You're comfortable reading TypeScript error messages and resolving type-level issues. You can write type utilities.
TypeScript with Frameworks
React + TypeScript
// Component props
interface ButtonProps {
label: string
onClick: () => void
variant?: "primary" | "secondary"
disabled?: boolean
}
const Button: React.FC<ButtonProps> = ({ label, onClick, variant = "primary", disabled = false }) => {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
)
}
// useState with types
const [user, setUser] = React.useState<User | null>(null)
// useRef with types
const inputRef = React.useRef<HTMLInputElement>(null)
// Event handlers
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value)
}
Node.js + TypeScript
npm install --save-dev typescript @types/node ts-node
// server.ts
import http from "node:http"
const server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
res.writeHead(200, { "Content-Type": "application/json" })
res.end(JSON.stringify({ status: "ok" }))
})
server.listen(3000, () => {
console.log("Server running on port 3000")
})
Recommended Learning Resources
Official:
- TypeScript Handbook — the authoritative reference, readable top-to-bottom
- TypeScript Playground — run TypeScript in browser, share code snippets
Interactive:
- Total TypeScript — Matt Pocock's free exercises and workshops, best practical curriculum
- Type Challenges — GitHub repo with progressively harder type-level challenges
Video:
- No Bs TypeScript series by Jack Herrington — concise, practical, skips the fluff
- TypeScript Full Course by Traversy Media — good for beginners
Common TypeScript Mistakes to Avoid
Overusing any:
// Bad — defeats the purpose
function process(data: any): any { ... }
// Good — use unknown and narrow
function process(data: unknown): string { ... }
Ignoring strictNullChecks:
Make sure "strict": true or "strictNullChecks": true is in your tsconfig.json. Without it, null and undefined are assignable to every type, and you lose half the safety guarantees.
Type assertions without validation:
// Risky — you're asserting without checking
const user = apiResponse as User
// Better — validate the shape first
function isUser(data: unknown): data is User {
return typeof data === "object" && data !== null && "id" in data && "name" in data
}
if (isUser(apiResponse)) {
// TypeScript knows it's User here
}
Writing types when inference works fine:
// Unnecessary — TypeScript infers this
const count: number = 0
const name: string = "Alice"
// Only annotate when inference can't determine the type
let result: User | null = null // needed because initial value is null
Practical Project to Cement Learning
The fastest way to learn TypeScript is to migrate a small JavaScript project to TypeScript. Process:
- Rename
.jsfiles to.ts(.jsxto.tsxfor React) - Fix errors one by one — don't use
anyto silence them - Add
"strict": truetotsconfig.jsonand fix the additional errors - Replace
anytypes with proper interfaces - Add utility types where you're copy-pasting type definitions
A 500-line JavaScript project takes 2-4 hours to migrate properly and teaches more than a month of tutorials.
The goal isn't zero TypeScript errors forever — it's building the habit of thinking about types as you write, not after.
Level Up Your Dev Workflow
Found this useful? Explore DevPlaybook — cheat sheets, tool comparisons, and hands-on guides for modern developers.
🛒 Get the DevToolkit Starter Kit on Gumroad — 40+ browser-based dev tools, source code + deployment guide included.
Top comments (0)