JavaScript doesn’t care what type your variable is. Pass a number where a function expects a string — no warning, no error, just broken behaviour at runtime. Scale that up to a team of ten developers and a codebase of fifty thousand lines, and you have a bug-finding nightmare.
TypeScript fixes this. It adds a type system on top of JavaScript so you catch mistakes while writing code — not after shipping it.
What Is TypeScript?
TypeScript is a superset of JavaScript. Every valid JavaScript file is a valid TypeScript file. You’re not learning a new language — you’re adding one layer on top of the one you already know.
JavaScript ⊂ TypeScript
The TypeScript compiler (tsc) takes your .ts files and outputs plain .js. Browsers and Node.js never see TypeScript — they run the compiled JavaScript. TypeScript exists entirely at development time.
your-code.ts → tsc → your-code.js → browser/node
What TypeScript adds:
- Static types — catch type errors before running the code
- Better tooling — autocomplete, inline docs, safe refactoring in your editor
- Self-documenting code — function signatures tell you exactly what goes in and comes out
Used by: Microsoft, Google, Airbnb, Slack, and virtually every large-scale JavaScript project.
Setup
Install TypeScript globally or per-project:
npm install -g typescript # global
npm install --save-dev typescript # per-project
Compile a file:
tsc index.ts # outputs index.js
tsc --watch # watches for changes, recompiles automatically
Initialise a config file:
tsc --init # creates tsconfig.json
tsconfig.json controls how TypeScript compiles your project. The most important option:
{
"compilerOptions": {
"strict": true, // enable all strict checks — always turn this on
"target": "ES6", // output JS version
"module": "commonjs", // module system
"outDir": "./dist", // where compiled JS goes
"rootDir": "./src" // where your TS source is
}
}
Always enable strict: true. It activates a group of checks — strictNullChecks, noImplicitAny, and others — that catch the most common bugs. Starting without strict mode is starting with TypeScript’s hands tied.
Type Inference
TypeScript is smarter than you think. You don’t always have to declare types — it figures them out automatically.
let name = 'Dhaivick'; // TypeScript infers: string
let age = 27; // TypeScript infers: number
let isActive = true; // TypeScript infers: boolean
name = 42; // Error: Type 'number' is not assignable to type 'string'
Once TypeScript infers a type, it enforces it. This is type inference — the compiler reads your values and deduces the types without you writing them.
The rule: let inference work when the type is obvious from the value. Annotate explicitly when it isn’t.
Type Annotations
When inference isn’t enough, annotate manually using : type:
let username: string = 'Dhaivick';
let score: number = 100;
let isPremium: boolean = false;
More useful on function parameters and return values — where there’s no value to infer from:
const greet = (name: string): string => {
return `Hello, ${name}`;
};
greet('Dhaivick'); // ✓
greet(42); // Error: Argument of type 'number' is not assignable to parameter of type 'string'
Basic Types
let a: string = 'hello';
let b: number = 42; // covers integers AND floats
let c: boolean = true;
let d: null = null;
let e: undefined = undefined;
// Arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ['a', 'b', 'c']; // generic syntax — same thing
// Any, unknown, never, void — covered below
any, unknown, and never
These three are where most interview questions live. They look similar but behave very differently.
any
Opts out of type checking entirely. Treat it as a fire escape — use it and TypeScript stops helping you.
let value: any = 'hello';
value = 42; // fine
value = true; // fine
value.foo.bar.baz; // fine — TypeScript doesn't check this at all
any is contagious. Once a value is any, anything you do with it becomes any too. Avoid it unless you’re migrating a JS codebase and need a temporary escape hatch.
unknown
The type-safe alternative to any. A value can be assigned anything, but you can’t use it until you narrow its type.
let value: unknown = 'hello';
value.toUpperCase(); // Error — you can't call methods on unknown
// You must check the type first
if (typeof value === 'string') {
value.toUpperCase(); // ✓ — TypeScript now knows it's a string
}
Use unknown when you genuinely don’t know the type — API responses, user input, error objects. It forces you to handle the type safely before using the value.
never
Represents something that never happens. A function that always throws, or a loop that never exits, returns never. Also used to signal unreachable code.
const throwError = (message: string): never => {
throw new Error(message); // this function never returns normally
};
// The exhaustive check pattern — common in interviews
type Shape = 'circle' | 'square';
const getArea = (shape: Shape): number => {
switch (shape) {
case 'circle': return Math.PI;
case 'square': return 1;
default:
const exhaustiveCheck: never = shape; // if Shape gets a new value and you forget to handle it, this errors
return exhaustiveCheck;
}
};
any → no type safety at all — avoid
unknown → unknown type, must narrow before use — use for external data
never → a value that can never exist — use for exhaustive checks and functions that always throw
void
Return type for functions that don’t return a value:
const logMessage = (msg: string): void => {
console.log(msg);
// no return statement
};
Tuples
Arrays with a fixed number of elements, each with a known type at each position.
const coordinate: [number, number] = [10, 20];
const entry: [string, number] = ['age', 27];
entry[0].toUpperCase(); // ✓ — TypeScript knows index 0 is a string
entry[1].toUpperCase(); // Error — index 1 is a number
Tuples are useful when the position carries meaning — coordinates, key-value pairs, RGB values.
Type Aliases
type creates a reusable name for any type — primitive, object, union, function.
type UserID = string;
type Coordinates = [number, number];
type User = {
id: UserID;
name: string;
age: number;
email?: string; // optional property — may or may not exist
};
const user: User = {
id: 'u_01',
name: 'Dhaivick',
age: 27,
// email is optional, so omitting it is fine
};
Interfaces
interface defines the shape of an object. Looks similar to type but has key differences.
interface User {
id: string;
name: string;
age: number;
email?: string;
}
// Extending an interface
interface Admin extends User {
role: 'admin' | 'superadmin';
}
const admin: Admin = {
id: 'a_01',
name: 'Dhaivick',
age: 27,
role: 'admin',
};
type vs interface — the question that always comes up:
interface type
───────────────────────────────── ────────────────────────────────────
Only describes object shapes Can describe any type (primitives, unions, tuples)
Extendable with `extends` Extended with intersection (&)
Can be merged (declaration merging) Cannot be merged — redeclaring errors
Cannot use union (|) directly Can use union, intersection, conditional types
// Declaration merging — only possible with interface
interface Window {
myCustomProperty: string;
}
interface Window {
anotherProperty: number;
}
// TypeScript merges these into one — useful for extending third-party types
Rule of thumb: use interface for objects that describe a shape (especially when extending is expected). Use type for everything else — unions, tuples, primitives, function signatures.
Union Types
A value that can be one of several types. Use |.
type ID = string | number;
let userId: ID = 'u_01';
userId = 42; // also fine
userId = true; // Error
// Union with literal types — extremely common
type Status = 'loading' | 'success' | 'error';
type Direction = 'north' | 'south' | 'east' | 'west';
let currentStatus: Status = 'loading';
currentStatus = 'pending'; // Error — 'pending' is not in the union
Literal type unions are one of TypeScript’s most powerful features. They turn a plain string into a set of valid values the compiler enforces.
Intersection Types
Combines multiple types into one. Use &. The result must satisfy all of them.
type Timestamped = {
createdAt: Date;
updatedAt: Date;
};
type User = {
id: string;
name: string;
};
type UserRecord = User & Timestamped;
const record: UserRecord = {
id: 'u_01',
name: 'Dhaivick',
createdAt: new Date(),
updatedAt: new Date(),
};
Functions
TypeScript lets you type parameters, return values, and even the function itself.
// Named function
function add(a: number, b: number): number {
return a + b;
}
// Arrow function
const multiply = (a: number, b: number): number => a * b;
// Optional parameter — must come after required ones
const greet = (name: string, greeting?: string): string => {
return `${greeting ?? 'Hello'}, ${name}`;
};
// Default parameter
const createUser = (name: string, role: string = 'viewer'): User => ({
id: Date.now().toString(),
name,
role,
});
// Rest parameters
const sum = (...nums: number[]): number => nums.reduce((a, b) => a + b, 0);
// Function type as a variable
type MathOperation = (a: number, b: number) => number;
const divide: MathOperation = (a, b) => a / b;
Type Guards and Narrowing
TypeScript tracks what you know about a type inside conditional blocks — this is called narrowing.
const processInput = (input: string | number): string => {
if (typeof input === 'string') {
return input.toUpperCase(); // TypeScript knows it's a string here
}
return input.toFixed(2); // TypeScript knows it's a number here
};
Built-in type guards:
typeof value === 'string' // for primitives
value instanceof Date // for class instances
Array.isArray(value) // for arrays
'property' in object // for checking if a property exists
Custom type guard — when built-ins aren’t enough:
type Cat = { meow: () => void };
type Dog = { bark: () => void };
// The return type `pet is Cat` tells TypeScript what's true if this returns true
const isCat = (pet: Cat | Dog): pet is Cat => {
return 'meow' in pet;
};
const makeNoise = (pet: Cat | Dog): void => {
if (isCat(pet)) {
pet.meow(); // TypeScript knows it's Cat
} else {
pet.bark(); // TypeScript knows it's Dog
}
};
Discriminated Unions
A pattern that combines union types with a shared literal property to make narrowing clean and exhaustive. One of the most powerful patterns in TypeScript.
type LoadingState = { status: 'loading' };
type SuccessState = { status: 'success'; data: string[] };
type ErrorState = { status: 'error'; message: string };
type RequestState = LoadingState | SuccessState | ErrorState;
const renderState = (state: RequestState): string => {
switch (state.status) { // TypeScript narrows based on the discriminant
case 'loading': return 'Loading...';
case 'success': return state.data.join(', '); // data is available here
case 'error': return `Error: ${state.message}`; // message is available here
}
};
The status field is the discriminant — the shared property whose literal type tells TypeScript which branch of the union you’re in. Interviewers love asking about this pattern.
Generics
Generics let you write reusable code that works with any type while still being type-safe. Think of them as type parameters.
// Without generics — only works with numbers
const firstNumber = (arr: number[]): number => arr[0];
// With generics — works with any type
const first = <T>(arr: T[]): T => arr[0];
first([1, 2, 3]); // T is inferred as number
first(['a', 'b', 'c']); // T is inferred as string
first<boolean>([true]); // T explicitly set to boolean
Generic interfaces:
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
type UserResponse = ApiResponse<User>;
type PostsResponse = ApiResponse<Post[]>;
Generic constraints — limit what types T can be:
// T must have a .length property
const getLength = <T extends { length: number }>(item: T): number => {
return item.length;
};
getLength('hello'); // ✓ — strings have length
getLength([1, 2, 3]); // ✓ — arrays have length
getLength(42); // Error — numbers don't have length
Enums
Named constants. When you have a fixed set of related values, an enum makes them explicit and self-documenting.
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
const move = (direction: Direction): void => {
console.log(`Moving ${direction}`);
};
move(Direction.Up); // ✓
move('UP'); // Error — must use the enum
Numeric enums (the default — avoid these in most cases):
enum Status {
Pending, // 0
Active, // 1
Inactive, // 2
}
Numeric enums have a quirk: TypeScript allows assigning any number to them, which defeats the purpose. String enums are safer — prefer them.
Const enums — completely erased at compile time, replaced with literal values:
const enum Direction {
Up = 'UP',
Down = 'DOWN',
}
// Direction.Up in your code becomes 'UP' in the compiled JS — no runtime object
Classes
TypeScript adds access modifiers to JavaScript classes.
class BankAccount {
public owner: string; // accessible from anywhere (default)
private balance: number; // only accessible inside this class
protected accountNumber: string; // accessible inside this class and subclasses
readonly id: string; // can be read anywhere, set only in the constructor
constructor(owner: string, initialBalance: number) {
this.owner = owner;
this.balance = initialBalance;
this.accountNumber = Math.random().toString();
this.id = crypto.randomUUID();
}
deposit(amount: number): void {
this.balance += amount;
}
getBalance(): number {
return this.balance;
}
}
const account = new BankAccount('Dhaivick', 1000);
account.owner; // ✓
account.balance; // Error — private
account.id = 'new-id'; // Error — readonly
Shorthand constructor — declare and assign in one line:
class User {
constructor(
public name: string,
private age: number,
readonly id: string,
) {}
// No body needed — TypeScript creates the properties automatically
}
Implementing interfaces:
interface Printable {
print(): void;
}
class Invoice implements Printable {
print(): void {
console.log('Printing invoice...');
}
}
Type Assertions
When you know more about a type than TypeScript does, you can assert it with as.
const input = document.getElementById('search') as HTMLInputElement;
input.value = 'hello'; // ✓ — without the assertion, TypeScript sees HTMLElement | null
// Or angle bracket syntax (not valid in JSX files)
const input2 = <HTMLInputElement>document.getElementById('search');
Non-null assertion (!) — tells TypeScript a value is definitely not null or undefined:
const button = document.getElementById('submit')!; // assert it exists
button.addEventListener('click', handleClick);
Use assertions sparingly. They override the type checker — if you’re wrong, you get a runtime error, not a compile-time one. If you find yourself using as frequently, it’s usually a sign the types aren’t modelled correctly.
Utility Types
TypeScript ships with built-in types that transform other types. These are extremely common in real codebases and in interviews.
type User = {
id: string;
name: string;
email: string;
age: number;
};
Partial<T> — makes all properties optional:
type UserUpdate = Partial<User>;
// { id?: string; name?: string; email?: string; age?: number }
const updateUser = (id: string, updates: Partial<User>) => { /* ... */ };
updateUser('u_01', { name: 'New Name' }); // only pass what changes
Required<T> — makes all properties required (opposite of Partial):
type StrictUser = Required<User>;
// all properties are now required, even if original had optionals
Readonly<T> — makes all properties readonly:
type ImmutableUser = Readonly<User>;
const user: ImmutableUser = { id: '1', name: 'D', email: 'e', age: 27 };
user.name = 'X'; // Error — cannot assign to readonly property
Pick<T, K> — create a type with only the properties you choose:
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: string; name: string }
Omit<T, K> — create a type with specific properties removed:
type PublicUser = Omit<User, 'email' | 'age'>;
// { id: string; name: string }
Record<K, V> — creates an object type with keys of type K and values of type V:
type StatusMap = Record<string, boolean>;
const featureFlags: StatusMap = {
darkMode: true,
notifications: false,
};
// With literal union keys — much more precise
type RolePermissions = Record<'admin' | 'viewer' | 'editor', boolean>;
ReturnType<T> — extracts the return type of a function:
const getUser = () => ({ id: '1', name: 'Dhaivick' });
type User = ReturnType<typeof getUser>;
// { id: string; name: string }
Parameters<T> — extracts the parameter types of a function as a tuple:
const createUser = (name: string, age: number) => ({ name, age });
type CreateUserArgs = Parameters<typeof createUser>;
// [name: string, age: number]
TypeScript with React
TypeScript and React are built for each other. Here’s how the core React patterns translate.
Typing component props:
type CardProps = {
name: string;
email: string;
id: number;
onClick?: () => void; // optional callback
};
const Card = ({ name, email, id, onClick }: CardProps) => {
return (
<div onClick={onClick}>
<h2>{name}</h2>
<p>{email}</p>
</div>
);
};
useState with a type:
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null); // starts null, becomes User after fetch
const [items, setItems] = useState<string[]>([]);
When the initial value makes the type obvious, inference works — useState(0) infers number. Annotate explicitly when the initial value doesn’t reveal the full type (useState<User | null>(null)).
Typing events:
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
console.log(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault();
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>): void => {
console.log('clicked');
};
// In JSX
<input onChange={handleChange} />
<form onSubmit={handleSubmit}>
<button onClick={handleClick}>
useRef with a type:
const inputRef = useRef<HTMLInputElement>(null);
// Accessing it safely
if (inputRef.current) {
inputRef.current.focus();
}
Typing children:
type LayoutProps = {
children: React.ReactNode; // anything React can render
};
const Layout = ({ children }: LayoutProps) => {
return <div className="layout">{children}</div>;
};
useContext with a type:
type ThemeContextType = {
theme: 'light' | 'dark';
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextType | null>(null);
// Safe consumption with a custom hook
const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used inside ThemeProvider');
return context;
};
Interview Essentials
What interviewers actually test — beyond the syntax.
type vs interface in one sentence
Use interface when defining object shapes that might be extended or merged. Use type for everything else.
Structural Typing
TypeScript uses structural typing (duck typing) — not nominal typing. A type is compatible if it has the right shape, regardless of what it’s called.
type Point2D = { x: number; y: number };
type Coordinate = { x: number; y: number };
const point: Point2D = { x: 1, y: 2 };
const coord: Coordinate = point; // ✓ — same shape, TypeScript is fine with this
// Extra properties are fine for assignment
const point3D = { x: 1, y: 2, z: 3 };
const p: Point2D = point3D; // ✓ — has x and y, extra z is ignored
This surprises people coming from Java or C#. TypeScript cares about structure, not names.
keyof and typeof
type User = { id: string; name: string; age: number };
type UserKeys = keyof User; // 'id' | 'name' | 'age'
// Practical use — a function that safely gets a property
const getProperty = <T, K extends keyof T>(obj: T, key: K): T[K] => {
return obj[key];
};
const user: User = { id: '1', name: 'Dhaivick', age: 27 };
getProperty(user, 'name'); // ✓ — returns string
getProperty(user, 'phone'); // Error — 'phone' is not a key of User
// typeof gets the type of a value
const config = { apiUrl: 'https://api.example.com', timeout: 3000 };
type Config = typeof config; // { apiUrl: string; timeout: number }
Conditional Types
Types that depend on a condition — like a ternary but for types:
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<string[]>; // true
type B = IsArray<string>; // false
You’ll see this in utility type implementations and advanced library typings.
Declaration Merging
Only works with interface. When you declare the same interface twice, TypeScript merges them:
interface Request {
user?: User;
}
// In a middleware type extension file:
interface Request {
token?: string;
}
// TypeScript sees: { user?: User; token?: string }
This is how libraries like Express allow you to add custom properties to their types.
as const
Turns a value into its most specific literal type and makes it deeply readonly:
const config = {
env: 'production',
version: 3,
} as const;
// Without as const: { env: string; version: number }
// With as const: { readonly env: 'production'; readonly version: 3 }
config.env = 'staging'; // Error — readonly
Common use: arrays of literal values that you want TypeScript to treat as a fixed tuple or union:
const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = typeof ROLES[number]; // 'admin' | 'editor' | 'viewer'
Mapped Types
Transform every property in a type — the engine behind utility types like Partial and Readonly:
// How Partial<T> is implemented internally
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Making all values a string
type Stringified<T> = {
[K in keyof T]: string;
};
You won’t write these daily, but understanding them helps you reason about utility types and read complex library types.
The Mental Model
TypeScript is a conversation between you and the compiler. You describe the shape of your data. TypeScript checks that you use it consistently. When something doesn’t match, it tells you at compile time — before a user ever sees the bug.
The more precisely you describe your types, the more TypeScript can help you. Vague types (any, overuse of string) give you weak guarantees. Precise types (literal unions, generics, discriminated unions) give you a compiler that catches real bugs.
Types are documentation that never goes stale. When you annotate a function’s parameters and return type, you’re telling every future reader — including yourself — exactly what that function expects and what it gives back.