---
title: 'Type Aliases'
source: 'https://academia.sh/en/courses/typescript/type-aliases'
course: TypeScript
language: en
updated: '2026-08-17T18:09:54+00:00'
license: 'CC BY-SA 4.0'
---

# Type Aliases

The differences between a type declaration and an interface: naming any type, the absence of declaration merging, circular-reference rules, a class contract written with a type alias, and a selection criterion.

The same measurement model was written with `type` in the previous topic and with
`interface` in this topic's first lesson. The two declaration forms are interchangeable
in most cases; they are not equivalent.

A **type alias** is a declaration that gives a name to an existing type. It does not
create a new type — after `type Id = string`, `Id` and `string` are the same type and
interchangeable. An interface, on the other hand, **defines** a named object type. This
lesson establishes where the difference shows up and when to choose which.

## Naming Any Type

An alias can name any type. An interface can only define object and callable shapes:

```typescript
type Unit = "C" | "Pa" | "%";
type Id = string;
type Interval = [low: number, high: number];
type Converter = (value: number) => number;

const unit: Unit = "C";
const interval: Interval = [-40, 85];
const toKelvin: Converter = (v) => v + 273.15;

console.log(unit, interval, toKelvin(21.4));
```

Output:

```text
C [ -40, 85 ] 294.54999999999995
```

None of these four can be written with an interface. A union, a primitive type, and a
tuple are not object types; a function type can be written with an interface, but it
needs much longer syntax. The mapped, conditional, and template literal types from the
next topic can likewise only be named with an alias.

Notice the output's last value: `21.4 + 273.15` gives `294.54999999999995`, not
`294.55`. The type system correctly counted the `number` type; floating-point
representation's precision limit is not its concern. This is a concrete example of the
"what it does not prove" list from the first lesson.

## No Declaration Merging

Interfaces could be redeclared under the same name and merged. Type aliases have no
such thing:

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

```text
h2.ts(1,6): error TS2300: Duplicate identifier 'Measurement'.
h2.ts(2,6): error TS2300: Duplicate identifier 'Measurement'.
```

The diagnostic appears twice, because both declarations are party to the conflict.

This is not a limitation of the alias but a guarantee: a type alias's definition is
complete where it is written. It is impossible for another file to add a field to that
type. In your own codebase's data models, this predictability is usually what you want.

In the other direction, an interface can be derived from a type alias through
**extension**:

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

interface TimedMeasurement extends NumericMeasurement {
  time: number;
}

const m: TimedMeasurement = { id: "s-01", value: 21.4, time: 1 };
console.log(m);
```

This file compiles without error. But if the alias does not name an object type,
extension fails:

```typescript
type NumericMeasurement = { id: string; value: number };
type TextMeasurement = { id: string; text: string };
type Entry = NumericMeasurement | TextMeasurement;

interface Extended extends Entry {
  time: number;
}
```

```text
h7.ts(5,28): error TS2312: An interface can only extend an object type or intersection of object types with statically known members.
```

Extending a union has no meaning: which member `Extended` inherits from is ambiguous.
Writing the same operation with an intersection, though, is valid — `type Extended =
Entry & { time: number }` produces a union that adds a `time` field to both members.

## Circular Reference

When measurement records turn into a nested structure, the type has to refer to
itself. A reference inside an object field is valid:

```typescript
type MeasurementNode = {
  id: string;
  value: number;
  children: MeasurementNode[];
};

const root: MeasurementNode = {
  id: "boiler-2",
  value: 21.4,
  children: [{ id: "s-01", value: 21.4, children: [] }],
};

function sumNodes(node: MeasurementNode): number {
  return 1 + node.children.reduce((t, c) => t + sumNodes(c), 0);
}

console.log(sumNodes(root));
```

The file compiles without error; its output is `2`.

A **direct** circular reference, by contrast, is rejected:

```typescript
type Chain = Chain | null;
declare const z: Chain;
console.log(z);
```

```text
h3.ts(1,6): error TS2456: Type alias 'Chain' circularly references itself.
```

The distinction: the `MeasurementNode` reference sits inside an object field, and the
compiler resolves that field's type only when needed. The `Chain` declaration, though,
needs itself again to resolve its own definition — an expansion that never terminates.
The rules for recursive type definitions will be covered in detail in the Advanced
Types topic.

## A Class Contract Written with a Type Alias

The contract a class satisfies can also be written with a type alias. As long as the
alias names an object type, the `implements` declaration works exactly as it does with
an interface:

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

class FileRecord implements Measurement {
  constructor(
    readonly id: string,
    readonly value: number,
  ) {}
}

const record: Measurement = new FileRecord("s-01", 21.4);
console.log(record.id, record.value);
```

Output is `s-01 21.4`.

The limit is the same as with extension: if the alias names a union, it cannot be
implemented.

```typescript
type NumericMeasurement = { id: string; value: number };
type TextMeasurement = { id: string; text: string };
type Entry = NumericMeasurement | TextMeasurement;

class FileRecord implements Entry {
  constructor(readonly id: string) {}
}
```

```text
u2.ts(5,29): error TS2422: A class can only implement an object type or intersection of object types with statically known members.
```

The reasoning is the same: which member the class satisfies is ambiguous. A class
being "either this shape or that one" cannot be expressed with a single declaration.
In such a model, the contract is satisfied by separate classes for each union member,
and the union is only used at the value level.

## Selection Criterion

The differences between the two declaration forms can be gathered into a table:

| Criterion | `interface` | `type` |
|---|---|---|
| Defines an object type | Yes | Yes |
| Names a union, tuple, primitive | No | Yes |
| Redeclaration under the same name | Merges | `TS2300` |
| Extension | `extends` (checked at declaration) | `&` (silently collapses) |
| Implemented by a class | `implements` | `implements` (if object type) |
| Mapped, conditional type | No | Yes |

An actionable criterion follows from the table:

**An interface for contracts shaped like an object.** A data record's fields, the
methods a service offers, the contract a class satisfies. Because extension is
checked, errors are caught at declaration time; diagnostic messages carry the name.

**A type alias for everything else.** Unions, discriminated unions, tuples, function
types, types computed at the type level. These either have no interface equivalent or
forcing one is awkward.

In this course, the measurement record itself is written with an interface, and its
statuses and derived types are written with type aliases:

```typescript
interface Measurement {
  readonly id: string;
  sensor: string;
  value: number;
  unit: "C" | "Pa" | "%";
  time: number;
}

type MeasurementStatus =
  | { status: "valid"; measurement: Measurement }
  | { status: "failed"; id: string; code: string };

function summarize(record: MeasurementStatus): string {
  return record.status === "valid"
    ? `${record.measurement.id}=${record.measurement.value}${record.measurement.unit}`
    : `${record.id}: ${record.code}`;
}

console.log(
  summarize({
    status: "valid",
    measurement: {
      id: "s-01",
      sensor: "temperature",
      value: 21.4,
      unit: "C",
      time: 1706000000000,
    },
  }),
);
console.log(summarize({ status: "failed", id: "s-02", code: "E17" }));
```

Output:

```text
s-01=21.4C
s-02: E17
```

The division of labor between the two declaration forms shows here: `Measurement` is a
record's shape and can grow; `MeasurementStatus` is a closed set of options, and its
growth should be a deliberate decision.

## Summary

- A type alias names an existing type; it can name any type, including unions,
  tuples, primitives, and function types.
- Interfaces get redeclared under the same name and merge; a type alias's second
  declaration produces a `TS2300` diagnostic, which guarantees the definition is
  complete in one place.
- An interface can extend a type alias that names an object type; it cannot extend a
  union type.
- A circular reference inside an object field is valid; a directly circular type
  alias produces a `TS2456` diagnostic.
- A type alias naming an object type can be implemented with `implements`; one
  naming a union produces a `TS2422` diagnostic.
- Criterion: object-shaped contracts are written with an interface; unions and types
  computed at the type level are written with a type alias.

## Next Step

The contracts so far have defined only data and operation signatures; the
implementation came from a separate function each time. Classes hold the two
together, and TypeScript adds visibility control to classes. The next lesson takes up
access modifiers, abstract classes, and what they leave behind in the compiled output.
