From JavaScript to TypeScript
TypeScript is a superset of JavaScript that adds an optional static type system on top of the language you already know. You write types to describe the shape of your data, and the TypeScript compiler checks them before your code ever runs — catching typos, wrong arguments, and undefined access at edit time instead of in production. The best part is that every valid JavaScript file is already valid TypeScript, so you can adopt it gradually rather than rewriting everything at once.
Why add static types
In plain JavaScript, mistakes like calling a function with the wrong arguments or reading a property that does not exist only surface at runtime — often far from where the bug was introduced. TypeScript surfaces those errors immediately, right in your editor, with red squiggles and precise messages. It also powers rich autocomplete, safe refactoring (rename a property everywhere at once), and self-documenting code: the types are the documentation.
The trade-off is a compile step and some upfront effort describing your data. For anything beyond a small script, that investment pays for itself quickly as the codebase and team grow.
Basic types
You annotate a variable, parameter, or return value with a colon followed by the type. TypeScript can also infer most types, so you rarely annotate everything.
// Primitives — often inferred, shown explicitly here
const title: string = "Docs";
const count: number = 42;
const ready: boolean = true;
// Arrays and unions
const tags: string[] = ["js", "ts"];
let id: number | string = 7; // can hold either type
// Function with typed params and return type
const greet = (name: string): string => `Hello, ${name}!`;
console.log(greet("Ada"));
Output:
Hello, Ada!
If you call greet(42), the compiler refuses to build and tells you exactly why — number is not assignable to string.
Interfaces and object shapes
Most real data is structured. Use an interface (or a type alias) to describe the shape of an object once, then reuse it everywhere.
interface User {
id: number;
name: string;
email?: string; // optional property
}
const formatUser = (user: User): string =>
`${user.name} (#${user.id})`;
const ada: User = { id: 1, name: "Ada Lovelace" };
console.log(formatUser(ada));
Here email? marks the property as optional. If you forget a required field or add an unknown one, TypeScript flags it. type aliases do nearly the same job and additionally support unions and intersections:
type Status = "active" | "paused" | "archived"; // string literal union
type Account = User & { status: Status }; // intersection
Gradual adoption
You do not have to convert a project all at once. TypeScript is designed to meet existing JavaScript where it is.
| Approach | What it does | When to use |
|---|---|---|
allowJs | Lets .js files live alongside .ts files in the build | Mixed codebases mid-migration |
checkJs | Type-checks plain .js files too | Catch bugs before renaming files |
| JSDoc types | Add types via comments, no syntax change | Libraries that must ship as .js |
Rename .js → .ts | Full TypeScript, file by file | Modules ready to fully convert |
With JSDoc, you get real type checking in a plain .js file — no compile step required if your tooling reads it:
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
const add = (a, b) => a + b;
Tip: Start by enabling
checkJson a few files with// @ts-checkat the top. You will discover real bugs before writing a single type annotation.
tsconfig basics
A tsconfig.json at the project root configures the compiler. A solid modern starting point:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src"]
}
The single most important option is "strict": true. It enables a family of checks — notably strictNullChecks, which forces you to handle null and undefined explicitly and eliminates a huge class of runtime errors. target controls which JavaScript version the compiler emits, and outDir is where the compiled .js lands.
The build step
TypeScript types are erased at compile time — browsers and Node run plain JavaScript. Install the compiler and run it:
npm install --save-dev typescript
npx tsc --init # generates a tsconfig.json
npx tsc # compiles src/ into dist/ per tsconfig
Add tsc --watch for incremental rebuilds during development. In practice, a bundler (Vite, esbuild) or a runtime that understands TypeScript directly (modern Node, Deno, Bun) often handles the transform for you, letting you skip the manual tsc step for app code — though running tsc --noEmit in CI for type-checking remains a great habit.
Warning: A bundler that strips types (esbuild, swc) does not check them. Keep a separate
tsc --noEmitstep so type errors actually fail your build.
Best Practices
- Turn on
"strict": truefrom day one — retrofitting it later is painful. - Prefer type inference; annotate function parameters, return types, and public APIs, but let TypeScript infer obvious locals.
- Migrate incrementally with
allowJs/checkJsrather than attempting a big-bang rewrite. - Avoid
any— reach forunknownand narrow it, or model the real shape with an interface. - Run
tsc --noEmitin CI so type errors block merges, even when a bundler does the actual transpiling. - Install community types (
@types/...) for libraries that do not ship their own.