March 27, 20267 min read

TypeScript Complete Guide: From JavaScript Developer to TypeScript Pro

Everything you need to go from JavaScript to TypeScript — setup, core types, generics, utility types, discriminated unions, and real-world patterns. No fluff, just practical knowledge.

typescript javascript guide web-development types
Ad 336x280

TypeScript won. That's not hype — it's just what happened. Every major framework (React, Angular, Vue, Svelte, Next.js) either uses TypeScript internally or has first-class TypeScript support. It surpassed Python in new GitHub repositories in 2025. If you write JavaScript professionally, TypeScript isn't optional anymore.

But here's the thing — most TypeScript tutorials either drown you in academic type theory or barely scratch the surface. This guide is the middle path. Practical TypeScript, progressively, with real code.

Setup: Get Strict From Day One

npx tsc --init

This generates a tsconfig.json. The most important setting:

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "outDir": "./dist",
    "rootDir": "./src",
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}
Always enable strict: true from the start. People who start without strict mode and try to enable it later end up with 400 errors across the codebase and give up. Strict from day one means you learn the right habits immediately.

Basic Types — Let TypeScript Do the Work

// Explicit types
let name: string = "Alice";
let age: number = 30;
let active: boolean = true;

// But TypeScript infers types — don't over-annotate
let name = "Alice"; // TS knows this is string
let age = 30; // TS knows this is number
let active = true; // TS knows this is boolean

// Arrays
let scores: number[] = [95, 87, 92];
let names: string[] = ["Alice", "Bob"];

// Objects
let user: { name: string; age: number; email?: string } = {
name: "Alice",
age: 30
};

The rule of thumb: annotate function parameters and return types. Let inference handle the rest. Over-annotating makes code noisy without adding safety.

Interfaces vs Types — The Real Difference

// Interface — best for object shapes and classes
interface User {
  id: number;
  name: string;
  email: string;
}

// Interfaces can be extended
interface AdminUser extends User {
permissions: string[];
}

// Type — best for unions, intersections, and computed types
type Status = "active" | "inactive" | "suspended";
type ApiResponse<T> = { data: T; error: null } | { data: null; error: string };

// Types can do things interfaces can't
type UserKeys = keyof User; // "id" | "name" | "email"

In my experience, the debate doesn't matter much. Use interfaces for object shapes you might extend. Use types for everything else. Most codebases pick one and stick with it — consistency beats correctness here.

Generics — The "Aha" Moment

Generics let you write code that works with any type while keeping type safety. Once they click, you'll use them everywhere.

// Without generics — you lose type information
function getFirst(arr: any[]): any {
  return arr[0];
}

// With generics — type flows through
function getFirst<T>(arr: T[]): T {
return arr[0];
}

const num = getFirst([1, 2, 3]); // TypeScript knows: number
const str = getFirst(["a", "b", "c"]); // TypeScript knows: string

Real-world example — a typed API fetch:

async function fetchApi<T>(url: string): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(HTTP ${res.status});
  return res.json() as Promise<T>;
}

interface User { id: number; name: string; }

const user = await fetchApi<User>("/api/users/1");
// user.name is typed as string — autocomplete works

Generics aren't fancy. They're just "I don't know the type yet, but it'll be consistent." That's it.

Utility Types — Don't Reinvent the Wheel

TypeScript ships with built-in types that transform other types. These save enormous amounts of boilerplate.

interface User {
  id: number;
  name: string;
  email: string;
  avatar: string;
}

// Partial — all properties become optional
type UpdateUser = Partial<User>;
// { id?: number; name?: string; email?: string; avatar?: string }

// Pick — select specific properties
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }

// Omit — remove specific properties
type CreateUser = Omit<User, "id">;
// { name: string; email: string; avatar: string }

// Record — typed key-value maps
type RolePermissions = Record<string, string[]>;
// { [key: string]: string[] }

// Required — make all optional properties required
type CompleteUser = Required<Partial<User>>;

In my experience, Partial, Pick, and Omit cover 90% of real-world use cases. Learn those three well before worrying about the rest.

Discriminated Unions — The Most Powerful Pattern

This is, hands down, the most useful TypeScript pattern for modeling real application state. It eliminates entire categories of bugs.

// Model API response states explicitly
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; // TS knows data exists here
case "error":
return state.error; // TS knows error exists here
}
}

The status field is the "discriminant." TypeScript narrows the type in each branch automatically. You literally cannot access data in the error state — the compiler won't let you. This replaces dozens of defensive if checks and null guards.

Type Narrowing and Guards

TypeScript narrows types when you check them:

function process(input: string | number) {
  if (typeof input === "string") {
    return input.toUpperCase();  // TS knows it's string
  }
  return input.toFixed(2);      // TS knows it's number
}

// Custom type guard
function isUser(obj: unknown): obj is User {
return typeof obj === "object" && obj !== null && "name" in obj;
}

const data: unknown = await fetchSomething();
if (isUser(data)) {
console.log(data.name); // Safe — TS knows it's User
}

Working with APIs — Zod for Runtime Validation

Here's the thing — TypeScript types disappear at runtime. They don't validate incoming data. For API responses, you need runtime validation. Zod gives you both.

import { z } from "zod";

const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
role: z.enum(["admin", "user", "guest"]),
});

// Infer the TypeScript type FROM the schema
type User = z.infer<typeof UserSchema>;

// Now validate at runtime AND get type safety
const response = await fetch("/api/user/1");
const user = UserSchema.parse(await response.json());
// user is fully typed AND validated — if the API returns garbage, it throws

This is the modern pattern. Define your schema once, get both runtime validation and compile-time types. No duplication.

TypeScript with React

// Props typing
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary";
  disabled?: boolean;
}

function Button({ label, onClick, variant = "primary", disabled }: ButtonProps) {
return <button onClick={onClick} disabled={disabled}>{label}</button>;
}

// Hooks are typed automatically
const [count, setCount] = useState(0); // number
const [user, setUser] = useState<User | null>(null); // explicit when TS can't infer

// Event handlers
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}

Common Mistakes

Using any to shut up the compiler. Every any is a hole in your type safety. Use unknown instead — it forces you to narrow the type before using it. Over-typing everything. If TypeScript can infer it, let it. const x: number = 5 is just noise. Not using strict mode. Non-strict TypeScript is barely better than JavaScript. You're paying the syntax cost without getting the safety. Ignoring discriminated unions. If you're writing status: string and checking if (status === "loading") with no type narrowing, you're missing TypeScript's best feature.

Where to Go From Here

TypeScript's type system is deep — you can model almost anything. But you don't need to learn it all at once. Start with basic types, interfaces, and generics. Add utility types and discriminated unions as your codebase grows. The advanced stuff (conditional types, mapped types, template literal types) is for library authors, not day-to-day application code.

Practice with real projects on CodeUp — writing TypeScript in an interactive environment where you see compiler errors immediately is the fastest way to build the muscle memory. The type system makes more sense when you're hitting real errors than when you're reading about them.

Ad 728x90