Free Handbook · Runs in your browser

Interfaces & Type Aliases

Interfaces and type aliases for describing object shapes: extending, readonly and optional properties, index signatures, and why excess property checks exist.

0 / 105 lessons🔥 0 day streak
ShareXLinkedIn

Module 04 · what you'll be able to do

  • Declare an interface and understand structural typing — matching shape is enough
  • Choose between interface and type for a given piece of code
  • Extend an interface, and mark properties readonly or optional
  • Type a dictionary-like object with an index signature
  • Explain why an object literal is checked more strictly than a variable of the same shape
01

Interface basics

An interface describes the shape of an object — the property names and their types — without saying anything about where the object came from. TypeScript uses structural typing (sometimes called duck typing): any object with the right properties satisfies the interface, whether or not it was ever declared with that interface in mind. This is different from languages like Java or C#, where a class has to explicitly say it implements an interface.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
Ada is 36
Grace is 40
Your turn
Add an email: string property to the User interface, then fix ada so it still satisfies the type.
Error you will hit

TS2741: a required property is missing

typescript
interface User {
  name: string
  age: number
}

const bad: User = { name: "Ada" }
app.ts:6:7 - error TS2741: Property 'age' is missing in type '{ name: string; }' but required in type 'User'.

6 const bad: User = { name: "Ada" }
        ~~~
Why the compiler said that

Structural typing checks that an object has every property the interface requires, with a compatible type for each one. bad is missing age entirely, so its shape does not match User.

The fix

Add the missing property.

typescript
const bad: User = { name: "Ada", age: 36 }
Structural typing
A type system that checks compatibility by comparing shape (property names and types) rather than by name or explicit declaration.
Duck typing
"If it walks like a duck and quacks like a duck…" — the informal name for structural typing: an object qualifies by having the right members, not by declaring an identity.
02

interface vs type: when each is idiomatic

Both interface and type can describe an object shape, and for plain object shapes they are nearly interchangeable. The differences that decide which one to reach for: an interface can be reopened and extended — declared again with more members, which get merged — and it uses extends for inheritance; a type alias can represent things an interface cannot, like a union ("a" | "b"), a tuple, or a mapped type, but once declared it is final — you cannot declare the same type name twice.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
true 3
Your turn
Add a third interface Config { timeout: number } declaration and include timeout in cfg.

interface

  • Can be declared multiple times — TypeScript merges the members (declaration merging)
  • Extends another interface (or several) with the extends keyword
  • Can only describe object / function shapes
  • The conventional choice for public API shapes: props, class contracts, library types consumers might extend

type

  • Can only be declared once — a second declaration with the same name is an error
  • Combines with & (intersection) rather than extends, and can reference a union
  • Can describe anything: unions, tuples, primitives, mapped and conditional types
  • The conventional choice for unions, tuples, and any composed or derived type
Error you will hit

TS2300: redeclaring a type alias

typescript
type Config = { debug: boolean }
type Config = { retries: number }
app.ts:2:6 - error TS2300: Duplicate identifier 'Config'.

2 type Config = { retries: number }
       ~~~~~~
Why the compiler said that

Unlike an interface, a type alias is not mergeable — the name Config can only be bound once in a given scope, so the second declaration collides with the first instead of adding to it.

The fix

Combine the two into a single type declaration, or use an intersection if you want to compose them.

typescript
type Config = { debug: boolean } & { retries: number }
NeedReach for
A plain object shapeEither — pick whichever your team defaults to
Something other code might extend laterinterface
A union, tuple, or mapped typetype
Merging declarations from multiple files (e.g. augmenting a library's types)interface — this is exactly what declaration merging is for
JuniorHow do you decide between interface and type when you just need to describe a plain object?

For a plain object shape either works identically in practice, so most teams pick one as a house default for consistency — interface is the more common default because it reads slightly clearer for object contracts and supports extension later. The decision actually matters once the shape needs something type-only can do (unions, tuples, mapped types) or something interface-only can do (declaration merging, extends).

What they are really testing: Whether the candidate treats this as a real tradeoff rather than a dogmatic rule, and knows the cases that actually force one or the other.

03

Extending an interface

An interface can extend one or more other interfaces, inheriting all of their members and adding its own. The extending interface must be compatible with everything it extends — if it redeclares an inherited property, the new type has to be assignable to the original, not just any type.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
Rex Labrador
Here is Rex
Your turn
Add a second interface, Cat extends Animal with its own indoor: boolean, and pass a Cat to announce too.
Error you will hit

TS2430: an incompatible property override

typescript
interface Animal {
  name: string
}
interface Dog extends Animal {
  name: number
}
app.ts:4:11 - error TS2430: Interface 'Dog' incorrectly extends interface 'Animal'.
  Types of property 'name' are incompatible.
    Type 'number' is not assignable to type 'string'.

4 interface Dog extends Animal {
            ~~~
Why the compiler said that

Every Dog is supposed to also be usable as an Animal — that is the whole point of extending it. If Dog.name were a number, code written against Animal.name: string would break the moment it received a Dog, so the compiler refuses the redeclaration outright.

The fix

Keep the inherited property type the same, or add a differently-named property instead of overriding the inherited one incompatibly.

typescript
interface Animal {
  name: string
}
interface Dog extends Animal {
  breed: string
}
Any Dog can go where an Animal is expected
This is exactly what happened when rex — typed Dog — was passed into announce(animal: Animal) above. Because Dog has everything Animal has (and more), it satisfies the narrower type. This is structural typing again, applied to inheritance.
04

readonly properties

Marking a property readonly means it can be set when the object is first created, but never reassigned afterward. It is a compile-time-only guarantee — JavaScript objects have no real immutability, so readonly is the type checker refusing to compile an assignment, not a runtime lock. It is also shallow: a readonly property that holds an object does not make that nested object's own properties readonly.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
0 0
5
Your turn
Add a third readonly property, label: string, to Point and include it in both object literals.
Error you will hit

TS2540: reassigning a readonly property

typescript
interface Point {
  readonly x: number
  readonly y: number
}
const origin: Point = { x: 0, y: 0 }
origin.x = 10
app.ts:6:1 - error TS2540: Cannot assign to 'x' because it is a read-only property.

6 origin.x = 10
  ~~~~~~~
Why the compiler said that

x is declared readonly on the Point interface, which forbids any assignment to it after the object is constructed — including inside the same file that created it.

The fix

Build a new object with the changed value instead of mutating the existing one.

typescript
const moved = { ...origin, x: 10 }
console.log(moved.x)
readonly is shallow
interface Box { readonly items: string[] } stops you reassigning box.items to a new array, but it does not stop box.items.push("x") — the array itself is still fully mutable. For a deeply immutable array, the property type needs to be readonly string[], not just the outer property.
05

Optional properties (?)

A property marked with ? may be omitted entirely when constructing an object of that type. Reading it back gives a type unioned with undefinedtimeout?: number means options.timeout has type number | undefined — so you generally need a fallback, most often with the ?? operator, before using it as a plain number.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
Connecting with timeout 3000
Connecting with timeout 500
Your turn
Use options.retries the same way, with a default of 3, and log it too.
Error you will hit

TS2532: using an optional property without a guard

typescript
interface Options {
  label?: string
}
function shout(options: Options) {
  console.log(options.label.toUpperCase())
}
app.ts:5:23 - error TS2532: Object is possibly 'undefined'.

5   console.log(options.label.toUpperCase())
                        ~~~~~
Why the compiler said that

Because label is optional, its type is string | undefined. Calling .toUpperCase() directly assumes it is always present, but the type says it might not be — the compiler will not let that assumption through unchecked.

The fix

Guard it first, with a fallback or a conditional.

typescript
function shout(options: Options) {
  console.log((options.label ?? "").toUpperCase())
}
Optional property
A property that may be omitted from an object of that type, declared with a trailing ?. Its type when read is unioned with undefined.
Mid-levelWhat does readonly actually prevent, and what does it not prevent?

readonly prevents reassigning the property itself after the object is created — origin.x = 10 fails to compile. It is compile-time only (JavaScript has no real immutable properties, so nothing stops it at runtime through, say, a type assertion) and it is shallow: if the property holds an array or object, readonly stops you swapping that array/object out, but the contents inside it are still fully mutable unless their own type says otherwise, like readonly string[].

What they are really testing: Whether the candidate knows readonly is a type-checker guarantee, not a runtime one, and that it does not go deep automatically.

06

Index signatures: [key: string]: number

An index signature types an object whose exact keys are not known ahead of time, but whose values all share one type — a dictionary. [player: string]: number means: whatever string key someone uses, the value at that key is a number. The name inside the brackets (player) is just documentation; only the key type (string) and value type (number) are checked.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
90 95
ada, grace, linus
270
Your turn
Add a fourth entry, scores.newcomer = 70, before the loop, and predict the new total before running it.
VisualizeSumming a dictionary-typed object with for...inStep 1 / 7
const scores = { ada: 90, grace: 85, linus: 95 }
let total = 0
for (const player in scores) {
total += scores[player]
}
console.log(total)
Line 1

Bind scores to an object with three entries.

Variables now
scores{ ada: 90, grace: 85, linus: 95 }
All 7 steps as a table
StepLineWhat happenedVariables now
11Bind scores to an object with three entries.scores = { ada: 90, grace: 85, linus: 95 }
22Bind total to 0.total = 0
33for...in iterates the object's own enumerable keys, one per pass, in insertion order: "ada", then "grace", then "linus".
44First pass: player is "ada". Add scores["ada"], which is 90, to total.player = 'ada' total = 90
54Second pass: player is "grace". Add scores["grace"], which is 85, to total.player = 'grace' total = 175
64Third pass: player is "linus". Add scores["linus"], which is 95, to total.player = 'linus' total = 270
76The loop has no more keys — print the final total.
Error you will hit

TS2322: a value that does not match the index signature

typescript
interface Scores {
  [player: string]: number
}
const scores: Scores = { ada: "ninety" }
app.ts:4:32 - error TS2322: Type 'string' is not assignable to type 'number'.

4 const scores: Scores = { ada: "ninety" }
                               ~~~~~~~~
Why the compiler said that

Every value in a Scores object must be a number, regardless of which key it sits under — that is the whole promise an index signature makes. "ninety" is a string, so it violates that promise the same way any other property with the wrong type would.

The fix

Use a real number.

typescript
const scores: Scores = { ada: 90 }
SituationRight tool
Keys are known ahead of time (name, age, email)Named properties
Keys are arbitrary, only the value type is fixedAn index signature, [key: string]: V
Both a few known properties and arbitrary extra keysNamed properties plus an index signature, as long as every named property's type is compatible with the index signature's value type
07

Excess property checks

Structural typing normally only checks that an object has at least the required properties — extra ones are fine, because anything with the right shape is compatible. But when you pass a fresh object literal directly where a typed value is expected, TypeScript runs a stricter check on top: it also flags any property the literal has that the target type does not. This is an excess property check, and it exists specifically to catch typos — a misspelled optional property would otherwise silently vanish with no warning at all.

typescriptEdit it. ⌘/Ctrl + Enter runs.
You should see
1 2
ok
Your turn
Now inline the same object directly into the call — logPoint({ x: 1, y: 2, z: 3 }) — instead of passing the extra variable. The next block shows what a real compiler does with that.
Error you will hit

TS2345: excess property check on an object literal

typescript
interface Point {
  x: number
  y: number
}
function logPoint(p: Point) {
  console.log(p.x, p.y)
}
logPoint({ x: 1, y: 2, z: 3 })
app.ts:7:10 - error TS2345: Argument of type '{ x: number; y: number; z: number; }' is not assignable to parameter of type 'Point'.
  Object literal may only specify known properties, and 'z' does not exist in type 'Point'.

7 logPoint({ x: 1, y: 2, z: 3 })
           ~~~~~~~~~~~~~~~~~~~
Why the compiler said that

Because this exact literal is written right at the call site, TypeScript can see its full shape and knows z has no reason to be there — most likely a typo for an existing property, or a leftover field that does not belong. It only applies this strict check to a literal written in place; once a value is behind a variable, that same reasoning does not apply.

The fix

Remove the extra property, or add it to the Point interface if it genuinely belongs there.

typescript
logPoint({ x: 1, y: 2 })
Quick check

Why did logPoint(extra) compile fine above, while logPoint({ x: 1, y: 2, z: 3 }) does not — even though extra holds the exact same shape?

This is a feature, not an inconsistency
If excess property checks applied everywhere, a huge amount of ordinary code — like passing a full user record into a function that only needs a couple of its fields — would stop compiling. Limiting the strict check to fresh literals gets the typo-catching benefit exactly where it is cheap and useful, without breaking structural typing everywhere else.
SeniorWhy does TypeScript only run excess property checks on object literals and not through a variable — is this a hole in the type system?

It is a deliberate, narrow exception, not a hole. Structural typing's whole value is that a wider-shaped object can be used anywhere a narrower type is expected — that is how passing a full record into a function needing only some of its fields works. If excess property checks applied through variables too, that pattern would break across the board. Limiting the strict check to literals written directly at the call site targets exactly the case where a typo is likely and the full shape is visible to the compiler, without sacrificing structural typing's flexibility everywhere else.

What they are really testing: Whether the candidate can defend the design rather than just describe the behavior — a strong signal of real structural-typing understanding.

Mid-levelWhen is an index signature the right tool versus just adding named properties to an interface?

Named properties are right when the set of keys is known ahead of time and each one might reasonably have a different type. An index signature is right when the keys are not known in advance — user-supplied identifiers, arbitrary configuration keys — but every value under those keys shares one type. The two can combine, as long as any explicitly named property's type is still compatible with the index signature's value type.

What they are really testing: Whether the candidate reaches for the simplest fitting tool rather than defaulting to an index signature out of habit.

Finish the TypeScript handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.