Skip to content

Instantly share code, notes, and snippets.

@avermeulen
Last active April 20, 2024 08:33
Show Gist options
  • Save avermeulen/72598daf29171088689793fc145b999c to your computer and use it in GitHub Desktop.
Save avermeulen/72598daf29171088689793fc145b999c to your computer and use it in GitHub Desktop.
TypeScript basics with Mocha setup - learn how to use interfaces

Setup a new TypeScript project with Mocha support

Ensure you have TypeScript installed globally using this command:

npm install -g typescript

This outlines how to setup a new TypeScript project with mocha support.

Create a new project folder. I will call mine ts-mocha-go.

mkdir ts-mocha-go
cd ts-mocha-go

Then initialize your NodeJS project and initialize TypeScript.

npm init --y
tsc --init

Open the project in VSCode:

code .

Configure TypeScript

Open the generated tsconfig.json file and make these changes.

Change the outDir where JavaScript files will be generated to ./dist. The outDir property is commented out by default.

It should look like this:

"outDir": "./dist",

Change your target language to ES6

    "target": "es6", 

Create a test

Create a test folder in your project root folder. In that folder create a file called test.ts.

Copy this code into the file:

import assert from 'assert';

describe('My function', function() {
    it('should test', function() {
        assert.equal(1, 2);
    });
});

If you open the test in VSCode you will see some errors.

TypeScript don't know what mocha is and it can't import the assert node module.

Test setup

To fix all of the above type errors install these dependencies:

npm install --save-dev mocha typescript ts-mocha

Ann install these TypeScript types:

npm install --save-dev @types/mocha
npm install --save-dev @types/node

Once you installed the above there should be no errors in your test.ts file.

Configure ts-mocha

Add an entry to your package.json file to configure your mocha test written in TypeScript to run using ts-mocha

"scripts": {
    "test": "ts-mocha test/*.ts"
  }

Run your mocha tests using:

npm test

You should have one failing test. Fix the it and run:

npm test

You should have one failing test.

Interfaces intro

Create a function called greet in a file called greet.ts like this:

export default function greet(firstName: string, lastName: string) {
  return `Hello, ${firstName} ${lastName}`;
}

Create a new test in your test folder called user.tests.ts

Now you can greet a user on first and last name.

Add one tests to make this assert pass:

  assert.equal("Hello, Bob Crow", greet("Bob", "Crow"));

Not much new here, but note that importing modules in TypeScript is different.

import greet from '../greet'

https://www.typescriptlang.org/docs/handbook/modules.html

Note the difference between default modules and not default modules

Create an interface

Now create a Person interface in a file called person.ts export it as a default module.

export default interface Person {
  firstName: string
  lastName: string
}

https://www.typescriptlang.org/docs/handbook/interfaces.html

And then change your greet function to greet a Person interface instead.

import Person from './person';

export default function greet(person: Person) {
  return `Hello, ${person.firstName} ${person.lastName}`;
}

This code should fail now in your test:

assert.equal("Hello, Bob Crow", greet("Bob", "Crow"));

As the greet function takes in an Person interface now.

Change it to this instead:

assert.equal("Hello, Bob Crow", greet({
  firstName : "Bob", 
  lastName : "Crow"
}));

Add email to the interface

Add an email property to the Person interface.

This should cause an error in your test in TypeScript. Fix the test by adding an email property.

Change your function to return this:

Hello, Bob Crow we will be in touch at: [email protected]

when called with:

greet({
  firstName : "Bob", 
  lastName : "Crow",
  email : "[email protected]"
})

Be sure to add a test.

You can make a property in an interface optional by adding a ? (question mark) after the property name - try it out.

email?: string

Interfaces control which properties and functions should be present in a object instance.

Can you make your function return this:

Change your function to return this: Hello, Bob Crow we can't contact you.

when called with:

greet({
  firstName : "Bob", 
  lastName : "Crow"
})

Be sure to add a test.

Interfaces with classes

You can create a GreetIn interface like this:

interface GreetIn {
  greet (name: string) : string
}

And then create an implementation for the function like this:

class GreetInXhosa implements GreetIn {
  greet (name: string) {
    return "Molo, " + name;
  }
}

What happens if you say a class implement the GreetIn interface, but it got no greet method

Create at least two more implementations of the Greetin interface which can greet in different languages.

Note: test all your interface implementations using a mocha test

How can you use the GreetIn interface in our original Greetings class?

Using GreetIn

Using the GreetIn interface we can remove some repition from a typical greet function that can greet a user in 3 languages.

Create the language enum in a file called language.ts

export enum language {
	afr,
	eng,
	xhosa
}

Import this into greet.ts.

Instead of creating a function like this:

export function greet(name: string, chosenLanguage: language) {
    if (chosenLanguage === language.afr) {
        return "Goeie more, " + name;
    }

    if (chosenLanguage === language.eng) {
        return "Good morning, " + name;
    }

    if (chosenLanguage === language.xhosa) {
        return "Molo, " + name;
    }
}

At this stage you should be able to call the greet function you will need to pass an enum entry in.

Using the classes that's implementing the GreetIn interface

We can do this:

export function greet(name: string, chosenLanguage: language) {
    let greetIn : GreetIn = new GreetInEnglish();
    if (chosenLanguage === language.afr) {
        greetIn = new GreetInAfrikaans();
    }
    if (chosenLanguage === language.xhosa) {
        greetIn = new GreetInXhosa();
    }
    
    return greetIn.greet(name);
}

Depending on which language a user is greeted in a specific instance of the GreetIn interface is instantiated.

Using a Map to greet

We can also use the Map object for this.

You can create a Map instance that use the Language enumeration as a key and the value an instance of the GreetIn interface.

let theGreetInMap : Map<Language, GreetIn>  = new Map();

Now you can map a Language to a given GreetIn implementation.

Map an Object for English:

theGreetInMap.set(Language.eng, new GreetInEnglish());

Get the English greeting:

let greeting = theGreetInMap.get(Language.eng);
console.log(greeting.greet('Lindani'));

Now create the class like this:

export class Greeter {
    // create a Map that has a languages enum as a key and a GreetIn interface instance as a value
    private greetLanguages:Map<Language, GreetIn>

    constructor(greetLanguages:Map<Language, GreetIn>){
        this.greetLanguages = greetLanguages;
    }
    
    greet(name: string, chosenLanguage:Language) {
        let greetIn = this.greetLanguages.get(chosenLanguage);
        if (greetIn) {
            return greetIn.greet(name);
        }
        return "";
    }
  }

Using a Map Object we can map a Language enum to the appropriate GreetIn class instance. The greet function can lookup the appropriate GreetIn instance to call based on the Language enum passed into it.

Adding a new language is easy now:

  • Add a new class that implements the GreetIn interface for the language,
  • add a new entry to Language enum for the new language
  • and add the KeyValue pair to the map instance passed into Greeter.

Try this code in a Unit Test & add more tests to ensure that you are happy that the new Greeter class is working.

let greetMap = new Map<Language, GreetIn>();  
greetMap.set(Language.afr, new GreetInAfrikaans());
greetMap.set(Language.eng, new GreetInEnglish());

let greeter = new Greeter(greetMap);

assert.equal("Goeie dag, Andre", greeter.greet("Andre", Language.afr));
assert.equal("Good day, Andrew", greeter.greet("Andrew", Language.eng));
assert.equal("", greeter.greet("Andrew", Language.fr));

Another interface

Let's think about an interface for the User Greet Counter functionality.

It should be able to keep track of:

  • how many users have been greeted,
  • and be able to tell us how many times a given user has been greeted.

This interface will do:

interface UserGreetCounter {
    countGreet(firstName: string) : void // returns nothing
    greetCounter : number
    userGreetCount(firstName: string) : number
}

Note: This interface should not create any greeting message. So non of it's methods is returning a string.

The countGreet function takes in a user's firstname and an implementation of the interface should use this method to keep track of which users has been greeted already. The greetCounter property/attribute should return the number of individual users greeted. And the userGreetCount should return how many times a given user has been greeted.

The new ES6 Map

Implement the UserGreetCounter interface using the Map Object from ES6.

The Map Object can be used very similar to an Object Literal.

For example using an Object Literal you do this:

var objectMap = {};
// set the value
objectMap['lindani'] = 1;
// get the value from the Object Literal
console.log(objectMap['lindani'])

Using the Map object:

var theMap = new Map();
// set the value
theMap.set('lindani', 1);

// get the value
console.log(theMap.get('lindani'));

Using the Map Object we can go one step further and add type information to the Map to specify what data types can be added to the Map. Let's define the Map's key to be a string and the value to be a number. The key will be the username and the value would be the counter of how many time a user has been greeted.

var theMap = new Map<string, number>();
theMap.set('lindani', 1);
console.log(theMap.get('lindani'));

// you will get an error in TypeScript as the key should be a string
theMap.set(2, 'Joe'); // [ts] Argument of type '2' is not assignable to parameter of type 'string'.

The example above is using generics to specify that the Map's key is a string and the value a number new Map<string, number>() the part in the angle brackets is the generic declaration <string, number> this can be any other valid data type definition.

Currently you are using generics lightly - you can read more about it here: https://www.typescriptlang.org/docs/handbook/generics.html.

Learn more about the new Map Object : https://medium.com/front-end-hacking/es6-map-vs-object-what-and-when-b80621932373.

Create private map

private theGreetedUsers : Map<string, number>

In the constructor initialise it like this:

this.theGreetedUsers = new Map<string, number>();

Accessor functions - get and set

The greetCounter is a read only property of UserGreetCounter interface. User of the interface implementation should not be able to change it directly.

This should fail:

// UserGreetCounterImpl doesn't exist it's just an imaginary implementation of the interface
let userGreetCount = new UserGreetCounterImpl();

// this should give an error
userGreetCount.greetCounter = 0;

// this should work fine
console.log(userGreetCount.greetCounter);

You can do this using a 'Accessor' functions. You can read more here:

Create a get accessor for the UserGreetCounter greetCounter property like this:

get greetCounter() : number {
    return usersGreeted.keys.length;
}

Implement the UserGreetCounter

Implement the UserGreetCounter interface in a class called MapUserGreetCounter. Write unit tests to ensure you are happy with the implementation.

Read more about classes: https://www.typescriptlang.org/docs/handbook/classes.html

Use:

  • A Map object to store who has been greeted
  • Use a get Accessor for the greetCounter property

Use all the interfaces together

We have splitted the functionality of the Greet class into two interfaces GreetIn and UserGreetCounter. The one greeting creating the greet messages and the other keeping track of the greet counter/s. In this section we will explore how to use them together.

Extend the Greeter class to take a UserGreetCounter in it's constructor.

export class Greeter {
    // create a Map that has a languages enum as a key and a GreetIn interface instance as a value
    private greetLanguages: Map<language, GreetIn>
    private userGreetCounter: UserGreetCounter;

    constructor(greetLanguages: Map<language, GreetIn>, userGreetCounter: UserGreetCounter) {
        this.greetLanguages = greetLanguages;
        this.userGreetCounter = userGreetCounter;
    }

    greet(name: string, chosenLanguage: language) {
        let greetIn = this.greetLanguages.get(chosenLanguage);
        // keep track of how many users has been greeted
        this.userGreetCounter.countGreet(name);
        if (greetIn) {
            return greetIn.greet(name);
        }
        return "";
    }
    
    // call the greetCounter on the userGreetCounter
    public get greetCounter(): number {
        return this.userGreetCounter.greetCounter;
    }
    
    // call the userGreetCount on the userGreetCounter
    userGreetCount(firstName: string): number {
        return this.userGreetCounter.userGreetCount(firstName);
    }
}

To instantiate the class you will need to pass in an instance of the UserGreetCounter interface use your MapUserGreetCounter from earlier.

const greetMap = new Map<Language, GreetIn>();

greetMap.set(Language.afr, new GreetInAfrikaans());
greetMap.set(Language.eng, new GreetInEnglish());

const mapUserGreetCounter = new MapUserGreetCounter();
const greeter = new Greeter(greetMap, mapUserGreetCounter);

Test this new version of the Greeter class using mocha.

Note: That some of your previous tests should start fail now.

Implement an UserGreetCounter that stores data in PostgreSQL

Create an implementation the UserGreetCounter interface that stores data in PostgreSQL and write unit tests to ensure you are happy with the implementation.

Note: You need to install the PostgreSQL TypeScript types and the PostgreSQL database driver.

Install the dependencies:

npm install --save pg
npm install --save @types/pg

Use this new implementation with the Greeter function

The UserGreetCounter implementation should take in a Pool instance in it's constructor

Some cleanup

The Map Object mapping the GreetIn interface instances to a language is complicating the Greeter class. How can you can simplify it?

Start of by creating a new interface called Greetable:

  • with one function called greet
  • that takes two parameters - the language to greet in and a name to greet,
  • and eturns the greeting.

Creating this interface will help to decouple the greeting logic from the Greeter class.

interface Greetable {
    greet(firstName:string, language:Language) : string
}

Change the Greeter class to use the Greetable interface.

export class Greeter implements Greetable {
    private greetable: Greetable
    private userGreetCounter: UserGreetCounter;

    constructor(greetable: Greetable, userGreetCounter: UserGreetCounter) {
        this.greetable = greetable;
        this.userGreetCounter = userGreetCounter;
    }

    greet(name: string, chosenLanguage: Language) {
        // get the greeting message
        let message = this.greetable.greet(name, chosenLanguage);
        // mange the user count
        this.userGreetCounter.countGreet(name);
        return message;
    }

    public get greetCounter(): number {
        return this.userGreetCounter.greetCounter;
    }

    userGreetCount(firstName: string): number {
        return this.userGreetCounter.userGreetCount(firstName);
    }
}

Create a class called GreetInManager that implements the Greetable interface. Which use the Map Object to lookup the matching GreetIn class for the Language specified.

class GreetInManager implements Greetable {
    
    constructor(private greetLanguages: Map<Language, GreetIn>) {
        this.greetLanguages = greetLanguages;
    }

    greet(firstName: string, language: Language): string {
        let greetIn = this.greetLanguages.get(language);
        if (greetIn) {
            return greetIn.greet(name);
        }
        return "";
    }
}

The GreetInManager has the single responsibility now of selecting GreetIn object instance to use and then the greet the user correctly. It also returns a blank greeting if the language the user is to be greeted in is not available in the Map.

Test your new version of Greeter that is using Greetable using mocha.

Greet from the database.

Implement a version of Greetable that stores the the language and greeting mappings in PostgreSQL. A new language to greet in can be added by adding a new row in the database.

The Greetable implementation should take in a Pool instance in it's constructor

See the link how to instantiate an enum from a string: https://stackoverflow.com/questions/17380845/how-to-convert-string-to-enum-in-typescript

Test your new interface implementation using Mocha and also use it with the Greeter Class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment