Last 30 Days
No notifications
TypeScript is a superset of JavaScript that adds optional static typing. It catches errors at compile time, improves IDE autocompletion, and serves as living documentation.
let name: string = "Alice";
let age: number = 25;
let active: boolean = true;
let data: null = null;
let items: string[] = ["a", "b", "c"];| Feature | Interface | Type Alias | |
| Object shapes | ✅ | ✅ | |
| Extends | extends keyword | Intersection & | |
| Declaration merging | ✅ | ❌ | |
| Unions | ❌ | ✅ A \ | B |
| Primitives/Tuples | ❌ | ✅ |
function identity<T>(value: T): T { return value; }
identity<string>("hello"); // T = string
identity(42); // T inferred as numberenum Status { Pending, Active, Closed }
let s: Status = Status.Active; // 1TypeScript narrows types using control flow:
| Technique | Example |
typeof | if (typeof x === "string") |
in | if ("name" in obj) |
instanceof | if (err instanceof Error) |
| Discriminated union | if (shape.kind === "circle") |
Partial, Required, Pick, Omit, Record, Readonly
TypeScript is JavaScript with a type checker bolted on. You write almost the same code, but you also describe the *shape* of your data — and the compiler catches mistakes before the code ever runs.
function double(n: number) { return n * 2; }
double("hi"); // ❌ caught at compile time, before users see a crashThe catch: TypeScript disappears at build time. The browser only sees plain JavaScript. Types are pure documentation enforced by the compiler.
let name: string = "Asha";
let age: number = 21;
let active: boolean = true;
let tags: string[] = ["js", "ts"];
let pair: [number, string] = [1, "x"]; // tuple — fixed length & types
let nothing: null = null;
let unknownValue: unknown; // safer than any
let anything: any; // ⚠ disables type checkingTip: TypeScript can usually infer types — write let age = 21 and it already knows it's a number. Annotate function parameters and return types; let inference do the rest.
type vs interfaceBoth describe the shape of an object. Either is fine; pick one and be consistent.
type User = {
id: number;
name: string;
email?: string; // optional
readonly createdAt: Date;
};interface User {
id: number;
name: string;
email?: string;
}
Tiny differences:
interface can be extended: interface Admin extends User { ... }.type can describe anything: unions, intersections, primitives, mapped types.interface declarations with the same name merge; type aliases don't.interface for object shapes you might extend, type for unions and utility types.function add(a: number, b: number): number {
return a + b;
}const greet = (name: string): string => Hi, ${name};
// optional + default params
function log(msg: string, level: "info"
"warn" = "info") { /* … */ }Union & Literal Types
A union means "this OR that":
let id: string
number; // either is allowed
type Status = "idle" "loading" "success"
"error"; // string literal unionString literal unions are amazing for prop options — you get autocomplete for free.
const nums: number[] = [1, 2, 3];
const ages: Array<number> = [21, 22]; // identical
const map: Record<string, number> = { a: 1 }; // object as a dictionaryfunction first<T>(arr: T[]): T | undefined {
return arr[0];
}
first<string>(["a", "b"]); // T is string
first([1, 2, 3]); // inferred as number
is a generic — a placeholder for any type. It lets you write a function or component that works with many types while keeping safety.
type ButtonProps = {
label: string;
onClick: () => void;
variant?: "primary" | "ghost";
};function Button({ label, onClick, variant = "primary" }: ButtonProps) {
return <button onClick={onClick} className={variant}>{label}</button>;
}
const [count, setCount] = useState<number>(0);
const inputRef = useRef<HTMLInputElement>(null);
For event handlers:
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}1. Reaching for any to silence errors — defeats the entire point. Use unknown instead and narrow it.
2. Annotating everything — TS infers most types. Annotate boundaries (function parameters, returns, exports), not internal variables.
3. Mixing null and undefined randomly — pick a convention. Most code-bases prefer undefined for "missing".
4. Type assertion as everywhere — that's "trust me bro, no runtime check". Reach for type guards instead.
5. Forgetting strict: true in tsconfig.json — without it, half the safety is off.
TypeScript uses structural typing ("if it walks like a duck"). Two types are compatible if their *shapes* match — names don't matter:
interface Point { x: number; y: number }
const p = { x: 1, y: 2, z: 3 }; // extra property is fine
const point: Point = p; // ✅ validThis is why you can pass an object literal that "happens to fit". Watch out for the excess property check: passing a literal *directly* into a typed slot will flag extras.
Inside a guard, TS narrows the type for you:
function show(x: string | number) {
if (typeof x === "string") {
x.toUpperCase(); // x is string here
} else {
x.toFixed(2); // x is number here
}
}// instanceof
if (err instanceof Error) err.message;
// in operator
if ("email" in user) user.email;
// custom type guard (predicate)
function isUser(x: unknown): x is User {
return typeof x === "object" && x !== null && "id" in x;
}
Narrowing is the way you make unknown useful — never reach for as first.
Combine a union with a shared literal field for safe pattern matching:
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;
}
}
This pattern is *the* idiomatic way to model "one of N variants" — Redux actions, API responses, drawing primitives.
TypeScript ships dozens of helpers that transform types. Daily-use ones:
type User = { id: number; name: string; email: string };Partial<User> // all fields optional
Required<User> // all fields required
Readonly<User> // all fields readonly
Pick<User, "id" | "name"> // subset of fields
Omit<User, "email"> // every field except…
Record<"a" | "b", number> // { a: number; b: number }
ReturnType<typeof fn> // the type fn returns
Awaited<Promise<string>> // string
Compose them: Partial for an "edit user" form.
function getLength<T extends { length: number }>(item: T): number {
return item.length; // safe — T must have a .length
}getLength("hi"); // ✅
getLength([1, 2]); // ✅
getLength(42); // ❌ number has no length
Constrain generics whenever you need to use a property of T inside.
unknown vs anyany opts out of type checking — never use it without a really good reason. unknown opts in — you must narrow before using it. Always prefer unknown for parsed JSON, third-party data, etc.
const data: unknown = JSON.parse(raw);
if (typeof data === "object" && data && "id" in data) {
// safe to use data.id now
}// Conditional: if X extends Y, do A, else B
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<5>; // false// Mapped: transform every key
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
// keyof + indexed access
type Keys = keyof User; // "id"
"name"
"email"
type Email = User["email"]; // stringThese power the entire utility-type ecosystem and library typings.
Type-level string manipulation:
type Color = "red" | "blue";
type Variant = bg-${Color}-500; // "bg-red-500" | "bg-blue-500"Used heavily by libraries like Tailwind plugins and CSS-in-TS.
satisfies — The Best of Both WorldsWhen you want TS to *check* your literal matches a type, but keep the narrow inferred type:
const config = {
retries: 3,
mode: "fast",
} satisfies { retries: number; mode: "fast" "slow" };config.mode; // type is "fast", not "fast"
"slow"nevernever means "this can't happen". Use it to prove a switch covers every case:
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; // compile error if a Shape variant is missed
return _exhaustive;
}
}Add a new variant to Shape later → TS immediately points at the unhandled case.
1. Type a small library: a User interface, an updateUser(id: number, patch: Partial function, and a discriminated-union Result for success/error.
2. Convert a JS file to TS without using any. Use unknown + narrowing for anything imported from JSON.
3. Build a generic useFetch hook returning { data: T .
4. Add an exhaustive
null; loading: boolean }null; error: Error switch over a string-literal union and force a missed-case compile error using never.