Interfaces

One of TypeScript’s core principles is that type checking focuses on the shape that values have. This is sometimes called "duck typing" or "structural subtyping".

Our First Interface

function printLabel(labeledObj: { label: string }) {
  console.log(labeledObj.label);
}

const myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

The printLabel function has a single parameter that requires that the object passed in has a property called label of type string. If the object passed in has more properties than this, the compiler will ignore it.

interface LabeledValue {
  label: string;
}

function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}

The interface LabeledValue is a name we can now use to describe the requirement in the previous example.

Optional Properties

Interfaces with optional properties are written similar to other interfaces, with each optional property denoted by a ? at the end of the property name in the declaration.

interface SquareConfig {
  color?: string;
  width?: number;
}

The advantage of optional properties is that you can describe these possibly available properties while still also preventing use of properties that are not part of the interface.

Readonly properties

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

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // Error

let ro: ReadonlyArray<number> = [1, 2, 3, 4];
ro[0] = 12; // Error

Variables use const whereas properties use readonly.

Excess Property Checks

Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments.

If an object literal has any properties that the “target type” doesn’t have, you’ll get an error.

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig) {}

let mySquare = createSquare({ colour: "red", width: 100 }); // Error: 'colour'

let myObj = { colour: "red", width: 100 };
let mySquare = createSquare(myObj); // No Error

We could solve this by adding a string index signature if you’re sure that the object can have some extra properties that are used in some special way.

interface SquareConfig {
  color?: string;
  width?: number;
}

Function Types

To describe a function type with an interface, we give the interface a call signature.

interface SearchFunc {
  (source: string, subString: string): boolean;
}
let mySearch: SearchFunc = function (source, subString) {
  return source.search(subString) > -1;
};

Indexable Types

Indexable types have an index signature that describes the types we can use to index into the object.

interface StringArray {
}

There are two types of supported index signatures: string and number. It is possible to support both types of indexers, but the type returned from a numeric indexer must be a subtype of the type returned from the string indexer.

While string index signatures are a powerful way to describe the "dictionary" pattern, they also enforce that all properties match their return type.

interface NumberDictionary {
  length: number;
  name: string;
  // Error, since obj.name is the same as obj['name']
}

You can make index signatures readonly in order to prevent assignment to their indices.

interface ReadonlyStringArray {
  readonly [index: number]: string;
}

Class Types

Implementing an interface

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}

class Clock implements ClockInterface {
  constructor(h: number, m: number) {}
  currentTime: Date = new Date();
  setTime(d: Date) {
    this.currentTime = d;
  }
}

The static and instance sides

To work with the static side of the class directly, we should define two interfaces.

interface ClockConstructor {
  new (hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
  tick(): void;
}

const Clock: ClockConstructor = class Clock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("beep beep");
  }
};

const clock = new Clock(12, 17);

Extending Interfaces

Like classes, interfaces can extend each other. An interface can extend multiple interfaces, creating a combination of all of the interfaces.

interface Shape {
  color: string;
}

interface PenStroke {
  penWidth: number;
}

interface Square extends Shape, PenStroke {
  sideLength: number;
}

Hybrid Types

An object could act as both a function and an object, with additional properties.

interface Counter {
  (start: number): string;
  interval: number;
  reset(): void;
}

function getCounter(): Counter {
  let counter = function (start: number) {} as Counter;
  counter.interval = 123;
  counter.reset = function () {};
  return counter;
}

Last updated