What TypeScript Adds Over JavaScript
TypeScript is a strict superset of JavaScript that adds a static type system on top of the language you already know. Every valid .js file is a valid .ts file, but TypeScript lets you annotate values with types that are checked at compile time. The types are erased during compilation, so the JavaScript you ship at runtime is unchanged.
The payoff: bugs caught before you run code, editor autocomplete and refactoring, self-documenting function signatures, and confidence when changing large codebases. TypeScript does not add runtime type checking on its own; if you need to validate untrusted data at runtime, reach for a schema library like Zod.
// JavaScript: silent bug at runtime
function greet(name) { return "Hi " + name.toUppercase(); } // typo!
// TypeScript: error before you run
function greet(name: string): string {
return "Hi " + name.toUpperCase(); // toUppercase would be flagged
}
Basic Types
The core primitives mirror JavaScript: string, number, boolean, plus bigint, symbol, null, and undefined.
let title: string = "TS";
let count: number = 42;
let active: boolean = true;
// Arrays: two equivalent syntaxes
let ids: number[] = [1, 2, 3];
let names: Array<string> = ["a", "b"];
// Tuple: fixed-length, ordered types
let pair: [string, number] = ["age", 30];
let rgb: [number, number, number] = [255, 0, 0];
// Enum: named set of constants
enum Direction { Up, Down, Left, Right }
let d: Direction = Direction.Up; // 0
// Prefer union of literals over enum in many cases:
type Dir = "up" | "down" | "left" | "right";
The special types deserve care. any opts out of type checking entirely (avoid it). unknown is the type-safe counterpart: you must narrow it before use. never represents values that never occur (a function that always throws, or an exhausted union). void is the absence of a return value.
let a: any = 5; a.foo.bar; // allowed, unsafe
let u: unknown = 5; // u.foo; // error: must narrow first
if (typeof u === "number") u.toFixed(2); // ok after narrowing
function fail(msg: string): never { throw new Error(msg); }
function log(msg: string): void { console.log(msg); }
| Aspect | any | unknown |
|---|---|---|
| Assign anything to it | Yes | Yes |
| Use without narrowing | Yes (unsafe) | No (must narrow) |
| Assign it to other types | Yes | Only to any/unknown |
| Recommended | Avoid | Prefer for untyped input |
Type Inference
You rarely need to annotate everything. TypeScript infers types from initializers, return statements, and context. Idiomatic TS annotates function parameters and public API boundaries but lets local variables and return types be inferred.
let x = 3; // inferred: number
const y = "hi"; // inferred: "hi" (literal type via const)
const nums = [1, 2, 3]; // inferred: number[]
function double(n: number) { return n * 2; } // return inferred as number
Interfaces vs Type Aliases
Both describe the shape of an object. interface supports declaration merging and is idiomatic for object/class contracts. type aliases can name anything — unions, tuples, primitives, mapped types — and are more flexible.
interface User {
id: number;
name: string;
email?: string; // optional
readonly createdAt: Date; // cannot be reassigned
}
interface Admin extends User { role: "admin"; } // extension
type Point = { x: number; y: number };
type ID = string | number; // only type can do this
type Handler = (e: Event) => void;
Rule of thumb
Use interface for object shapes and public APIs that may be extended; use type for unions, intersections, and anything not a plain object shape.
Union & Intersection Types
A union (|) means "one of these". An intersection (&) combines multiple types into one that has all their members.
type Status = "loading" | "success" | "error"; // union of literals
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged; // must have both name and age
const p: Person = { name: "Ada", age: 36 };
Literal Types
A literal type is an exact value: "GET" or 200. Combined with unions they model precise sets. Use as const to freeze an object into deeply readonly literal types.
type Method = "GET" | "POST" | "PUT" | "DELETE";
function request(url: string, method: Method) { /* ... */ }
const config = { retries: 3, mode: "fast" } as const;
// config.mode is "fast" (not string), config is readonly
Generics
Generics let you write reusable code that works over many types while preserving type information. A type parameter (conventionally T) is a placeholder filled in when the function or class is used.
// Generic function
function identity<T>(value: T): T { return value; }
const n = identity<number>(5); // T = number
const s = identity("hi"); // T inferred as string
// Generic function over arrays
function first<T>(arr: T[]): T | undefined { return arr[0]; }
// Generic class
class Box<T> {
constructor(private value: T) {}
get(): T { return this.value; }
}
const b = new Box<string>("hello");
Constraints with extends
Use extends to require that a type parameter has certain members, so you can safely use them inside.
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest([1, 2], [1, 2, 3]); // ok, arrays have length
longest("ab", "abc"); // ok, strings have length
// Constrain a key to actual keys of an object
function prop<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
Type Narrowing
Narrowing is how TypeScript refines a broad type (like a union) to something more specific within a branch of code. The compiler follows your control flow.
typeof, instanceof, and in
function format(x: string | number) {
if (typeof x === "string") return x.trim(); // x: string here
return x.toFixed(2); // x: number here
}
class Dog { bark() {} }
class Cat { meow() {} }
function speak(a: Dog | Cat) {
if (a instanceof Dog) a.bark(); else a.meow();
}
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(pet: Fish | Bird) {
if ("swim" in pet) pet.swim(); else pet.fly();
}
Discriminated Unions
Give each union member a shared literal discriminant field. TypeScript narrows on it, and a never check in the default case enforces exhaustiveness.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2;
case "square": return s.side ** 2;
default:
const _exhaustive: never = s; // errors if a case is missed
return _exhaustive;
}
}
Type Guards & Assertion Functions
// User-defined type guard: return type is "x is T"
function isString(x: unknown): x is string {
return typeof x === "string";
}
// Assertion function: narrows by throwing
function assertDefined<T>(v: T | null | undefined): asserts v is T {
if (v == null) throw new Error("Expected value");
}
Utility Types
TypeScript ships built-in generic types that transform other types. These save enormous boilerplate.
| Utility | Effect |
|---|---|
Partial<T> | Makes all properties optional |
Required<T> | Makes all properties required |
Readonly<T> | Makes all properties readonly |
Pick<T, K> | Keeps only keys K |
Omit<T, K> | Removes keys K |
Record<K, V> | Object with keys K and values V |
ReturnType<F> | Extracts a function's return type |
Parameters<F> | Tuple of a function's parameter types |
Awaited<T> | Unwraps a Promise type |
interface User { id: number; name: string; email: string; }
type Draft = Partial<User>; // all optional
type Public = Omit<User, "email">; // { id, name }
type Summary = Pick<User, "id" | "name">; // { id, name }
type Frozen = Readonly<User>;
type ById = Record<number, User>; // { [k: number]: User }
function getUser() { return { id: 1, name: "Ada" }; }
type Got = ReturnType<typeof getUser>; // { id: number; name: string }
type P = Awaited<Promise<string>>; // string
keyof, typeof & Indexed Access
keyof T yields a union of a type's keys. typeof value lifts a runtime value into its type. Indexed access T[K] looks up a property type.
interface User { id: number; name: string; }
type UserKeys = keyof User; // "id" | "name"
type NameType = User["name"]; // string
const settings = { dark: true, fontSize: 14 };
type Settings = typeof settings; // { dark: boolean; fontSize: number }
type Values = Settings[keyof Settings]; // boolean | number
Mapped Types
Mapped types build new types by iterating over the keys of another type. Modifiers ? and readonly can be added or removed with +/-, and keys can be remapped with as.
type Optional<T> = { [K in keyof T]?: T[K] }; // like Partial
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // strip readonly
// Key remapping with template literal
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string }>; // { getName: () => string }
Conditional Types & infer
Conditional types choose between two types based on a relationship: T extends U ? X : Y. The infer keyword captures a type from within a match.
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<number>; // false
// infer extracts the element type of an array
type ElementOf<T> = T extends (infer U)[] ? U : never;
type E = ElementOf<string[]>; // string
// A hand-rolled ReturnType
type MyReturn<F> = F extends (...args: any[]) => infer R ? R : never;
Template Literal Types
String literal types can be composed like template strings, producing unions of concrete strings — great for typed event names, routes, and CSS units.
type Color = "red" | "green";
type Shade = "light" | "dark";
type Variant = `${Shade}-${Color}`;
// "light-red" | "light-green" | "dark-red" | "dark-green"
type EventName<T extends string> = `on${Capitalize<T>}`;
type Click = EventName<"click">; // "onClick"
Classes & Access Modifiers
TypeScript classes add public (default), private, and protected modifiers, parameter properties, readonly, abstract classes, and can implements interfaces. Modern TS also supports native ECMAScript #private fields.
interface Speaker { speak(): string; }
abstract class Animal implements Speaker {
// parameter property: declares + assigns in one line
constructor(protected readonly name: string) {}
abstract speak(): string;
}
class Dog extends Animal {
#tricks = 0; // truly private (runtime enforced)
speak(): string { return `${this.name} says woof`; }
}
Modules & Declaration Files
TypeScript uses standard ES module import/export. Use import type for type-only imports so bundlers can drop them. Declaration files (.d.ts) describe the types of plain JavaScript libraries; community types live under @types/*.
// user.ts
export interface User { id: number; }
export function makeUser(): User { return { id: 1 }; }
// app.ts
import { makeUser } from "./user";
import type { User } from "./user"; // erased at build time
// globals.d.ts
declare global {
interface Window { myApp: { version: string }; }
}
export {};
tsconfig Strict Options
The compiler is configured with tsconfig.json. Turning on "strict": true enables a bundle of safety flags and is strongly recommended for new projects.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true, // enables the flags below
"noImplicitAny": true,
"strictNullChecks": true, // null/undefined are explicit
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true // let a bundler emit JS
}
}
Why strict matters
strictNullChecks alone eliminates a huge class of "cannot read properties of undefined" runtime crashes by forcing you to handle null and undefined explicitly.
Tooling
The reference compiler is tsc. For running .ts files directly, tsx is the fast modern choice (an alternative to the older ts-node). Recent Node.js and Deno/Bun can strip TS types natively. Lint with typescript-eslint.
# Type-check the whole project (no output)
npx tsc --noEmit
# Watch mode
npx tsc --watch
# Run a TypeScript file directly
npx tsx src/index.ts
# Lint
npx eslint . --ext .ts,.tsx
Practice Exercises
- Model an API result as a discriminated union
Result<T>with{ ok: true; data: T }and{ ok: false; error: string }, then write a handler that narrows onok. - Write a generic function
pluck<T, K extends keyof T>(items: T[], key: K): T[K][]that extracts one property from every element in an array. - Implement a user-defined type guard
isUser(x: unknown): x is Userthat safely validates an unknown value fromJSON.parse. - Build a mapped type
DeepReadonly<T>that recursively marks every nested property asreadonly. - Use a conditional type with
inferto writeUnwrap<T>that returns the inner type of aPromise<T>or the type itself if not a promise. - Enable
"strict": trueon an existing JavaScript file renamed to.ts, then fix every error using proper narrowing instead ofanycasts.