TypeScript
TypeScript from the ground up: why static types pay off, primitives, interfaces vs type aliases, unions and intersections, generics, enums, type narrowing, the everyday utility types (Partial/Pick/Omit/Record), typing functions and React props, structural typing, and the tsconfig strictness that makes it all worthwhile — beginner to advanced in one page.
Why types at all? TypeScript is JavaScript plus a static type layer that is checked by the compiler and then erased — the browser runs plain JavaScript with no types left. The payoff is catching whole classes of bugs (typos, wrong arguments, null access, refactors that miss a call site) in your editor, seconds after typing, instead of in production. You also get world-class autocomplete and safe rename-across-project. For a Java developer the deal is familiar: trade a little ceremony for compile-time guarantees and tooling. In this program TypeScript is mandatory.
Primitives and inference. The core types are string, number, boolean, null, undefined, plus bigint and symbol. Arrays are number[] (or Array<number>); fixed-length tuples are [string, number]; a value can be object or a precise shape. You rarely annotate everything — TypeScript infers types from initialisers, so let n = 3 is already number. Annotate the boundaries (function parameters, exported APIs, empty containers); let inference handle the middle.
Interfaces describe object shapes. An interface is a contract for the shape of data — like a Java interface or POJO. Fields can be optional with ?, readonly, or index signatures ([key: string]: number). Interfaces can extend other interfaces and are open (declaration merging), which suits public object and class contracts you expect others to build on.
Interfaces vs type aliases. A type alias names any type, not just object shapes: primitives, unions, intersections, tuples, function types, and mapped/conditional types. For a plain object shape interface and type are nearly interchangeable. The practical convention: reach for interface when you want an extendable/mergeable object or class contract, and type when you need a union, intersection, tuple, or a computed type. Pick one style and stay consistent.
Unions and intersections. A union (A | B) means a value is one of several types — the workhorse being string-literal unions like 'meals' | 'travel' | 'other', which constrain a field to a fixed allowed set at compile time (a lightweight enum). An intersection (A & B) means a value has all the members of both — handy for composing props (ButtonProps & { icon: string }). Discriminated unions (each variant carrying a common literal kind field) are the idiomatic way to model 'one of several shapes' and narrow them safely.
Type narrowing. Inside a branch, TypeScript narrows a broad type to a more specific one using control-flow analysis: typeof x === 'number', Array.isArray(x), instanceof, truthiness checks (if (user)), in checks ('radius' in shape), and custom type guards (function isCat(a): a is Cat). For a discriminated union, switch (shape.kind) narrows to the matching variant in each case. This is the compiler tracking your instanceof-style checks for you so the code inside is fully typed.
Generics. Generics parameterise code by type, exactly like Java generics. function firstItem<T>(items: T[]): T | undefined preserves the element type through the call so the result isn't any. You can constrain a parameter (<T extends { id: number }>), give defaults (<T = string>), and generic types flow through classes, interfaces, and React components (useState<User | null>(null)). They give you reuse without losing type information.
Enums (and the alternative). An enum names a set of related constants (enum Status { Draft = 'draft', Sent = 'sent' }). String enums are readable and safe; numeric enums auto-increment. Note enums emit real runtime code (they aren't erased), so many teams prefer a string-literal union plus a const object (as const) for a zero-runtime-cost equivalent. Know both; enums remain common in existing codebases.
Utility types. These transform existing types so you don't duplicate shapes. Partial<T> makes every field optional (perfect for update/patch payloads). Required<T> is the inverse. Pick<T, K> selects a subset of keys; Omit<T, K> removes keys. Record<K, V> builds a map type (Record<string, User>). Readonly<T> freezes all fields. Parameters<F> and ReturnType<F> extract a function's argument and return types. Deriving variants from one source type keeps your models DRY.
Typing functions and React props. A function type is (a: number, b: number) => number; parameters can be optional (b?: number), have defaults, or use rest (...args: number[]). In React you type a component's props with an interface or type and destructure them: a Button taking a label string, an optional variant string-literal union, and an onClick handler typed as () => void. Children are typed with React.ReactNode, and events with the matching React event type (React.ChangeEvent<HTMLInputElement>). Well-typed props are living documentation and prevent misuse at the call site.
Structural typing (the big mental shift from Java). TypeScript is structurally typed, not nominally: compatibility is decided by shape, not by name or explicit implements. If an object has all the members a type requires, it is that type — no declaration needed. This is 'duck typing' checked at compile time, and it's why a plain object literal can satisfy an interface it never mentions. It makes TypeScript flexible but occasionally surprising to Java developers used to nominal typing.
tsconfig and strictness. The compiler's behaviour lives in tsconfig.json. The one setting that matters most is "strict": true, which turns on strictNullChecks (null/undefined are not silently assignable — you must handle them), noImplicitAny (untyped values are an error, not silent any), and several more. Strict mode is where TypeScript earns its keep; turning it off gives you JavaScript with extra syntax. The cardinal rule: never reach for any (it disables checking) — use unknown for genuinely unknown values and narrow it before use.
The mental model (memorise this). Types are compile-time only and erased at runtime. Describe shapes with interfaces/types, constrain values with string-literal unions, reuse logic with generics, and derive variants with utility types (Partial/Pick/Omit/Record). Let inference do the interior work and annotate the boundaries. Compatibility is structural (shape, not name). Turn on strict, handle null explicitly, and never use any — reach for unknown and narrow.
TypeScript is Java's type discipline applied to JavaScript, with two twists. An interface is a Java interface/POJO contract, generics are Java generics (including bounds via `T extends ...`), string-literal unions are a constrained enum of allowed values, and type narrowing with typeof/instanceof/type guards is the compiler tracking your instanceof checks so the branch is fully typed. Utility types are like deriving DTO variants from one entity instead of hand-writing each. The key difference is structural vs nominal typing: Java needs an explicit `implements`, whereas TypeScript accepts any object whose shape matches — duck typing verified at compile time. And `"strict": true` with strictNullChecks is the equivalent of finally taking Optional and NullPointerException seriously.
- Types are erased at compile time — the browser runs plain JavaScript. TypeScript's value is editor-time bug catching, autocomplete, and safe refactors.
- Annotate boundaries (params, exported APIs, empty containers); let inference handle everything in between.
- interface for extendable object/class contracts; type alias for unions, intersections, tuples, and computed types. They overlap for plain object shapes.
- String-literal unions ('a' | 'b' | 'c') constrain a value to a fixed set at compile time — a lightweight, zero-runtime enum.
- Narrowing (typeof, instanceof, in, Array.isArray, truthiness, custom guards, discriminated unions) refines a broad type inside a branch.
- Generics (<T>, plus constraints and defaults) preserve type information through functions, classes, and React components — just like Java generics.
- Utility types derive variants from one source: Partial, Required, Pick, Omit, Record, Readonly, ReturnType — keep models DRY.
- TypeScript is structurally typed: compatibility is by shape, not by name or explicit implements (duck typing at compile time).
- Never use any (it disables checking); use unknown for truly unknown values and narrow with a type guard before use.
- Turn on "strict": true — strictNullChecks and noImplicitAny are where TypeScript actually earns its keep.
Worked Code
// Primitives + inference (no annotation needed here)
let count = 0; // inferred number
const name = 'Ada'; // inferred string
const ids: number[] = []; // annotate empty containers
const pair: [string, number] = ['x', 1]; // tuple
// interface — an extendable object contract (like a Java interface/POJO)
interface Expense {
readonly id: number; // can't be reassigned after creation
amount: number;
category: 'meals' | 'travel' | 'other'; // string-literal union
approved: boolean;
note?: string; // optional field
}
interface RecurringExpense extends Expense {
everyDays: number; // interfaces extend
}
// type alias — unions, intersections, function types
type Id = number | string; // union
type WithTimestamps = { createdAt: number; updatedAt: number };
type StoredExpense = Expense & WithTimestamps; // intersection// Narrowing a union with typeof
function format(value: string | number): string {
if (typeof value === 'number') return value.toFixed(2); // value: number here
return value.toUpperCase(); // value: string here
}
// Custom type guard: 'x is Cat' teaches the compiler
interface Cat { meow(): void }
interface Dog { bark(): void }
function isCat(a: Cat | Dog): a is Cat {
return (a as Cat).meow !== undefined;
}
// Discriminated union — narrow on a shared literal 'kind'
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rect'; w: number; h: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2; // narrowed to circle
case 'rect': return shape.w * shape.h; // narrowed to rect
}
}
// unknown forces narrowing before use (never use 'any')
function parse(input: unknown): string {
if (typeof input === 'string') return input;
return String(input);
}// Generic (like Java generics) — preserves the element type
function firstItem<T>(items: T[]): T | undefined {
return items[0];
}
// Constrained generic — T must have an id
function byId<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find((i) => i.id === id);
}
// Enum — named constants (emits runtime code)
enum Status { Draft = 'draft', Sent = 'sent', Approved = 'approved' }
const current: Status = Status.Draft;
// Zero-runtime alternative: literal union + const object
type StatusLit = 'draft' | 'sent' | 'approved';
interface User { id: number; name: string; email: string; role: string }
// Utility types derive variants from ONE source type
type UserPatch = Partial<User>; // all optional (update payload)
type UserPublic = Omit<User, 'email'>; // drop a field
type UserKeyInfo = Pick<User, 'id' | 'name'>; // keep a subset
type UsersById = Record<number, User>; // map type
type FrozenUser = Readonly<User>; // immutable// Function types: optional, default, and rest params
type Adder = (a: number, b?: number) => number;
const add: Adder = (a, b = 0) => a + b;
function sumAll(...nums: number[]): number {
return nums.reduce((s, n) => s + n, 0);
}
// React props typed with an interface
interface ButtonProps {
label: string;
variant?: 'primary' | 'ghost'; // optional literal union
disabled?: boolean;
onClick: () => void; // event handler type
children?: React.ReactNode; // renderable children
}
function Button({ label, variant = 'primary', onClick, children }: ButtonProps) {
return (
<button className={`btn btn-${variant}`} onClick={onClick}>
{label}
{children}
</button>
);
}
// Typed input change handler
function NameField() {
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
return <input onChange={onChange} />;
}{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true, /* strictNullChecks + noImplicitAny + more */
"noUncheckedIndexedAccess": true, /* arr[i] is T | undefined */
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src"]
}▶Try It Live
Edit the code and press Run — it executes safely in a sandboxed iframe. Use the Console tab for log output.
Interview-Ready Q&A
TypeScript is JavaScript with an optional static type layer checked by the compiler and then erased — the browser runs ordinary JavaScript with no type information left. The benefit is entirely at development time: catching type errors, wrong arguments, and null access in the editor, plus autocomplete and safe project-wide refactors. Because types are erased, you can't do runtime type checks against interfaces; you narrow with runtime checks like typeof or a validation library.
- 1Types are compile-time only and erased at runtime — the browser runs plain JavaScript.
- 2Annotate boundaries (params, exported APIs, empty arrays); let inference do the interior.
- 3interface = extendable object/class contract; type = unions, intersections, tuples, computed types.
- 4String-literal unions constrain a value to a fixed set — a zero-runtime enum.
- 5Narrow with typeof / instanceof / in / Array.isArray / custom guards / discriminated unions.
- 6Generics (with extends constraints and = defaults) preserve type info through functions and components.
- 7Utility types derive variants from one source: Partial, Required, Pick, Omit, Record, Readonly.
- 8TypeScript is structurally typed — compatibility is by shape, not by name (no implements needed).
- 9Never use any; use unknown and narrow. Enums emit runtime code — literal unions are the lean alternative.
- 10Turn on "strict": true (strictNullChecks + noImplicitAny) — that's where TypeScript earns its keep.