Expert · TypeScript basics · topic 44 of 61 · 30 min
Object types, interfaces and unions
Explanation
Describing objects
interface Product {
readonly id: number; // cannot be changed after creation
name: string;
price: number;
discount?: number; // optional: may be missing
}
const fan: Product = { id: 1, name: 'Ceiling fan', price: 2499 };
fan.price = 2299; // OK
fan.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.
const bad: Product = { id: 2, name: 'Bulb' }; // Error: Property 'price' is missing.interface or type?
Both describe object shapes. type can also name unions, tuples and primitives (type Id = string | number), while interface can be extended and reopened. Many teams use interface for object shapes and type for everything else; the important thing is to be consistent.
type Id = string | number;
interface Person { name: string }
interface Employee extends Person { empId: Id; dept: string }
type Customer = Person & { phone: string }; // & combines (intersection)Union types and narrowing
A union value could be any of its members, so TypeScript only lets you use what is common to all of them. Narrowing means checking at run time which member you have; inside the check, TypeScript knows the narrower type.
function formatId(id: string | number): string {
if (typeof id === 'number') {
return id.toFixed(0); // here id is number
}
return id.toUpperCase(); // here id is string
}Narrowing tools: typeof (primitives), Array.isArray, instanceof (classes), the in operator ('price' in item), equality (=== null), and truthiness checks.
Discriminated unions
The most useful pattern: every member of a union has a common field with a different literal value, usually called kind or type. Checking that field narrows to the exact member. This models real data like payment methods or API responses very clearly.
type Payment =
| { kind: 'upi'; vpa: string }
| { kind: 'card'; last4: string; network: 'VISA' | 'RUPAY' | 'MASTERCARD' }
| { kind: 'cod' };
function describe(p: Payment): string {
switch (p.kind) {
case 'upi': return `UPI ${p.vpa}`;
case 'card': return `${p.network} card ending ${p.last4}`;
case 'cod': return 'Cash on delivery';
default: {
const unreachable: never = p; // error here if a case is missing
return unreachable;
}
}
}Exhaustive checks with never
In the default branch above, p has type never if every kind was handled. If someone later adds { kind: 'wallet' } to Payment and forgets to update describe, the assignment to never becomes a compile error pointing at the exact place to fix.
Type guards
function isCard(p: Payment): p is Extract<Payment, { kind: 'card' }> {
return p.kind === 'card';
}
// after if (isCard(p)) { … } TypeScript knows p is the card memberNeed help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Object types, interfaces and unions · 1/6
In this lesson you will describe objects and values that can be one of several kinds.
Prefer a video by a teacher? Find free videos on “typescript interfaces union types narrowing discriminated unions” on YouTube ↗
In real life: Order status on a food app
A food-delivery app's order can be placed, out for delivery (with a rider and ETA) or delivered (with a time). A discriminated union makes it impossible to show a rider name for an order that is not out for delivery, and the never check catches any status the UI forgot.
type Order =
| { status: 'placed' }
| { status: 'out'; rider: string; etaMin: number }
| { status: 'delivered'; at: string };
function banner(o: Order): string {
if (o.status === 'out') return `${o.rider} arrives in ${o.etaMin} min`;
if (o.status === 'delivered') return `Delivered at ${o.at}`;
return 'Restaurant is preparing your food';
}Practice (0/3 done)
Write formatId(id: string | number): string. Numbers are padded to 6 digits with zeros ("000042"); strings are returned in upper case.
Using the Payment union (upi with vpa, card with last4, cod), write fee(p: Payment, amount: number): number. UPI is free (0), card is 2% of amount rounded to the nearest rupee, cod is a flat 40.
interface Product { name: string; price: number; discount?: number }. Write finalPrice(p: Product): number = price minus discount percent (if any), rounded to the nearest rupee.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- GenericsTypeScript basics · same module
- Unions and narrowingFrontend Web Development · TypeScript basics
- Typing functions and objectsFrontend Web Development · TypeScript basics
- Unions, enums and memory layoutC Programming (from zero to systems) · Structs, unions, enums and files
- Why TypeScriptFrontend Web Development · TypeScript basics
- Records and pattern matchingC# and .NET · Inheritance, interfaces and records
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
interface A { x?: number }. What is the type of a.x?
Answer: number | undefined
Optional properties may be missing, so undefined is included.
What makes a union "discriminated"?
Answer: Every member has a common field with a different literal value
The shared literal field lets TypeScript narrow precisely.
Why assign to never in a default case?
Answer: So adding a new union member without handling it becomes a compile error
It is an exhaustiveness check.
Which narrows string | string[] to the array?
Answer: Array.isArray(x)
typeof gives "object" for arrays; Array.isArray narrows correctly.
What is a discriminated union?Related
Answer: A union whose members share a literal field (like kind) that tells them apart
The shared literal field lets a switch or if narrow to the exact member.
From Unions and narrowing
type Product = { readonly id: string; price: number }; const p: Product = { id: "p1", price: 50 }; Which line is an error?Related
Answer: p.id = "p2";
id is readonly, so it cannot be assigned after the object is created. Making a new object with spread is fine.
Which type forces you to check a value before using it?Related
Answer: unknown
unknown accepts anything but blocks use until narrowed.
Which of these is NOT a primitive type?Related
Answer: array
Arrays are objects. The primitives are string, number, bigint, boolean, undefined, null and symbol.
From Values and types
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “TypeScript basics” before its exam: Generics, Utility types and real-world TypeScript.
- Module 9 exam: TypeScript basics · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Functional patterns and testing. 61 topics left in the course before the final exam.