TypeScript basics with Mocha setup - learn how to use interfaces · GitHub
source link: https://gist.github.com/avermeulen/72598daf29171088689793fc145b999c
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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 nogreet
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
- 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 aPool
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 aname
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 aPool
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
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK