Skip to content
aviral gupta

// B2.4 · ~28 min · Beginner

Type aliases versus interfaces

After this lesson you can name a type once with type or interface, build one type on another, and choose between the two for a good reason.

Lesson 4 of 5 in B2 Everyday types

You will be able to

  • Name an object type with a type alias or an interface, and use the name in annotations
  • Build a type on another: an interface with extends, a type alias with &
  • Explain the key difference, declaration merging, and choose between type and interface
  1. Warm-up · Activity 1 of 7

    From the last lesson: which of these are true of object types such as { name: string; email?: string }? Pick all that apply.

    Select all that apply.

  2. Predict · Activity 2 of 7

    Predict before you read on: a product id is stored in a variable meant for a user id. What does tsc report?

    type UserId = string;
    type ProductId = string;
    
    let current: UserId = "u-17";
    const product: ProductId = "p-9";
    current = product;
    console.log(current);
  3. Practice · Activity 3 of 7

    Fill in the keyword that makes Dog have every property of Animal, so that tsc accepts rex and the program prints Rex the Collie.

    interface Animal {
      name: string;
    }
    
    interface Dog ____ Animal {
      breed: string;
    }
    
    const rex: Dog = { name: "Rex", breed: "Collie" };
    console.log(rex.name + " the " + rex.breed);
    interface Dog Animal {
  4. Practice · Activity 4 of 7

    Person is an intersection of two aliases. What does tsc report first?

    type Named = { name: string };
    type Aged = { age: number };
    type Person = Named & Aged;
    
    const ada: Person = { name: "Ada" };
  5. Practice · Activity 5 of 7

    Match each declaration to what it does.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. Box is declared twice, and box has only a width. What does tsc report?

    interface Box {
      width: number;
    }
    
    interface Box {
      height: number;
    }
    
    const box: Box = { width: 2 };
  7. Apply · Activity 7 of 7

    Mini-task. Declare an interface Person with a name and an optional email, and an interface Employee that extends Person with a team. Write badge(employee: Employee), which returns text such as Ada (Compilers), and call it once. Then write Employee as a type alias with & instead, and check that tsc still accepts the same code.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Named types for a small catalogue

The catalogue holds books and films. ID is a type alias that accepts a number or a string. Item is an interface with the properties both kinds share. Book extends Item; Film is a type alias that combines Item with & instead. Each function names the type once in its signature. Run npx tsc, then node main.ts. Then leave out pages in the last call, or declare interface Item a second time with one more property, and read what tsc says.

main.ts

// Named types for a small library catalogue.
type ID = number | string;

interface Item {
  id: ID;
  title: string;
}

interface Book extends Item {
  author: string;
  pages: number;
}

type Film = Item & {
  minutes: number;
};

function describeBook(book: Book): string {
  return book.id + ": " + book.title + " by " + book.author + ", " + book.pages + " pages";
}

function describeFilm(film: Film): string {
  return film.id + ": " + film.title + ", " + film.minutes + " min";
}

const dune: Book = { id: 1, title: "Dune", author: "Frank Herbert", pages: 412 };
const metropolis: Film = { id: "F-7", title: "Metropolis", minutes: 153 };
console.log(describeBook(dune));
console.log(describeFilm(metropolis));
console.log(describeBook({ id: 2, title: "Emma", author: "Jane Austen", pages: 474 }));

Run it with

npx tsc
node main.ts

Output

1: Dune by Frank Herbert, 412 pages
F-7: Metropolis, 153 min
2: Emma by Jane Austen, 474 pages
  • dune has a number id and metropolis a string id: ID accepts both.
  • Book and Film each get id and title from Item, one through extends, one through &.
  • The last call passes an object literal straight to describeBook; it is checked against Book like any other.
  • The names exist only for tsc: Node.js strips them, and the output is what plain JavaScript prints.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

Name the type both functions use

Both functions take a Product, but tsc reports Cannot find name 'Product'. Declare Product above them, as an interface or a type alias, with a name (string) and a price (number). Change nothing in the functions. Run the tests: the first one is the type-check.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Both diagnostics are the same: the name Product is used but never declared.

  2. Hint 2

    An interface has no =: interface Product { … }. A type alias has one: type Product = { … };

  3. Hint 3

    interface Product { name: string; price: number; } above the first function is enough.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

interface Product {
  name: string;
  price: number;
}

export function label(product: Product): string {
  return product.name + ": " + product.price.toFixed(2) + " EUR";
}

export function isCheap(product: Product): boolean {
  return product.price < 5;
}
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export function label(product: Product): string {
  return product.name + ": " + product.price.toFixed(2) + " EUR";
}

export function isCheap(product: Product): boolean {
  return product.price < 5;
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {label, isCheap} from './main.ts';

test('label shows name and price', () => {
  assert.equal(label({name: 'Pen', price: 1.5}), 'Pen: 1.50 EUR', 'label should give Pen: 1.50 EUR');
});

test('isCheap is true below 5', () => {
  assert.equal(isCheap({name: 'Pen', price: 1.5}), true, 'a price of 1.5 should be cheap');
});

test('isCheap is false at 5 and above', () => {
  assert.equal(isCheap({name: 'Mug', price: 8}), false, 'a price of 8 should not be cheap');
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Exercise 2 of 2

Let Dog build on Animal

describe reads name and legs from a Dog, but Dog declares only breed: tsc reports Property 'name' does not exist on type 'Dog', and the same for legs. Do not copy the properties into Dog. Make Dog build on Animal instead, so it has every property of Animal plus breed.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    The JavaScript runs fine: only the type-check fails, because Dog does not know about name and legs.

  2. Hint 2

    One keyword after interface Dog makes it take over all the members of another interface.

  3. Hint 3

    Write interface Dog extends Animal {

Show a solution

One way to solve it. Yours can look different and still pass the checks.

interface Animal {
  name: string;
  legs: number;
}

interface Dog extends Animal {
  breed: string;
}

export function describe(dog: Dog): string {
  return dog.name + " the " + dog.breed + " has " + dog.legs + " legs";
}
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

interface Animal {
  name: string;
  legs: number;
}

interface Dog {
  breed: string;
}

export function describe(dog: Dog): string {
  return dog.name + " the " + dog.breed + " has " + dog.legs + " legs";
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {describe} from './main.ts';

test('describe names the dog, its breed and its legs', () => {
  assert.equal(describe({name: 'Rex', breed: 'Collie', legs: 4}), 'Rex the Collie has 4 legs', 'describe should give Rex the Collie has 4 legs');
});

test('describe works for another dog', () => {
  assert.equal(describe({name: 'Tripod', breed: 'Beagle', legs: 3}), 'Tripod the Beagle has 3 legs', 'describe should give Tripod the Beagle has 3 legs');
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Common mistakes

An = after interface

interface Point = {
  x: number;
  y: number;
};

What tsc or Node.js prints

main.ts(1,17): error TS1005: '{' expected.

Why, and the fix

type and interface are written differently. A type alias needs =: type Point = { x: number; y: number };. An interface has no =, the braces follow the name: interface Point { x: number; y: number }. The message '{' expected points at the =, column 17, where tsc wanted the brace.

The same type alias declared twice

type Settings = { theme: string };
type Settings = { fontSize: number };

What tsc or Node.js prints

main.ts(1,6): error TS2300: Duplicate identifier 'Settings'.

Why, and the fix

A type alias cannot be re-opened, so tsc reports both declarations (lines 1 and 2). Put all the properties into one: type Settings = { theme: string; fontSize: number };. Two interface Settings declarations would merge instead, which is allowed, but a second declaration far away in the file is hard to spot; keep one.

A property from the base interface left out

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

const rex: Dog = { breed: "Collie" };

What tsc or Node.js prints

main.ts(9,7): error TS2741: Property 'name' is missing in type '{ breed: string; }' but required in type 'Dog'.

Why, and the fix

Dog extends Animal, so a Dog needs every property of Animal as well as its own. The message names the missing one, name, and the type that requires it, Dog. Add it: { name: "Rex", breed: "Collie" }.

TypeScript in the browser: the TypeScript 6.0.3 compiler, Apache-2.0, then your browser’s own engine. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

A type alias gives a type a name

type Point = { x: number; y: number }; names an object type, so you write it once and annotate with Point everywhere else. An alias can name any type, not only objects: type ID = number | string names a type that accepts a number or a string (unions come in module B3). An alias is only a name: using it is exactly as if you had written the type out. So after type UserId = string and type ProductId = string, both are plain string, and tsc lets you mix them up. Type names are written in PascalCase by convention, and tsc uses them in its messages: … but required in type 'Point'.

An interface names an object type, and extends builds on one

interface Point { x: number; y: number } is the other way to name an object type: no =, the properties go straight into the braces. An interface can build on another with extends: interface Book extends Item { author: string } copies the members of Item into Book and adds author, and interface C extends A, B {} combines two. A type alias combines types with & instead, an intersection: type Film = Item & { minutes: number } has every property of both sides. Either way, a value of the new type needs all of them, and tsc names the one that is missing. Intersections in depth, and what happens when properties clash, come in module B5.

The key difference, and which one to pick

The two are nearly interchangeable. The Handbook names the key difference: an interface can be re-opened, a type alias cannot. Two interface Box declarations merge into one Box with the properties of both, which is called declaration merging; two type Box declarations are error TS2300: Duplicate identifier 'Box'. Libraries use merging so you can add to types such as Window; in your own code it is more often a surprise. Only a type alias can name a primitive or a union, such as type ID = number | string; interface ID = string is a syntax error. The Handbook's rule of thumb: use interface until you need a feature of type.

Sources

Last reviewed September 30, 2026