Skip to content

Przemysław Konieczniak - Dev Notes

Interfaces

OOP, TypeScript, JavaScript

interface is a keyword that allows us to separate class interface from its implementation.
Interface defines the contract which should be fulfilled by the entity that implements this interface.

The above definition is the most common use of interfaces in languages like C# or Java.

Consider the implementation of the Stack class. The stack is an abstract data type that serves as a collection of elements.

The stack can be implemented in many ways, i.e. the values of the stack can be stored in arrays, lists or binary trees, the stack can have fixed size and so on.

Independently of the implementation, Stack class definition should contain methods like push and pop and this is our interface.

IStack.ts
1export interface IStack<T> {
2 push(item: T): void,
3 pop(): T | undefined,
4 firstElement(): T | undefined,
5 lastElement(): T | undefined
6}
Stack.ts
1import { IStack } from './IStack'
2
3export class Stack<T> implements IStack<T> {
4 constructor(private stack: T[] = []) {}
5
6 push(item: T) {
7 this.stack.unshift(item)
8 }
9
10 pop(): T | undefined {
11 return this.stack.shift()
12 }
13
14 firstElement(): T | undefined {
15 return this.stack[0]
16 }
17
18 lastElement(): T | undefined {
19 return this.stack[this.stack.length - 1]
20 }
21}
index.ts
1import { deepStrictEqual } from 'assert'
2import { Stack } from './Stack'
3
4const stringStack = new Stack<string>()
5stringStack.push('foo')
6stringStack.push('bar')
7deepStrictEqual(stringStack.lastElement(), 'foo')
8deepStrictEqual(stringStack.pop(), 'bar')
9
10const numberStack = new Stack<number>()
11numberStack.push(1)
12numberStack.push(2)
13deepStrictEqual(numberStack.lastElement(), 1)
14deepStrictEqual(numberStack.pop(), 2)

Interfaces are capable of describing the wide range of shapes that JavaScript objects can take.

For example, we can create the User interface which will describe the shape of the user object and then we can refer to this type in multiple places.

1interface User {
2 username: string,
3 password: string,
4 avatarURL?: string,
5 birthdate?: Date,
6}
7
8const createUser = (user: User): void => { /* Any logic */ }
9createUser({ username: 'foo', password: 'baz' })
10createUser({ username: 'foo' }) // Shows error property 'password' is missing
11
12const user: User = {
13 username: 'foo',
14 password: 'bar',
15 avatarURL: 'https://giphy.com/gifs/duck-kitten-ns9NbiMArJgkw'
16}
17
18const invalidUser: User = { password: 'foo' } // Shows error

Be aware that the TypeScript represents the Structural Typing system also called Duck Typing.

Shortly, it means that the TypeScript cares only about the shape of objects. The object's type itself is not important.

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

1interface Car {
2 id: number,
3 run(): void
4}
5
6interface Job {
7 id: number,
8 run(): void
9}
10
11const car: Car = {
12 id: 100,
13 run() { console.log(`Starting the engine...`) }
14}
15
16const job: Job = {
17 id: 1,
18 run() { console.log(`Processing the job...`) }
19}
20
21const start = (job: Job) => job.run()
22
23start(job)
24
25// TypeScript compiler will not throw an error, even if the start function expects the Job type as an argument,
26// because the car has the same shape as the job object.
27start(car)

With interfaces, we can also define the function signatures

1import { readFile } from 'fs'
2
3interface Callback {
4 (error: Error | null, data: string): void
5}
6
7const callback: Callback = (error, data) => {
8 if (error) throw error
9 data.search('foo')
10}
11
12readFile('/src/file.txt', 'utf8', callback)
13
14// This code will show an error.
15// We didn't define the encoding option.
16// Our callback will receive the Buffer instead of a string.
17// This code is not compatible with our interface definition.
18readFile('/src/file.txt', callback)

Interfaces allow us also to define indexable types

1interface StatusCodes {
2 [index: string]: number
3}
4
5const statusCodes: StatusCodes = {
6 'OK': 200,
7 'NOT_FOUND': 400,
8 'INTERNAL_SERVER_ERROR': 500
9}
10
11statusCodes['NOT_FOUND']

Interfaces, like classes, are extensible. An interface can also extend multiple interfaces.

1interface Person { name: string }
2interface Employee extends Person { job: string }
3interface Boss extends Person, Employee { title: string }
4
5const person: Person = { name: 'foo' }
6const employee: Employee = { name: 'foo', job: 'bar' }
7const boss: Boss = { name: 'foo', job: 'bar', title: 'baz' }
© 2020 by Przemysław Konieczniak - Dev Notes. All rights reserved.