Skip to content

Expert · TypeScript basics · topic 44 of 61 · 30 min

Object types, interfaces and unions

Explanation

Describe object shapes with interface or type, with optional (?) and readonly properties. Union types say a value can be one of several types, and narrowing (typeof, in, equality checks, a shared "kind" field) lets TypeScript know which one you have. A never check makes sure every case of a union is handled.

Describing objects

typescript
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.

typescript
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.

typescript
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.

typescript
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

typescript
function isCard(p: Payment): p is Extract<Payment, { kind: 'card' }> {
  return p.kind === 'card';
}
// after if (isCard(p)) { … } TypeScript knows p is the card member
A function returning "x is T" is a user-defined type guard.
Careful: Optional is not the same as "can be undefined at any time". With discount?: number, reading product.discount gives number | undefined, so product.discount * 2 is an error. Handle the missing case: (product.discount ?? 0) * 2.
Think of it like this: A discriminated union is like the coloured tags on luggage at an airport. The tag (kind) tells the handler at once which belt the bag goes to, so nobody has to open the bag to find out what is inside.
Tip: Model data so impossible states cannot be written. Instead of { loading: boolean; error?: string; data?: User }, use a union: { state: 'loading' } | { state: 'error'; error: string } | { state: 'done'; data: User }.

Need 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

Objects and unions

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.

typescript
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)

Task 1 · Code · Typescript

Write formatId(id: string | number): string. Numbers are padded to 6 digits with zeros ("000042"); strings are returned in upper case.

Task 2 · Code · Typescript

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.

Task 3 · Code · Typescript

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

Question 1

1.interface A { x?: number }. What is the type of a.x?

Question 2

2.What makes a union "discriminated"?

Question 3

3.Why assign to never in a default case?

Question 4

4.Which narrows string | string[] to the array?

Read more (free)

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.

From Typing functions and objects

Which type forces you to check a value before using it?Related

Answer: unknown

unknown accepts anything but blocks use until narrowed.

From Basic types and inference

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: Generics →

Next steps

  1. 2 more topics in “TypeScript basics” before its exam: Generics, Utility types and real-world TypeScript.
  2. Module 9 exam: TypeScript basics · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Functional patterns and testing. 61 topics left in the course before the final exam.

← Previous: Basic types and inference