Guide · TypeScript · JavaScript
TypeScript Crash Course for JavaScript Developers
Everything a JavaScript developer needs to start writing TypeScript confidently — types, interfaces, generics, enums, and practical patterns used in real Next.js and Node.js projects.
Jatinder Sandhu
Why Bother with TypeScript
If you already know JavaScript, TypeScript is not a new language to learn from scratch — it is JavaScript with a type layer bolted on top. Every valid JavaScript file is already valid TypeScript. What changes is that the compiler now checks your code before it runs, catching a whole category of bugs — wrong argument types, undefined properties, typos in object keys — at write time instead of in production.
On real client projects, I have seen this pay off most on medium and large codebases: refactors that would silently break three files in plain JavaScript get flagged immediately by the compiler in TypeScript. Editor autocomplete also becomes dramatically more useful because the IDE actually knows the shape of your data.
This guide is written for developers who already write JavaScript comfortably and want a fast, practical path into TypeScript — not a full language reference, just the parts you will actually use day to day in a Next.js or Node.js project.
The Basic Types You Will Use Constantly
Most of TypeScript in daily use comes down to a small set of building blocks. Here is the core set, with how each one behaves in practice:
| Type | Example | Notes |
|---|---|---|
| string | let name: string = 'Jatinder' | Text values, template literals included |
| number | let age: number = 31 | No separate int/float — just number |
| boolean | let active: boolean = true | Standard true/false |
| array | let tags: string[] = ['dev'] | Or Array<string> — same thing |
| unknown | let data: unknown | Safer than any — forces a type check before use |
| void | function log(): void | Function returns nothing useful |
One habit worth building immediately: avoid any. It quietly turns off type checking for that value, which defeats the entire purpose of using TypeScript. Reach for unknown instead when you genuinely do not know the shape of the data yet — it forces you to narrow the type before doing anything with it.
Interfaces vs Type Aliases
This is the question every JavaScript developer asks first, and the honest answer is: for most day-to-day object shapes, it does not matter much. Both describe the shape of data. The practical differences show up at the edges:
interface
Best for object shapes that might be extended later — components, API response models, class contracts. Interfaces can be reopened and merged, which is useful in libraries but rare in app code.
type
Best for unions, tuples, and anything that is not a plain object shape — for example a type that is either a string or a specific set of literals. Type aliases are also the only option for mapped and conditional types.
My working rule on client projects: use interface for props and API models, use type for unions and utility compositions. Consistency across the codebase matters more than which one you pick.
Typed Functions and a Real Use for Generics
Typing function parameters and return values is where TypeScript starts paying off immediately — the compiler will stop you from calling a function with the wrong argument shape before you ever run the code.
// Basic typed function
function calculateTotal(price: number, quantity: number): number {
return price * quantity
}
// Generic function — works with any type, stays type-safe
function getFirstItem<T>(items: T[]): T | undefined {
return items[0]
}
const firstUser = getFirstItem<User>(users) // typed as User | undefined
Generics look intimidating at first, but the mental model is simple: <T> is a placeholder for “whatever type gets passed in”. Instead of writing separate functions for arrays of users, products, and orders, you write one generic function and TypeScript fills in T automatically based on what you call it with — while still keeping full type safety.
Union Types and Enums for Fixed Options
Whenever a value can only be one of a small, known set of options — a status field, a user role, a payment state — resist the urge to type it as a plain string. Constrain it instead:
// Union type — lightweight, no runtime code generated
type OrderStatus = 'pending' | 'confirmed' | 'shipped' | 'delivered'
// Enum — has a runtime value, useful when you need to loop over options
enum UserRole {
Admin = 'admin',
Client = 'client',
Guest = 'guest',
}
In most modern Next.js and React codebases I recommend union string literal types over enums — they are lighter, do not generate extra runtime JavaScript, and work better with libraries that expect plain strings. Reach for an actual enum only when you need to iterate over the values at runtime or want namespaced constants.
Patterns You Will Actually Use in Next.js and Node.js Projects
Typing API responses with interfaces
Define an interface for every API response shape (e.g. User, Order, Product) and type your fetch or Server Action return values against it. This alone catches most of the “undefined is not an object” runtime errors before they reach production.
Typing React props with interfaces
Every component that accepts props should declare a Props interface above it. This gives autocomplete for every prop when the component is used elsewhere in the codebase, and immediately flags missing required props.
Using utility types instead of duplicating shapes
Partial<T>, Pick<T, K>, and Omit<T, K> let you derive new types from an existing one instead of copy-pasting a slightly different interface. For example, an update form usually needs Partial<User> since every field is optional during an edit.
Strict mode from day one
Turn on strict: true in tsconfig.json at project start, not later. Retrofitting strict mode onto a large codebase is painful — enforcing it from the first commit keeps the type coverage honest as the project grows.
Common Mistakes JavaScript Developers Make Early On
- Typing everything as any “just to make the error go away” — this silences the compiler instead of fixing the underlying type mismatch, and the bug just resurfaces at runtime later.
- Not enabling strict mode — without it, TypeScript quietly allows null and undefined in places that will crash your app, defeating most of the safety benefit.
- Over-engineering generics on day one — start with concrete types, and only reach for a generic once you notice yourself writing the same function twice for different types.
- Ignoring compiler warnings in the editor and only checking at build time — the whole value of TypeScript is catching mistakes while you type, not after a failed deploy.
Conclusion
TypeScript is not a separate skill from JavaScript — it is JavaScript with guardrails. Start small: type your function signatures, your component props, and your API responses first. Generics, mapped types, and the more advanced utility types can wait until you actually hit a problem they solve. In every Next.js and Node.js project I ship for clients, this incremental approach is what actually gets a team to adopt TypeScript instead of fighting it.
If you are migrating an existing JavaScript codebase or starting a new Next.js or Node.js project and want it built with clean, properly typed TypeScript from day one, that is exactly the kind of work I take on for clients.
Need a TypeScript-first Next.js or Node.js build?
I build production Next.js, Node.js, and API projects in strict TypeScript. Get in touch to discuss your project.
Get in Touch →