Course outline · 0% complete

0/26 lessons0%

Course overview →

Optional and Readonly Properties

lesson 3-2 · ~11 min · 9/26

Not every property is always there

Real data has gaps: a profile might not have a bio yet. Mark a property optional with ?:

interface Profile {
  readonly id: number;
  username: string;
  bio?: string;
}

bio?: string means the property is either a string or absent. TypeScript then forces you to handle the absent case: p.bio.length is an error until you check, for example if (p.bio !== undefined). That check is your first taste of narrowing, which Unit 4 covers properly.

readonly id means the property can be set when the object is created and never reassigned afterwards. p.id = 99 is a compile error. Like const for a single property, and like everything else in TypeScript, it is enforced only at compile time.

Reading an optional property safely

The first call below omits bio entirely, which is legal because bio is optional.

interface Profile {
  readonly id: number;
  username: string;
  bio?: string;
}

function show(p: Profile): void {
  if (p.bio !== undefined) {
    console.log(p.username + ": " + p.bio);
  } else {
    console.log(p.username + ": no bio yet");
  }
}

show({ id: 1, username: "ada" });
show({ id: 2, username: "grace", bio: "compiler pioneer" });

Output

ada: no bio yet
grace: compiler pioneer

Two annotations worth pausing on

  • The void return type marks a function that returns nothing useful. show prints instead of producing a value, and lesson 5-3 goes deeper on this.
  • Adding p.id = 99 inside show is a compile error, because id is declared readonly. The property can be set when the object is created and never reassigned afterwards.

The real type behind bio?: string

With bio?: string, reading p.bio gives you the type string | undefined.

Optional means the value might not be there, and TypeScript is explicit about that rather than pretending the property is always a string. The union spells out both possibilities.

This is why the compiler blocks p.bio.length outright. On the undefined half of that union there is no .length to read, and calling it would be the classic "cannot read properties of undefined" crash. A check such as if (p.bio !== undefined) rules the undefined case out, and inside that branch the type collapses to plain string so the access becomes legal.

A one-line if: the conditional operator

The next example needs a value that depends on a condition: the mark is "[x]" when a task is done, and "[ ]" otherwise. JavaScript has an expression form of if/else built for exactly this, the conditional operator, often called the ternary operator because it takes three parts. It has not appeared in your courses so far, so here is the whole rule:

const mark = t.done ? "[x]" : "[ ]";

Read condition ? a : b as: evaluate the condition, produce a if it is true, otherwise produce b.

It exists because an if/else statement cannot sit on the right side of an =. The conditional operator is an expression, so it produces a value you can assign, return, or pass as an argument. TypeScript checks both arms like any other expression, so a typo in either branch is still caught.

report: an optional field and a conditional mark

Task has a required title and done flag plus an optional due date, and report renders one line for any task.

interface Task {
  title: string;
  done: boolean;
  due?: string;
}

function report(t: Task): string {
  const mark = t.done ? "[x]" : "[ ]";
  if (t.due !== undefined) {
    return mark + " " + t.title + " (due " + t.due + ")";
  }
  return mark + " " + t.title;
}

console.log(report({ title: "Write resume", done: true }));
console.log(report({ title: "Apply to jobs", done: false, due: "Friday" }));

Output

[x] Write resume
[ ] Apply to jobs (due Friday)

Three pieces working together

  • The optional syntax is due?: string inside the interface, which is what makes the first call legal despite having no due.
  • The conditional operator from this lesson builds the checkbox: const mark = t.done ? "[x]" : "[ ]";. Both arms are strings, so mark is inferred as string.
  • The if (t.due !== undefined) guard comes before any use of t.due. Inside it the type is string, so concatenating it is allowed. Without the guard, the compiler would reject the line even though the code would often work at runtime.