Interfaces page

Learn how to write Interfaces, set optional properties, and about the power of using interfaces in classes and functions.

Overview

In this section, you will:

  • Write interfaces
  • Set optional properties
  • Use the power of interfaces in classes and functions

Objective 1: Define interfaces for objects

Interfaces are a powerful way to enforce types and document what our code provides. Interfaces used in classes allow "loose coupling" while providing a shape. Multiple classes can use interfaces in many different ways. This section will cover how to write interfaces, setting optional properties, and the power of using interfaces in classes and functions.

Interfaces in TypeScript

An interface in TypeScript is a way to define the shape an entity should adhere to. An interface defines the members: properties, methods, and events. It may be easy to think of it as the signature of an API. It’s worth noting that interfaces aren’t transpiled into our output JavaScript; they’re only used for type-checking during the development process.

//interface describing object
interface Dinosaur {
  name: string;
  breed: string;
  height: number;
  location: string;
}

//function with interface describing parameter
function trackDino(dino: Dinosaur) {
  console.info(dino.location);
}

const blue = {
  name: 'blue',
  breed: 'Velociraptor',
  height: 7,
  location: 'Section B'
};
trackDino(blue);
//Logs "Section B"

Make sure you have this solution implemented correctly before moving on to the next exercise.

Optional properties

Sometimes all properties on an object don’t need to be required, so using the ? tells the TypeScript compiler which properties aren’t required.

interface Dinosaur {
  name: string;
  breed: string;
  height?: number;
  location: string;
}

function trackDino(dino: Dinosaur) {
  console.info(dino.location);
}

const blue = {name: 'blue', breed: 'Velociraptor', location: 'Section B'};
//works
trackDino(blue);
//Logs "Section B"

Setup 1

✏️ Create src/interfaces/address.ts and update it to be:

interface Address {}

export default Address;

✏️ Create src/interfaces/dino-park.ts and update it to be:

import Address from "./address";

interface DinoPark {}

export default DinoPark;

Verify 1

✏️ Create src/interfaces/address.test.ts and update it to be:

import assert from "node:assert";
import { describe, it } from "node:test";
import type Address from "./address";

describe("Interfaces", () => {
  function checkAddress(address: Address) {
    var keys = Object.keys(address);
    assert.deepEqual(keys, ["street", "city", "state", "zip"]);
  }

  it("Address", () => {
    checkAddress({
      street: "123 Main",
      city: "Sandusky",
      state: "Ohio",
      zip: "12345",
    });
  });
});

✏️ Create src/interfaces/dino-park.test.ts and update it to be:

import assert from "node:assert";
import { describe, it } from "node:test";
import type DinoPark from "./dino-park";
import type Address from "./address";

describe("Interfaces", () => {
  function checkAddress(address: Address) {
    var keys = Object.keys(address);
    assert.deepEqual(keys, ["street", "city", "state", "zip"]);
  }

  it("DinoPark", () => {
    function checkFullDinoPark(dinoPark: DinoPark) {
      var keys = Object.keys(dinoPark);
      assert.deepEqual(keys, ["name", "image", "address"], "has an image");
      checkAddress(dinoPark.address);
    }

    checkFullDinoPark({
      name: "Isla Sorna Park",
      image: "http://dino.com/pic.jpg",
      address: {
        street: "123 Main",
        city: "Sandusky",
        state: "Ohio",
        zip: "12345",
      },
    });

    function checkPartialDinoPark(dinoPark: DinoPark) {
      var keys = Object.keys(dinoPark);
      assert.deepEqual(keys, ["name", "address"], "optional image");
      checkAddress(dinoPark.address);
    }
    checkPartialDinoPark({
      name: "Isla Sorna Park",
      address: {
        street: "123 Main",
        city: "Sandusky",
        state: "Ohio",
        zip: "12345",
      },
    });
  });
});

Exercise 1

We’re going to write some interfaces to set up for the next problem. Edit the files address.ts and dino-park.ts to create an interface to define an Address and DinoPark object shown below:

const address = {
  street: '',
  city: '',
  state: '',
  zip: ''
};

const park = {
  name: '',
  image: '',
  address: {
    street: '',
    city: '',
    state: '',
    zip: ''
  }
};

Hint: the interface should have properties and types:

  • name (string)
  • image (string) (optional)
  • address
    • street (string)
    • city (string)
    • state(string)
    • zip (string)

Have issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.

Solution 1

Click to see the solution

✏️ Update address.ts to the following:

interface Address {
  street: string;
  city: string;
  state: string;
  zip: string
}

export default Address;

✏️ Update dino-park.ts to the following:

import Address from "./address";

interface DinoPark {
  name: string;
  image?: string;
  address: Address;
}

export default DinoPark;

Have issues with your local setup? See the solution in StackBlitz or CodeSandbox.

Make sure you have this solution implemented correctly before moving on to the next exercise.

Classes implementing interfaces

In the case that a class needs to follow an object structure, we can use interfaces to define that 'contract'.

interface Dinosaur {
  name: string;
  breed: string;
  height?: number;
  location: string;
}


class DinoWithRoar implements Dinosaur {
  name: string;
  breed: string;
  height?: number;
  location: string;
  roar(): void {
    console.info('roar');
  };
}

Type assertion

We briefly mentioned type assertion when talking about types, but when dealing with interfaces, it can be a great tool for making sure our code behaves in the way we expect.

For instance, consider the following code:

const myObj = {};
myObj.name = 'Blue';
//property 'name' does not exist on type '{}'
myObj.height = 6;
//property 'height' does not exist on type '{}'

When we create empty object literals in TypeScript, they are inferred to be objects with zero properties. To fix this, we can use type assertions to let the compiler explicitly know what we want from our object.

interface Dinosaur {
  name: string;
  height: number;
}

const myObj = {} as Dinosaur;
myObj.name = 'Blue';
myObj.height = 6;

Objective 2: Define interfaces for functions

Interfaces in functions

Interfaces are incredibly useful in describing the shape of objects we want to use in multiple situations. The following functions both require a Dinosaur object shape we’ve defined in the Dinosaur interface.

interface Dinosaur {
  name: string;
  breed: string;
  location: string;
}

const dinoA = {
  name: 'Blue',
  breed: 'Velociraptor',
  location: 'Section B'
};

const dinoB = {
  name: 'Sally',
  location: 'Section C'
};

function dinoCatcher(dinosaur: Dinosaur) {
  console.info(`Caught ${dinosaur.name} at ${dinosaur.location}`);
}

dinoCatcher(dinoA);
//works!
dinoCatcher(dinoB);
//Argument of type '{ name: string; location: string; }' is not assignable to parameter of type 'Dinosaur'.
//Property 'breed' is missing in type '{ name: string; location: string; }'.

Interfaces describing functions

We can also use interfaces to describe functions, basically creating reusable types for functions. On the left side (in parenthesis) we list the parameters, and to the right of the colon, we state the return type.

interface DinoDNAMixer {
  (dino1: string, dino2: string, spliceIdx: number): string;
}

const dinoMaker : DinoDNAMixer = function (dino1: string, dino2: string, spliceIdx: number): string {
  return dino1.substring(spliceIdx) + dino2.substring(spliceIdx);
}

const newDino = dinoMaker('CGGCAD', 'ACGCAA', 3);
console.info(newDino); //logs 'CADCAA'

It’s possible to use the type keyword as an interface to describe a function.

type DinoDNAMixer = (dino1: string, dino2: string, spliceIdx: number) => string;

interface DinoFactory {
  makeDino: DinoDNAMixer;
  factoryName: string;
}

Setup 2

✏️ Create src/interfaces/create-park-slug.ts and update it to be:


export default function createParkSlug(dinoPark) {
  return dinoPark.name.replace(/ /g, '-');
}

Verify 2

✏️ Create src/interfaces/create-park-slug.test.ts and update it to be:

import assert from "node:assert";
import { describe, it } from "node:test";
import { createParkSlug } from "./create-park-slug";

describe("Interfaces", () => {
  it("createParkSlug", function () {
    let result = createParkSlug({
      name: "Isla Sorna Park",
      address: {
        street: "123 Main",
        city: "Sandusky",
        state: "Ohio",
        zip: "12345",
      },
    });
    assert.equal(result, "Isla-Sorna-Park", "slug works");
  });
});

✏️ Run the following to verify your solution:

npm run test

Exercise 2

In the create-park-slug.ts file edit the createParkSlug function to take a parameter that is the interface DinoPark created previously and returns a slug for the park by replacing any spaces with dashes. Ex. the park "Isla Sorna Park" should return the slug Isla-Sorna-Park.

Have issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.

Solution 2

Click to see the solution

✏️ Update create-park-slug.ts to the following:

import DinoPark from "./dino-park";

export function createParkSlug(dinoPark: DinoPark) {
  return dinoPark.name.replace(/ /g, '-');
}

Have issues with your local setup? See the solution in StackBlitz or CodeSandbox.

Next steps

Next, let’s take a look at keyof and typeof to create new types from types.