TypeScript Introduction
If you write JavaScript professionally, you will meet TypeScript — Microsoft's typed superset of JavaScript that has become the default for serious projects. The pitch is simple: keep everything you know about JavaScript, add type annotations, and catch a huge class of bugs before the code ever runs.
This page introduces TypeScript from a JavaScript developer's perspective: what it adds, how to adopt it gradually, and when the investment pays off. For a deep dive, this site has a full TypeScript tutorial series — this page is your on-ramp.
What TypeScript adds
TypeScript is JavaScript — every valid JS file is valid TS. On top of it, TypeScript adds a static type system that analyzes your code without running it. Consider this classic JavaScript bug:
// JavaScript — runs, then explodes at runtime
function getInitials(user) {
return user.firstName[0] + user.lastName[0]
}
getInitials({ firstName: 'Ada' })
// TypeError: Cannot read properties of undefined (reading '0')The same code in TypeScript refuses to compile:
type User = {
firstName: string
lastName: string
}
function getInitials(user: User): string {
return user.firstName[0] + user.lastName[0]
}
getInitials({ firstName: 'Ada' })
// Error: Property 'lastName' is missing in type '{ firstName: string; }'
// but required in type 'User'.Errors at compile time, not runtime — typos, missing properties, wrong argument types are caught as you type
Self-documenting code — the signature tells you exactly what a function accepts and returns
Fearless refactoring — rename a property and the compiler lists every place that must change
Superhuman editor support — autocomplete, inline docs, and go-to-definition all come from types
Type annotations: the basics
// Primitives
let title: string = 'Hello'
let count: number = 42
let active: boolean = true
// Arrays and objects
let tags: string[] = ['js', 'ts']
let point: { x: number; y: number } = { x: 1, y: 2 }
// Functions — parameter and return types
function repeat(text: string, times: number): string {
return text.repeat(times)
}
// Union types — one of several
let id: string | number = 'usr_1'
id = 7 // also fine
// Optional properties and parameters
function greet(name: string, title?: string) {
return title ? `${title} ${name}` : name
}You annotate far less than you might fear, because TypeScript infers types from values:
let score = 10 // inferred: number score = 'high' // Error: Type 'string' is not assignable to type 'number' const langs = ['js', 'ts'] // inferred: string[] const upper = langs.map((l) => l.toUpperCase()) // l inferred as string
Interfaces and type aliases
Shapes of objects are described with interface or type. They're nearly interchangeable for object shapes; interfaces can be extended and merged, type aliases can also name unions and other combinations:
interface User {
id: number
name: string
email?: string // optional
readonly createdAt: Date // cannot be reassigned
}
interface Admin extends User {
permissions: string[]
}
// Type alias — same idea, plus unions
type Status = 'draft' | 'published' | 'archived'
function publish(user: Admin, status: Status) {
if (!user.permissions.includes('publish')) {
throw new Error(`${user.name} cannot publish`)
}
// ...
}That Status union is a taste of what makes TypeScript special: the compiler knows the only legal values are those three strings, autocompletes them, and rejects 'pubished' (typo) instantly.
Gradual adoption: you don't have to rewrite anything
TypeScript was designed for incremental migration. A realistic path for an existing JavaScript project:
Add a
tsconfig.jsonwithallowJs: true— TypeScript tooling now understands your.jsfiles without changing them.Turn on
checkJs: true— the compiler type-checks plain JavaScript using inference, flagging real bugs with zero annotations.Add JSDoc types where inference needs help — full type safety while the files stay
.js.Rename files to
.tsone at a time — start with leaf modules (utilities) and work inward.Ratchet up strictness — enable
strict: true(or flag by flag:noImplicitAny,strictNullChecks) as the codebase gets cleaner.
Step 3 deserves a demo — JSDoc comments give you TypeScript checking in plain .js files:
// utils.js — still plain JavaScript!
// @ts-check
/**
* @param {number} price
* @param {number} [taxRate]
* @returns {number}
*/
export function withTax(price, taxRate = 0.2) {
return price * (1 + taxRate)
}
withTax('9.99') // Error in your editor: Argument of type 'string'
// is not assignable to parameter of type 'number'.tsconfig.json basics
The tsconfig.json file controls the compiler. A solid starter for a modern project:
{
"compilerOptions": {
"target": "ES2022", // JS version to emit
"module": "ESNext", // module system in output
"moduleResolution": "bundler",
"strict": true, // all strict checks — always on for new code
"noEmit": true, // just type-check; a bundler emits the JS
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src"]
}Option | What it does |
|---|---|
| Enables the full strict family — the single most valuable flag |
| How far down to transpile syntax (ES2022, ES2020, ...) |
| Type-check only; let Vite/esbuild/SWC produce the JavaScript |
| Include and type-check plain |
| Error when a type silently falls back to |
| Makes |
any turns off checking for everything it touches — errors silently spread through your code. Prefer unknown when you genuinely do not know a type; it forces you to check before use.Compiling: tsc and Vite
Two common ways to run TypeScript:
# 1. The TypeScript compiler directly npm install --save-dev typescript npx tsc # compile per tsconfig.json npx tsc --noEmit # type-check only npx tsc --watch # re-check on every save # 2. Vite (most apps) — esbuild strips types instantly, # tsc checks types separately npm create vite@latest my-app -- --template vanilla-ts
Modern setups split the jobs: a fast tool (esbuild/SWC) strips types during dev and build, while tsc --noEmit runs in CI to actually check them. That's why a Vite project's build script is typically tsc --noEmit && vite build.
Common first errors (and what they mean)
const el = document.getElementById('app')
el.innerHTML = 'Hi' // Error: 'el' is possibly 'null'.
// Fix: check it first
if (el) el.innerHTML = 'Hi'
let user: { name: string }
console.log(user.name) // Error: Variable 'user' is used before being assigned.
function pick(key) {} // Error: Parameter 'key' implicitly has an 'any' type.
// Fix: function pick(key: string) {}
const config = JSON.parse(raw) // config is 'any' — annotate it:
const parsed: { port: number } = JSON.parse(raw)$ npx tsc --noEmit src/main.ts:2:1 - error TS2531: Object is possibly 'null'. src/main.ts:8:9 - error TS7006: Parameter 'key' implicitly has an 'any' type. Found 2 errors in the same file, starting at: src/main.ts:2
These errors feel pedantic at first — then you realize each one is a real runtime crash TypeScript just prevented. "Object is possibly null" is the compile-time version of JavaScript's most common production error.
When does TypeScript pay off?
Situation | Verdict |
|---|---|
Team of 2+ working on shared code | Strong yes — types are enforced communication |
Codebase you will maintain for years | Strong yes — refactoring safety compounds |
Library published for others | Yes — consumers get autocomplete and type docs |
Small script or throwaway prototype | Optional — JSDoc + |
Learning JavaScript fundamentals | Learn JS first; types are easier once the base is solid |
TypeScript vs JSDoc: JSDoc annotations with checkJs give you perhaps 80% of the safety with no build step — a great fit for small Node scripts and config files. Full TypeScript wins when you need advanced types (generics, unions, mapped types), a team-wide contract, or you're in an ecosystem (React, Angular, NestJS) where TS is the norm.
Where to go next
This page only scratches the surface — generics, narrowing, utility types like Partial and Pick, declaration files, and strict-mode patterns all await. This site has a dedicated TypeScript tutorial series that covers the language from setup to advanced types; head there when you're ready to go deep.