Your First TypeScript Project Part 3
Hello in this post will be the finale to the TypeScript Project being built in the first and second parts. The application in case you haven’t read the other parts is a Todo Application for the command line. You will need to have completed the other two parts in order to follow along with this one. Without further ado let’s get started. You can find Part One and Part Two
Implementing a Third-Party Package
One of the best features of writing JavaScript code is the ecosystem of packages that can be added to your projects. TypeScript, being a superset of JavaScript, allows any package to be used within your project. The caveat of this though is that you will need to install the static types for the package. Let’s add a package now that allows for prompting the user for commands and processing responses called Inquierer.js.
$ npm install inquirer@6.3.1
The package will be added to our package.json file and we can then import the package. We will be importing the package in our index.ts file.
import { TodoItem } from "./todoItem";
import { TodoList } from "./todoList";
import * as inquirer from "inquirer";let todos: TodoItem[] = [
new TodoItem(1, "Learn TypeScript"), new TodoItem(2, "Play Sun Haven"), new TodoItem(3, "Create Pixel Art for Game"), new TodoItem(4, "Finish Loki", true)];let list: TodoList = new TodoList("Jonathan", todos);function displayTodoList(): void {
console.log(`${list.userName}'s Todo List ` + `(${ list.getItemCounts().incomplete } items to do)`);
collection.getTodoItems(true).forEach(item => item.printDetails());
}enum Commands {
Quit = "Quit"
}function promptUser(): void {
console.clear();
displayTodoList();
inquirer.prompt({
type: "list",
name: "command",
message: "Choose option",
choices: Object.values(Commands)
}).then(answers => {
if (answers["command"] !== Commands.Quit) {
promptUser();
}
})
}promptUser();
We have made use of the Inquirer.js package to prompt the user and offer a choice of commands. We only have one command currently which is Quit. We will add some more throughout this post. Let’s run our code to make sure all is working.
$ tsc
$ node dist/index.js
// =>
Jonathan's Todo List (3 items to do)
1 Learn TypeScript
2 Play Sun Haven
3 Create Pixel Art for Game
4 Finish Loki (complete)
? Choose option (Use arrow keys)
> Quit
If you see the above and can press enter which will select the Quit command the project should terminate and we have a fully working project. We can now see how to install the type declaration for the Inquirer Package.
Adding Type Declarations
TypeScript doesn’t prevent JavaScript code from being used, however without the type declarations for packages it can’t provide any assistance for its use. The compiler has no clue as to the data types that are being used by our Inquirer.js package. It is basically trusting that we are using the correct types safely. Let’s install the type definitions now
$ npm install --save-dev @types/inquirer
Let’s test that the type definitions were installed and are working. Inside of our promptUser() function in index.ts we can add a property that doesn’t exist on the API.
function promptUser(): void {
console.clear();
inquirer.prompt({
type: "list",
name: "command",
message: "Choose option",
choices: Object.values(Commands),
nonExistentProp: "hello"
}).then(answers => {
if (answers["command"] !== Commands.Quit) {
promptUser();
}
})
}
Let’s compile our code: $tsc
we shoud see an error message come back indicating that the noneExistentProp
property doesn’t exist. Our types definitions are now in place and we have tested that they are working.
Commands
Our application only handles one command. Though it handles the Quit command well, our application could use some more commands. I mean it’s not really a good todo list application if we can’t create todos or edit them right?
Filtering
We are going to add the ability to filter our items. Inside of index.ts let’s update the code:
import { TodoItem } from "./todoItem";
import { TodoList } from "./todoList";
import * as inquirer from 'inquirer';let todos: TodoItem[] = [
new TodoItem(1, "Learn TypeScript"), new TodoItem(2, "Play Sun Haven"), new TodoItem(3, "Create Pixel Art for Game"), new TodoItem(4, "Finish Loki", true)];let list: TodoList = new TodoList("Jonathan", todos);
let showCompleted = true;function displayTodoList(): void {
console.log(`${collection.userName}'s Todo List ` + `(${ list.getItemCounts().incomplete } items to do)`);
list.getTodoItems(showCompleted).forEach(item => item.printDetails());
} enum Commands {
Toggle = "Show/Hide Completed",
Quit = "Quit"
} function promptUser(): void {
console.clear();
displayTodoList();
inquirer.prompt({
type: "list",
name: "command",
message: "Choose option",
choices: Object.values(Command),
}).then(answers => {
switch(answers["command"]) {
case Commands.Toggle:
showCompleted = !showCompleted;
promptUser();
break;
}
})
}promptUser();
Let’s run the app
$ tsc
$ node dist/index.js
Now if we select the Show/Hide Completed Command we should see our completed todo items disappear from the list and reappear once hit again.
Tasks
Kind of hard to have a todo app when you can’t add tasks. Let’s update our index.ts file to add this new command now
import { TodoItem } from "./todoItem";
import { TodoList } from "./todoList";
import * as inquirer from "inquirer";let todos: TodoItem[] = [
new TodoItem(1, "Learn TypeScript"), new TodoItem(2, "Play Sun Haven"), new TodoItem(3, "Create Pixel Art for Game"), new TodoItem(4, "Finish Loki", true)];let list: TodoList = new TodoList("Jonathan", todos);
let showCompleted = true;function displayTodoList(): void {
console.log(`${list.userName}'s Todo List ` + `(${ list.getItemCounts().incomplete } items to do)`);
list.getTodoItems(showCompleted).forEach(item => item.printDetails()); enum Commands {
Add = "Add New Todo",
Toggle = "Show/Hide Completed",
Quit = "Quit"
} function promptAdd(): void {
console.clear();
inquirer.prompt({ type: "input", name: "add", message: "Enter todo:"})
.then(answers => {if (answers"add"] !== "") {
list.addTodo(answers["add"]);
}
promptUser();
})
} function promptUser(): void {
console.clear();
displayTodoList();
inquirer.prompt({
type: "list",
name: "command",
message: "Choose option",
choices: Object.values(Commands),
}).then(answers => {
switch(answers["command"]) {
case Commands.Toggle:
showCompleted = !showCompleted;
promptUser();
break;
case Commands.Add:
promptAdd();
break;
}
})
}promptUser();
Now we can run the project and make sure we can add a todo
$ tsc
$ node dist/index.js
Select the add new todo command and enter some text for the todo and hit enter. Once you hit enter a new item should be added to the end of the list.
Completing Todos
Completing a todo will be a two-stage process that will require the selection of a todo item and then mark it as complete. Let’s add the code now inside index.ts
import { TodoITem } from "./todoItem";
import { TodoList } from "./todoList";
import * as inquirer from "inquirer";let todos: TodoItem[] = [
new TodoItem(1, "Learn TypeScript"), new TodoItem(2, "Play Sun Haven"), new TodoItem(3, "Create Pixel Art for Game"), new TodoItem(4, "Finish Loki", true)];let list: TodoList = new TodoList("Jonathan", todos);
let showCompleted = true;function displayTodoList(): void {
console.log(`${list.userName}'s Todo List ` + `(${ list.getItemCounts().incomplete } items to do)`);
list.getTodoItems(showCompleted).forEach(item => item.printDetails());
}enum Commands {
Add = "Add New Task",
Complete = "Complete Task",
Toggle = "Show/Hide Completed",
Purge = "Remove Completed Tasks",
Quit = "Quit"
}function promptAdd(): void {
console.clear();
inquirer.prompt({ type: "input", name: "add", message: "Enter todo:"})
.then(answers => { if (answers["add"] !== "") {
list.addTodo(answers["add"]);
}
promptUser();
})
}function promptComplete(): void {
console.clear();
inquirer.prompt({ type: "checkbox", name: "complete",
message: "Mark Tasks Complete",
choices: list.getTodoItems(showCompleted).map(item =>
({name: item.task, value: item.id, checked: item.completed}))
}).then(answers => {
let completedTasks = answers["complete"] as number[];
list.getTodoItems(true).forEach(item => list.markComplete(item.id, completedTasks.find(id => id === item.id) != undefined));
promptUser();
})
}function promptUser(): void {
console.clear();
displayTodoList();
inquirer.prompt({
type: "list",
name: "command",
message: "Choose option",
choices: Object.values(Commands),
}).then(answers => {
switch (answers["command"]) {
case Commands.Toggle:
showCompleted = !showCompleted;
promptUser();
break;
case Commands.Complete:
if (list.getItemCounts().incomplete > 0) {
promptComplete();
} else {
promptUser();
}
break;
case Commands.Purge:
list.removeComplete();
promptUser();
break;
}
})
}promptUser();
Alright that was quite a bit of code since we had to add two phases for this command. The changes will add a new prompt to the application that presents the user with the list of todos and allows their state to be changed. showComplete variable is used to determine if completed items are shown and will create a link between the Toggle and Complete commands.
Persistent State
This section is technically optional. Adding persistent state is only if you want the ability to open the application and have the todo items you created previously appear each time you open after closing. If you don’t care about that then see the conclusion after this section.
Moving on let’s install the package that will help with storing state.
$ npm install lowdb@1.0.0
$ npm install --save-dev @types/lowdb
Lowdb is a database package that stores data in a JSON file which is then used as the data storage component for the json-server package. We will need to make the itemMap public let’s update our code in todoList.ts:
import { TodoItem } from "./todoItem";type ItemCounts = {
total: number,
incomplete: number
}export class TodoList {
private nextId: number = 1;
protected itemMap = new Map<number, TodoItem>(); constructor(public userName: string, todoItems: TodoItem[] = []) {
todoItems.forEach(item => this.itemMap.set(item.id, item));
} // I decided not to write out the whole file since we are just changing one word here. The word in bold
The protected keyword tells TSC that a property can be accessed only by a class or its subclass. We are going to create the subclass now. Create a file called jsonTodoList.ts in the src folder with the code:
import { TodoItem } from "./todoItem";
import { TodoCollection } from "./todoCollection";
import * as lowdb from "lowdb";
import * as FileSync from "lowdb/adapters/FileSync";type schemaType = {
tasks: { id: number; task: string; complete: boolean; }[]
};export class JsonTodoCollection extends TodoCollection {
private database: lowdb.LowdbSync<schemaType>;
constructor(public userName: string, todoItems: TodoItem[] = []) {
super(userName, []);
this.database = lowdb(new FileSync("Todos.json"));
if (this.database.has("tasks").value()) {
let dbItems = this.database.get("tasks").value();
dbItems.forEach(item => this.itemMap.set(item.id,
new TodoItem(item.id, item.task, item.complete)));
} else {
this.database.set("tasks", todoItems).write();
todoItems.forEach(item => this.itemMap.set(item.id, item));
}
} addTodo(task: string): number {
let result = super.addTodo(task);
this.storeTasks();
return result;
} markComplete(id: number, complete: boolean): void {
super.markComplete(id, complete);
this.storeTasks();
}
} removeComplete(): void {
super.removeComplete();
this.storeTasks();
} private storeTasks() {
this.database.set("tasks", [...this.itemMap.values()]).write();
}
}
We can apply the persistent storage inside of index.ts
import { TodoItem } from "./todoItem";
import { TodoList } from "./todoList";
import * as inquirer from "inquirer";
import { JsonTodoList } from "./jsonTodoList";let todos: TodoItem[] = [
new TodoItem(1, "Learn TypeScript"), new TodoItem(2, "Play Sun Haven"), new TodoItem(3, "Create Pixel Art for Game"), new TodoItem(4, "Finish Loki", true)];let list: TodoList = new JsonTodoList("Jonthan", todos);
let showCompleted = true;// rest of code omitted for time saving purposes.
Let’s build our application and run it:
$ tsc
$ node dist/index.js
We now have a fully working application that retains the items we have hardcoded which you can actually remove if you want to start with a clean slate. We can add new ones. Remove completed ones and if we close the applicaiton and restart it our list remains. How awesome.
Conclusion
Congratulations on finishing up your first TypeScript project. I know we covered a lot of features and, not to scare you away, probably only coverd a small sliver of the actual TypeScript langauge and features that it brings to the table. What I want you to walk away from with this project is that you stuck with it. Even though it was a bit of a long journey you completed the project and went step by step with me writing the code, hopefully not copy/paste, but if you did that’s fine too. Add this project to your GitHub to show off.