Objects and Interfaces
Most real data isn't a lone string or number — it's objects: users, orders, API responses. This page is about describing their shape.
Object Types
You can type an object inline by listing its properties:
function printUser(user: { name: string; age: number }): void {
console.log(`${user.name} is ${user.age} years old`);
}
printUser({ name: "Rizwan", age: 29 }); // ✅
printUser({ name: "Sara" }); // ❌ Property 'age' is missing
printUser({ name: "Ali", age: "thirty" }); // ❌ 'age' must be number
Inline types get unreadable fast. For anything used more than once, give the shape a name.
Interfaces
An interface names an object shape:
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
const rizwan: User = {
id: 1,
name: "Rizwan Ashiq",
email: "rizwan@example.com",
isActive: true,
};
function deactivate(user: User): User {
return { ...user, isActive: false };
}
Now every function dealing with users shares one definition. Rename a property in the interface and the compiler points at every place that needs updating — this is what makes refactoring large codebases sane.
Type Alias
A type alias can name an object shape too, and the syntax is nearly identical:
type Product = {
id: number;
title: string;
price: number;
};
const laptop: Product = { id: 1, title: "ThinkPad", price: 1200 };
Interface vs type — which one?
For plain object shapes they're interchangeable. The practical differences:
typecan name anything — unions, tuples, primitives — whileinterfaceonly describes object shapes:
type OrderStatus = "pending" | "shipped" | "delivered"; // interface can't do this
type Coordinate = [number, number]; // or this
type Id = string | number; // or this
interfacesupports declaration merging — declaring the same interface twice merges them. Mostly useful for extending third-party library types.
My convention (and a common one): interface for object shapes, type for everything else. Don't lose sleep over it — consistency matters more than the choice.
Optional Properties
A question mark makes a property optional:
interface UserProfile {
name: string;
email: string;
bio?: string; // may be missing
avatarUrl?: string; // may be missing
}
const minimal: UserProfile = { name: "Sara", email: "sara@example.com" }; // ✅
The type of an optional property inside the object is "its type or undefined", so TypeScript forces you to handle the missing case:
function bioLength(profile: UserProfile): number {
return profile.bio.length; // ❌ 'profile.bio' is possibly 'undefined'
}
function bioLengthSafe(profile: UserProfile): number {
return profile.bio?.length ?? 0; // ✅ optional chaining + fallback
}
That compile error is TypeScript preventing the classic Cannot read properties of undefined crash.
Readonly Properties
readonly blocks reassignment after creation:
interface Config {
readonly apiUrl: string;
readonly maxRetries: number;
}
const config: Config = { apiUrl: "https://api.example.com", maxRetries: 3 };
config.maxRetries = 5; // ❌ Cannot assign to 'maxRetries' because it is a read-only property
I use this for config objects and IDs — things that should be set once. Note it's a compile-time guarantee only; the emitted JavaScript object is still mutable at runtime.
Extending Interfaces
Interfaces can build on other interfaces with extends:
interface Person {
name: string;
email: string;
}
interface Employee extends Person {
employeeId: number;
department: string;
}
const dev: Employee = {
name: "Rizwan",
email: "rizwan@example.com",
employeeId: 42,
department: "Engineering",
};
An Employee has everything a Person has, plus its own fields. Any function that accepts a Person also accepts an Employee — the shape is compatible.
Type aliases achieve the same with an intersection (&):
type Timestamped = { createdAt: Date; updatedAt: Date };
type Post = Timestamped & {
title: string;
body: string;
};
Index Signatures
Sometimes you don't know the property names ahead of time — only their types. Think of a map of error messages keyed by field name:
interface ValidationErrors {
[field: string]: string;
}
const errors: ValidationErrors = {
email: "Email is invalid",
password: "Password too short",
};
errors.username = "Username is taken"; // ✅ any string key works
errors.age = 42; // ❌ values must be string
You can mix known properties with an index signature, as long as they're compatible:
interface ApiHeaders {
"content-type": string;
[header: string]: string;
}
For a plain "keys of type X, values of type Y" map, the built-in Record utility type reads better than writing an index signature by hand:
const errors: Record<string, string> = {
email: "Email is invalid",
};
const stock: Record<string, number> = { laptop: 12, mouse: 40 };
When you pass an object literal directly, TypeScript is extra strict and rejects unknown properties:
interface Point {
x: number;
y: number;
}
function plot(p: Point): void {
console.log(p.x, p.y);
}
plot({ x: 1, y: 2, z: 3 }); // ❌ 'z' does not exist in type 'Point'
const p3d = { x: 1, y: 2, z: 3 };
plot(p3d); // ✅ allowed — extra props on a variable are fine
This catches typos in literals (like colour instead of color) at the exact place you write them.
Recap
- Name your object shapes —
interfacefor objects,typefor unions, tuples, and everything else ?marks optional properties and forces you to handle the undefined casereadonlyprevents reassignment at compile timeextends(interfaces) and&(type aliases) compose shapes- Index signatures and
Recordtype objects with dynamic keys
Next: typing functions properly, and the feature that makes reusable typed code possible — generics.