Lesson 2 of 5

Interfaces & Type Aliases

In JavaScript, an object is whatever shape you decide to give it, and nothing checks that shape against anything. Pass the wrong object to a function and JavaScript won't complain until it tries to read a property that isn't there — usually as undefined quietly poisoning something three function calls downstream. TypeScript lets you name and enforce an object's shape before any of that happens.

💡 The Bottom Line

An interface (or a type alias) describes what properties an object must have and what type each one is. Once a function parameter is annotated with that shape, TypeScript checks every call site against it — passing an object missing a required property, or with a property of the wrong type, is a compile error, not a runtime surprise.

Describing an object shape

JavaScript
function printUser(user) {
  console.log(user.name, user.age);
}

printUser({ name: "Ada" });
// runs fine, logs: Ada undefined
TypeScript
interface User {
  name: string;
  age: number;
}

function printUser(user: User) {
  console.log(user.name, user.age);
}

printUser({ name: "Ada" });
// compile error: missing property 'age'

📋 Key differences

  • interface User { ... } declares a named shape once, reusable across many function signatures.
  • Each property line is name: Type; — no commas between properties, just semicolons (or newlines).
  • The JavaScript version silently produces undefined for a missing field; the TypeScript version refuses to compile the call in the first place.
  • The object literal itself doesn't need to say it's a User — TypeScript checks its shape against the parameter type automatically.

interface vs type — pick one, mostly by convention

TypeScript actually gives you two ways to name this shape: interface and type. For describing plain object shapes, they're close enough to interchangeable that most style guides just pick one and move on.

interface
interface User {
  name: string;
  age: number;
}
type alias
type User = {
  name: string;
  age: number;
};

interface can be re-opened later to add more properties (declaration merging) and reads slightly more like a class contract, which is why it's the common default for object shapes. type is required for things an interface can't express — unions like string | number (next lesson), tuples, or aliasing a primitive. A common rule of thumb: reach for interface for object shapes, type for everything else.

Optional properties

Not every field is always present — a user might not have a nickname yet. Mark a property optional with a ? right after its name, and TypeScript will allow the object with or without it, while still catching typos on the properties that are present.

Required only
interface User {
  name: string;
  nickname: string;
}
// { name: "Ada" } is now an error
Optional property
interface User {
  name: string;
  nickname?: string;
}
// { name: "Ada" } compiles fine
// user.nickname is `string | undefined`

⚠️ Gotcha

An optional property's type isn't just its declared type — it's that type | undefined under the hood. Code that reads user.nickname.toUpperCase() without checking for undefined first will be flagged by TypeScript, and rightly so: that's exactly the "cannot read property of undefined" crash JavaScript developers are used to hitting at runtime, except now it's caught before you ship it.

Structural typing: TypeScript checks shape, not name

TypeScript's type system is structural — often described as "duck typing, but checked at compile time instead of discovered at runtime." An object doesn't need to say it implements User; if it has all the required properties with compatible types, TypeScript accepts it. This is different from languages with nominal typing, where a class must explicitly declare which interfaces it implements.

Plain JS — no shape check at all
function printUser(user) {
  console.log(user.name);
}
// any object with or without .name
// is accepted; failures happen at
// runtime when .name is read
TypeScript — checked, but structural
interface User { name: string; }

function printUser(user: User) {
  console.log(user.name);
}

const admin = { name: "Ada", role: "admin" };
printUser(admin); // fine — shape matches,
                   // extra properties are OK here

🔧 Try It Yourself

Define an interface Product with id: number, name: string, and an optional discount?: number. Write a function describe(product: Product) that builds a sentence from those fields. Call it once with all three properties, once without discount, and once deliberately missing name — confirm the compiler only complains about the third call.

🙋 There Are No Dumb Questions

Q: Since it's just checking shape, can I pass an object with extra properties that aren't in the interface?
A: Usually yes, as shown above — this is called "excess property" tolerance for values stored in a variable first. The one exception: passing an object literal directly at the call site (not through a variable) triggers a stricter "excess property check," which flags typos like passing { name: "Ada", agee: 30 } straight into a call — a deliberate exception designed to catch misspelled property names.

✅ Quick Check

You define interface User { name: string; age?: number; }. Which call compiles without error?


Next up: interfaces describe one fixed shape. To write a function or class that works safely across many shapes without losing type information, you need generics.

← Back to
Next →