TypeScript adds a static type system to JavaScript. It catches errors early, documents your data, and makes large codebases easier to maintain.
Annotate parameters and return types; let TypeScript infer the rest.
function greet(name: string): string {return `Hello, ${name}`;}greet("Alice"); // OKgreet(123); // Type errorconst age = 25; // inferred as number
Types are erased at compile time, so they don’t validate API responses at runtime. Use a validator such as Zod for that.
type vs interfaceBoth describe object shapes:
interface User {id: number;name: string;email: string;nickname?: string; // optionalreadonly createdAt: Date; // can't be reassigned}
type can also express unions, intersections, and tuples:
type Status = "idle" | "loading" | "success" | "error";type UserWithTimestamps = User & { updatedAt: Date };
Use interface when you need declaration merging (e.g. extending library types), and type for composing types. A union of string literals is often simpler than an enum.
A union (A | B) is one of several types. An intersection (A & B) combines them.
Unions with a shared status field, called discriminated unions, are great for modeling state:
type ApiResult =| { status: "success"; data: User[] }| { status: "error"; message: string };function handleResult(result: ApiResult) {if (result.status === "success") {console.log(result.data); // data is available} else {console.error(result.message); // message is available}}
TypeScript narrows types using checks like typeof, instanceof, in, and equality:
function formatValue(value: string | number): string {if (typeof value === "string") {return value.toUpperCase(); // string}return value.toFixed(2); // number}
For custom checks, write a type guard with value is Type:
function isUser(value: unknown): value is User {return (typeof value === "object" &&value !== null &&"id" in value &&typeof value.id === "number");}
keyof and typeofDerive types from existing code instead of duplicating them:
const config = { theme: "dark", pageSize: 20 };type Config = typeof config; // { theme: string; pageSize: number }type ConfigKey = keyof typeof config; // "theme" | "pageSize"function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {return obj[key];}getProperty(config, "theme"); // stringgetProperty(config, "missing"); // Type error
Built-in helpers that transform existing types:
type UserUpdate = Partial<User>; // all fields optionaltype UserPreview = Pick<User, "id" | "name">; // only selected fieldstype NewUser = Omit<User, "id">; // remove fieldstype UsersById = Record<number, User>; // key → value map
Generics let functions work with many types while keeping type information, without falling back to any:
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {return items.find((item) => item.id === id);}
The constraint T extends { id: number } requires an id, while the full item type is preserved. Generics shine in API clients, custom hooks, and shared components.
satisfiessatisfies checks a value against a type without widening its inferred type:
type Theme = Record<string, string>;const colors = {primary: "#2563eb",danger: "#dc2626",} satisfies Theme;colors.primary; // OKcolors.missing; // Type error (with `: Theme` this would be allowed)
Useful for configs, themes, and route maps.
any, unknown, and neverany turns off type checking. Avoid it outside of migrations.unknown accepts anything, but you must narrow it before use. Prefer it for untrusted data.never is a value that can’t happen. Use it for exhaustive checks:type Status = "idle" | "loading";function getLabel(status: Status): string {switch (status) {case "idle":return "Idle";case "loading":return "Loading";default: {const unreachable: never = status; // errors if a case is missingreturn unreachable;}}}
Most packages ship their own types; others have them on DefinitelyTyped (npm i -D @types/node). If none exist, declare them yourself:
// types/legacy-lib.d.tsdeclare module "legacy-lib" {export function formatDate(date: Date, pattern: string): string;}