Lesson 5 of 25
Object Types and Interfaces
Describing the shape of an object with an inline type or an interface, optional and readonly properties, and index signatures for dynamic keys.
Most real data isn't a single primitive — it's an object with several named
fields. This lesson covers describing that shape, first inline and then with
the more reusable interface syntax.
Inline object types
An object type literal describes a shape directly, right where you need it:
function printLesson(lesson: { title: string; minutes: number }) {
console.log(`${lesson.title} — ${lesson.minutes} min`);
}
printLesson({ title: "Object Types", minutes: 9 });This works, but it's not reusable — if three functions all take a "lesson" shaped object, you'd be repeating that literal three times, and a change to the shape means updating it in three places.
Naming the shape with an interface
An interface gives an object type a name you can reuse:
interface Lesson {
title: string;
minutes: number;
}
function printLesson(lesson: Lesson) {
console.log(`${lesson.title} — ${lesson.minutes} min`);
}
const lesson: Lesson = { title: "Object Types", minutes: 9 };
printLesson(lesson);Remember from the first lesson that TypeScript is structural: lesson
doesn't need to be declared as a Lesson to satisfy this function — it
just needs the right shape. The interface is a name for a shape, not a
runtime tag stamped onto the value.
Optional properties
A ? after a property name makes it possible to omit that property
entirely — not merely to set it to undefined, but to leave it out:
interface Lesson {
title: string;
minutes: number;
published?: boolean;
}
const draft: Lesson = { title: "Drafting", minutes: 5 }; // fine — published omittedReading an optional property gives you boolean | undefined, so code that
uses it usually needs a narrowing check or a default:
function status(lesson: Lesson) {
return lesson.published ? "live" : "draft"; // handles undefined too, via falsiness
}readonly properties
readonly blocks reassignment of that property after the object is
constructed — enforced by the compiler, not at runtime:
interface Lesson {
readonly slug: string;
title: string;
}
const lesson: Lesson = { slug: "object-types", title: "Object Types" };
lesson.title = "Renamed"; // fine
lesson.slug = "renamed";
// Cannot assign to 'slug' because it is a read-only property.Because readonly is compile-time only, it doesn't make the underlying
object literally immutable — Object.freeze() is the runtime equivalent, and
the two are unrelated to each other as far as the type system is concerned.
Index signatures
Sometimes an object's keys aren't known in advance — think of a dictionary built from user input or an API response. An index signature describes that:
interface WordCounts {
[word: string]: number;
}
const counts: WordCounts = {};
counts.typescript = 3;
counts["the"] = 12;
counts.typescript; // number
counts.missing; // number — TypeScript trusts the signature, even though this key was never setThat last line is a real sharp edge: TypeScript can't verify a specific key
actually exists at runtime, so counts.missing type-checks as number but
is actually undefined if you look it up. The noUncheckedIndexedAccess
compiler option (not on by default, even under strict) closes this gap by
typing every index access as T | undefined instead.
Nesting and extending
Interfaces can reference other interfaces, and can extend one another to build up a shape:
interface Address {
city: string;
country: string;
}
interface User {
name: string;
address: Address;
}
interface AdminUser extends User {
permissions: string[];
}
const admin: AdminUser = {
name: "Kwame",
address: { city: "Accra", country: "Ghana" },
permissions: ["publish", "delete"],
};extends copies every member of User into AdminUser, then adds
permissions on top — the same idea as a subclass, applied to a pure type
rather than a runtime class.
Declaration merging
A feature genuinely unique to interface (as opposed to type, covered
next lesson): declaring the same interface name twice in the same scope
merges the two declarations into one, combined shape.
interface Shape {
area(): number;
}
interface Shape {
color: string;
}
// Shape now requires both area() and color.
const square: Shape = {
area: () => 16,
color: "blue",
};This looks like a strange footgun in isolation, but it's exactly the
mechanism libraries use to let you extend a type they defined — augmenting
Window with a global your script adds, for instance. It comes back in the
modules lesson later in this track.
Try it yourself
What to remember
- An
interfacenames a reusable object shape; TypeScript still checks it structurally, not by declared label. ?allows a property to be omitted entirely;readonlyblocks reassignment, at compile time only.- An index signature (
{ [key: string]: T }) types a dictionary-like object with unknown keys but uniform value types. - Interfaces support
extendsand declaration merging — two featurestypealiases don't have, covered next.
Check yourself
4 questions · pass 3/4 to unlock Type Aliases vs Interfaces
1.Given
interface User { name: string; age?: number }, is{ name: "Amara" }a valid User?2.What does marking a property
readonlyactually prevent?3.What is an index signature like
{ [key: string]: number }for?4.Given
interface Shape { area(): number }andinterface Shape { color: string }declared separately in the same scope, what happens?
4 left to answer