---
title: Interfaces
source: 'https://academia.sh/en/courses/typescript/interfaces'
course: TypeScript
language: en
updated: '2026-08-17T18:09:53+00:00'
license: 'CC BY-SA 4.0'
---

# Interfaces

Defining a structural contract with an interface, optional and readonly fields, extension, index signatures, and declaration merging.

The previous topic established the types of individual values. This topic moves on to
naming and sharing those types: how is the contract a module exposes written, and how
are different implementations that satisfy the same contract defined?

An **interface** is a declaration that names the fields and methods a value has to
carry. It is the direct counterpart of the **abstract data type** idea from the Data
Structures course: a stack is defined not by its internal representation but by its
`push`, `pop`, `peek`, and `is_empty` operations. An interface likewise defines a type
by the operations it offers, not by its internal structure.

## Interface Declaration

The measurement record's model can be written as an interface:

```typescript
type Unit = "C" | "Pa" | "%";

interface Measurement {
  readonly id: string;
  sensor: string;
  value: number;
  unit: Unit;
  time: number;
  note?: string;
}

const record: Measurement = {
  id: "s-01",
  sensor: "temperature",
  value: 21.4,
  unit: "C",
  time: 1706000000000,
};

console.log(record.note);
record.value = 22.1;
record.id = "s-02";
```

```text
g1.ts(22,8): error TS2540: Cannot assign to 'id' because it is a read-only property.
```

Two new markers appear in the declaration.

The **`readonly id`** field blocks assignment at compile time: the `value` assignment
on line 21 is valid, the one on line 22 is not. The boundary from the previous lesson
holds here too — `readonly` is erased at runtime and does not protect the object
itself.

The **`note?: string`** field is declared **optional**. Assignment is accepted even
though it is missing from the object literal; accessing `record.note` has the type
`string | undefined` and has to be narrowed before it can be used directly. Line 20
only prints it, so nothing goes wrong there — its output is `undefined`.

An optional field carries the "this information may not be present" case up to the
type level. The alternative — making the field required and putting an empty string
there — loses information: an empty string could mean "not measured" or "the note was
empty."

## Extension

An interface can inherit all of another interface's requirements:

```typescript
interface Measurement {
  id: string;
  value: number;
}

interface LocatedMeasurement extends Measurement {
  lat: number;
  lon: number;
}

const record: LocatedMeasurement = {
  id: "s-01",
  value: 21.4,
  lat: 41.0,
  lon: 29.0,
};
console.log(record.id, record.lat);

const missing: LocatedMeasurement = { id: "s-02", value: 22.1 };
```

```text
g2.ts(19,7): error TS2739: Type '{ id: string; value: number; }' is missing the following properties from type 'LocatedMeasurement': lat, lon
```

The `extends` syntax produces the same result as an intersection type: the
`LocatedMeasurement` type requires both its own fields and `Measurement`'s fields.
There are two differences.

First, `extends` checks compatibility **at declaration time**:

```typescript
interface Base {
  unit: string;
}

interface Derived extends Base {
  unit: number;
}

declare const x: Derived;
console.log(x.unit);
```

```text
g6.ts(5,11): error TS2430: Interface 'Derived' incorrectly extends interface 'Base'.
  Types of property 'unit' are incompatible.
    Type 'number' is not assignable to type 'string'.
```

The diagnostic points to the declaration where the conflict lives. The same conflict
written with an intersection type would silently collapse to `never`, as seen in the
previous lesson, with the error surfacing only at the point of use.

Second, the `extends` chain appears by name in diagnostic messages and improves
readability.

This inheritance is separate from the class inheritance in the Programming
Fundamentals course: what is inherited here is not behavior, only the contract. An
interface contains no implementation.

## Declaration Merging

Interfaces have a property type aliases lack: they can be declared more than once
under the same name. The declarations get merged.

```typescript
interface Measurement {
  id: string;
}

interface Measurement {
  value: number;
}

const record: Measurement = { id: "s-01", value: 21.4 };
console.log(record.id, record.value);

const missing: Measurement = { id: "s-02" };
```

```text
g3.ts(12,7): error TS2741: Property 'value' is missing in type '{ id: string; }' but required in type 'Measurement'.
```

The two declarations formed a single interface; the twelfth line being rejected proves
it.

This looks like a convenience, but its real purpose is different: it enables
**extending an existing type from the outside**. A field can be added to an interface
a library defines without modifying that library. The Declaration Files lesson shows
this capability's real use case.

The same capability also carries a risk: an interface's full definition may not be
visible in a single file. Relying on declaration merging in your own code makes the
definition harder to trace.

## Index Signatures

For cases where field names are not known in advance, an **index signature** is
written:

```typescript
interface TaggedMeasurement {
  id: string;
  value: number;
  [tag: string]: string | number;
}

const record: TaggedMeasurement = {
  id: "s-01",
  value: 21.4,
  location: "boiler-2",
  shift: 3,
};

console.log(record.location, record["shift"]);
console.log(record.missingTag);
```

This file passes type checking. Its output:

```text
boiler-2 3
undefined
```

Two rules are at work. First, with an index signature present, the named fields'
types **have to fit** the signature's type; `id: string` and `value: number` are
accepted because they fall inside the `string | number` union. A `boolean` field
would trigger a diagnostic.

Second, and more importantly: **an index signature does not treat access to a
nonexistent key as an error.** The `record.missingTag` expression on the last line has
type `string | number` as far as the compiler is concerned, and `undefined` at
runtime. Here the type system makes a promise beyond reality.

A compiler option closes this gap — a strictness flag that adds `undefined` to the
result of indexed access — covered in the Compiler Configuration lesson. General rule:
if the key set is known, writing the fields out individually or using a mapped type is
safer than an index signature.

## Interface as a Contract

An interface's real power shows up less in defining a data shape and more in
establishing a **behavioral contract**. The next step for the measurement record model
is to abstract away where records come from:

```typescript
type Measurement = { id: string; value: number };

interface MeasurementSource {
  readonly name: string;
  next(): Measurement | null;
}

function arraySource(name: string, records: readonly Measurement[]): MeasurementSource {
  let index = 0;
  return {
    name,
    next() {
      return index < records.length ? records[index++] : null;
    },
  };
}

function constantSource(name: string, value: number): MeasurementSource {
  return {
    name,
    next() {
      return { id: name, value };
    },
  };
}

function firstTwo(source: MeasurementSource): string {
  const a = source.next();
  const b = source.next();
  return `${source.name}: ${a?.value ?? "-"} ${b?.value ?? "-"}`;
}

console.log(firstTwo(arraySource("file", [{ id: "s-01", value: 21.4 }])));
console.log(firstTwo(constantSource("constant", 20)));
```

Output:

```text
file: 21.4 -
constant: 20 20
```

`firstTwo` knows neither source; it knows only the `MeasurementSource` contract. This
is the type-level counterpart of the **interface–implementation** distinction from the
Programming Fundamentals course: the contract and the code satisfying it can change
independently.

Worth noticing: no implementation writes the name `MeasurementSource`. The object
literal `arraySource` returns carries no declaration saying "this is a measurement
source"; it is accepted only because it has the required fields. This is the
foundation of TypeScript's type compatibility, spelled out in the Structural Type
Compatibility lesson.

The return type being `Measurement | null` is also a design decision. The `a?.value`
and `?? "-"` syntax inside `firstTwo` has to handle that union; the compiler does not
allow the `null` possibility to be skipped.

## Summary

- An interface names the fields and methods a value has to carry; it is the
  type-level counterpart of the abstract data type idea.
- `readonly` fields block assignment at compile time; optional fields carry the
  possibility of `undefined` into the type.
- The `extends` syntax merges contracts and checks conflicts at declaration time; it
  does not pass on implementation.
- Interface declarations under the same name get merged; this capability is for
  extending external types, and it makes tracing a definition harder in your own
  code.
- An index signature types unknown keys but does not treat access to a nonexistent
  key as an error; if the key set is known, the fields are written out individually.

## Next Step

The same measurement model could also have been written with the `type` keyword
instead of `interface` — and indeed, that is how it was written in the previous topic.
The two declaration forms are interchangeable in most cases, but they are not exactly
equivalent. The next lesson will list the differences and build a criterion for which
one to choose in which situation.
