TypeScript Fundamentals: Types, Generics and Real-World Patterns · TypeScript Basics: Types, Narrowing and Functions
Objects, Interfaces and Type Aliases
Describe the shape of objects with interfaces and type aliases, mark properties optional or readonly, use index signatures, and understand how TypeScript compares object types.
What you'll build
A contact book: a typed Contact shape with optional and read-only properties, a lookup table keyed by name, and a function that formats a contact for display.
The idea
Most real data is an object, so the first skill is describing its shape.
Two tools do the job:
interfacenames an object shape. Interfaces can be extended (interface Employee extends Contact) and merged by declaring them twice.typecreates an alias for any type, including unions:type Id = string | number;. Use it for unions, tuples and computed types.
For plain object shapes either works. A good default: use interface for object shapes and type for everything else, and stay consistent within a project.
Property modifiers:
email?: string: optional. Its type becomesstring | undefined, so you must handle a missing value.readonly id: number: assigned once, then the compiler refuses reassignment.- Index signature
{ [name: string]: Contact }: an object used as a dictionary.
TypeScript uses structural typing: a value fits a type if it has the required properties, whatever it is called. Object literals are also checked for excess properties, which catches typos like emial.
The code
interface Address {
city: string;
country: string;
}
interface Contact {
readonly id: number;
name: string;
email?: string; // optional
address: Address;
}
type ContactBook = { [name: string]: Contact };
const book: ContactBook = {
ada: { id: 1, name: "Ada Lovelace", email: "[email protected]", address: { city: "London", country: "UK" } },
linus: { id: 2, name: "Linus Torvalds", address: { city: "Portland", country: "US" } },
};
function format(contact: Contact): string {
// email is optional, so we must handle undefined
const email = contact.email ?? "no email";
return `#${contact.id} ${contact.name} <${email}> from ${contact.address.city}`;
}
for (const key of Object.keys(book)) {
console.log(format(book[key]));
}
const ada = book["ada"];
// @ts-expect-error: id is readonly
ada.id = 99;
// Structural typing: any object with the right shape fits
const guest = { id: 3, name: "Guest", address: { city: "Paris", country: "FR" }, vip: true };
console.log(format(guest));
// @ts-expect-error: object literals are checked for excess or misspelt properties
const typo: Contact = { id: 4, name: "Typo", emial: "[email protected]", address: { city: "Rome", country: "IT" } };
Output
#1 Ada Lovelace <[email protected]> from London #2 Linus Torvalds <no email> from Portland #3 Guest <no email> from Paris
Notice that guest is accepted even though it has an extra vip property: excess-property checks apply only to object literals assigned directly to a typed target.
Common mistakes
- Forgetting that optional means
| undefined. Use??,?.or a check before using the value. - Believing
readonlyprotects data at runtime. It is a compile-time check; the JavaScript object can still be changed by untyped code. - Using
{ [key: string]: T }and trusting every lookup.book["nobody"]is typedContactbut isundefinedat runtime. UseRecord<string, Contact | undefined>,Map, or thenoUncheckedIndexedAccessoption. - Declaring huge interfaces by hand for data that already has a source of truth. Derive types from schemas or APIs where you can.
- Using
Objector{}as a type. They accept almost anything. Use a specific shape,object, orRecord<string, unknown>.
Key points
interfaceandtypeboth describe object shapes;typealso names unions, tuples and computed types.?makes a property optional (T | undefined) andreadonlyforbids reassignment.- TypeScript is structural: matching shape is enough, names don't matter.
- Object literals get excess-property checks, which catch misspelt property names.
Further reading: The Handbook: Object Types.
Practice questions
When should you use `type` rather than `interface`?
Answer
Use type when you need something an interface cannot express: unions, tuples, primitives aliases, mapped or conditional types. For plain object shapes both work; interfaces can also be extended and merged.
Why is `book["nobody"]` typed as `Contact` even though it is undefined at runtime, and how do you guard against it?
Answer
An index signature promises a value for every string key, which is not true for missing keys. Type the values as Contact | undefined (Record<string, Contact | undefined>), use a Map, or enable noUncheckedIndexedAccess so lookups include undefined.
Open in the app to mark lessons complete, take notes and track your progress.