Your First TypeScript Project Part 2
Hello, glad you could make it to the second post on building your first TypeScript project. If you are seeing this one and you haven’t read the first one you can visit this link to read the first one. You will need the code from the first one to continue following along with this one. Without further ado let’s get coding.
Creating the collection for the Todo Items
Our application will manage a list of todo items. A user will be able to see the list, add new items, mark items complete and filter the items. We are going to start creating the “data” the application is going to use. By the end of this post we should be able to create new items, check the basic data model, and hopefully be able to manipulate the items such as removal and editing.
Creating the Data Model
Let’s start by creating a file called todoItem.ts directly in our src
directory. Inside that file we will add this code:
export class TodoItem {
constructor(public id: number,
public task: string,
public complete: boolean = false) {
}
printDetails(): void {
console.log(`${this.id}\t${this.task} ${this.complete ? "\t(complete)": ""}`);
}
}
Here we can see the difference between JavaScript and TypeScript and kind of what TypeScript brings to the table. We have our constructor method which takes some attributes that are clearly given types. Now we could leave these types off and TypeScript will infer the types for us. However we want to make sure that it knows these types right off the bat. So we annotate them with their types.
Creating The Todo List Class
Now we are going create our todoList.ts. Now inside the file we will write:
import { TodoItem } from "./todoItem";export class TodoList {
private nextId: number = 1; // we can let TypeScript infer the type here but we want to be specific
constructor(public userName: string, public todoItems: TodoItem[] = []) {
} addTodo(task: string): number {
while (this.getTodoById(this.nextId)) {
this.nextId++;
}
this.todoItems.push(new TodoItem(this.nextId, task));
return this.nextId;
}
getTodoById(id: number): TodoItem {
return this.todoItems.find(item => item.id === id);
}
markComplete(id: number, complete: boolean) {
const todoItem = this.getTodoById(id);
if (todoItem) {
todoItem.complete = complete;
}
}
}
Check Basic Data Model
Now we can update our index.ts file to try out some of our data model. Modify index.tx:
import { TodoItem } from "./todoItem";
import { TodoList } from "./todoList";let todos = [
new TodoItem(1, "Learn TypeScript"), new TodoItem(2, "Play Run Goblin Run"), new TodoItem(3, "Create Pixel Art for Game"), new TodoItem(4, "Finish Loki", true)];let list = new TodoList("Jonathan", todos);console.clear();
console.log(`${list.userName}'s Todo List`);let newId = list.addTodo("Go to store");
let todoItem = list.getTodoById(newId);
todoItem.printDetails();list.addTodo(todoItem);
One thing you will notice in this file is that we aren’t using any type declarations or annotations. This is because the other two files have their types declared and therefore TypeScript will infer the types based on what is defined in the other two files. Pretty awesome right?
Right now the last line, which is a JavaScript statement in case you weren’t familiar with it, is using a TodoItem object as the argument. The compiler will look at the definition of the addTodo method and in the todoItem.ts file and will see that the method expects to receive a different data type.
Let’s run our code. Save the file and run this command:
tsc
The result is
error TS2345: Argument of type 'TodoItem' is not assignable to parameter of type 'string'.
As you can see TypeScript does a pretty good job of figuring out what is going on and identifying the problem. Not mention the error message is far better than one you would get in plain JavaScript. We can update the index.ts to this:
import { TodoItem } from "./todoItem";
import { TodoCollection } from "./todoCollection";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);console.clear();
console.log(`${list.userName}'s Todo List`);let newId: number = collection.addTodo("Go to store");
let todoItem: TodoItem = list.getTodoById(newId);
todoItem.printDetails();//list.addTodo(todoItem);
The type information that we just added isn’t going to change the way the code works, however it will make the data types being used explicit which can make the purpose of code more clear and doesn’t require the compiler to infer the data types being used. Let’s run it.
$ tsc
$ node dist/index.js
// =>
Jonathan's Todo List
5 Go to store
Congratulations if you were able to get this to print out correctly. We are on the right track. If things aren’t quite clicking just yet I assure you I am still getting used to used to the way TypeScript works and I have been coding in TypeScript for almost 2 years now. Just now it takes some time.
Adding Features to Our List Class
Now we can add some capabilities to our TodoList class. Let’s update our code like so:
import { TodoItem } from "./todoItem";export class TodoList {
private nextId: number = 1;
private itemMap = new Map<number, TodoItem>(); constructor(public userName: string, todoItems: TodoItem[] = []) {
todoItems.forEach(item => this.itemMap.set(item.id, item));
} addTodo(task: string): number {
while (this.getTodoById(this.nextId)) {
this.nextId++;
}
this.itemMap.set(this.nextId, new TodoItem(this.nextId, task));
return this.nextId;
} getTodoById(id: number): TodoItem {
return this.itemMap.get(id);
} markComplete(id: number, complete: boolean) {
const todoItem = this.getTodoById(id);
if (todoItem) {
todoItem.complete = complete;
}
}
}
Generic types are supported in TypeScript and we make use of them while utilizing the JavaScript Map, a general-purpose collection that stores key/value pairs. Since JavaScript has such a dynamic type system, a Map can be used to store any mix of data types using any mix of keys. We needed to restrict this so we provided generic type arguments that tell the TypeScript compiler which types are allowed for the keys and values. These are represented using <>
.
Accessing Todo Items
Currently our app doesn’t display the list of items. Let’s update our todoList.ts file:
import { TodoItem } from "./todoItem";export class TodoCollection {
private nextId: number = 1;
private itemMap = new Map<number, TodoItem>(); constructor(public userName: string, todoItems: TodoItem[] = []) {
todoItems.forEach(item => this.itemMap.set(item.id, item));
}
addTodo(task: string): number {
while (this.getTodoById(this.nextId)) {
this.nextId++;
}
this.itemMap.set(this.nextId, new TodoItem(this.nextId, task));
return this.nextId;
}
getTodoById(id: number): TodoItem {
return this.itemMap.get(id);
}
getTodoItems(includeComplete: boolean): TodoItem[] {
return [...this.itemMap.values()]
.filter(item=> includeComplete || !item.complete);
} markComplete(id: number, complete: boolean) {
const todoItem = this.getTodoById(id);
if (todoItem) P
todoItem.complete = complete;
}
}
}
This new method will get the objects from the Map using its values method and uses them to create an array. Objects are processed using the filter method to select the objects that are required, using the includeComplete parameter to decide which objects are needed. TSC uses the information that’s been given to follow the types through each step. Finally let’s add a statement to index.ts so that we can use the TodoList class feature and display the list of todos.
import { TodoItem } from "./todoItem";
import { TodoList } from "./todoList";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 collection: TodoList = new TodoList("Jonathan", todos);console.clear();
console.log(`${list.userName}'s Todo List`);list.getTodoItems(true).forEach(item => item.printDetails());
Now we can run the app and see our handiwork.
$ tsc
$ node dist/index.js
// =>
Jonathan's Todo List
1 Learn TypeScript
2. Play Sun Haven
3. Create Pixel Art for Game
4. Finish Loki (complete)
Looks pretty good. Now let’s add the ability to remove the completed items.
Removing Completed Items
As tasks are added and marked complete, the number of items in our list will grow and eventually become difficult to manage. Let’s fix this inside of todoList.ts
import { TodoItem } from "./todoItem";export class TodoList {
private nextId: number = 1;
private itemMap = new Map<number, TodoItem>();
constructor(public userName: string, todoItems: TodoItem[] = []) {
todoItems.forEach(item => this.itemMap.set(item.id, item));
} addTodo(task: string): number {
while (this.getTodoById(this.nextId)) {
this.nextId++;
}
this.itemMap.set(this.nextId, new TodoItem(this.nextId, task));
return this.nextId;
} getTodoById(id: number): TodoItem {
return this.itemMap.get(id);
}
getTodoItems(includeComplete: boolean): TodoItem[] {
return [...this.itemMap.values()]
.filter(item => includeComplete || !item.complete);
} markComplete(id: number, complete: boolean) {
const todoItem = this.getTodoById(id);
if (todoItem) {
todoItem.complete = complete;
}
} removeComplete() {
this.itemMap.forEach(item => {
it (item.complete) {
this.itemMap.delete(item.id);
}
})
}
}
We are essentially using the Map.forEach to inspect each TodoItem stored and calls the delete method for each item that has the complete property marked as true.
Now we need to update our index.ts file to reflect the removal of completed items. Fill in the code:
import { TodoItem } from "./todoItem";
import { TodoList } from "./todoList";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);console.clear();
console.log(`${list.userName}'s Todo List`);list.removeComplete();
list.getTodoItems(true).forEach(item => item.printDetails());
Now let’s save all the files and run our app
$ tsc
$ node dist/index.js
// This will produce the result:
Jonathan's Todo List
1. Learn TypeScript
2. Play Sun Haven
3. Create Pixel Art for Game
As you can see the removal of our completed item, number 4 Finish Loki for me, is no longer showing up. Pretty awesome.
Item Counts
The last feature we will need for our TodoList class is to provide the counts of the total number of TodoItem objects. This will include both the number of completed items as well as the number left to complete.
We have focused on the classes as this is a common way that programmers are used to creating data types. JavaScript objects can also be created using literal syntax. TypeScript is able to check and enforce static types on objects the same way that it does for classes. When handling object literals, TSC focuses on the combination of property names and the types of their values, known as an object’s shape. The specific combination of names and types is known as a shape type.
In our todoList.ts let’s update and add the code below
import { TodoItem } from "./todoItem";type ItemCounts = {
total: number,
incomplete: number
}export class TodoList {
private nextId: number = 1;
private itemMap = new Map<number, TodoItem>(); constructor(public userName: string, todoItems: TodoItem[] = []) {
todoItems.forEach(item => this.itemMap.set(item.id, item));
} addTodo(task: string: number {
while (this.getTodoById(this.nextId)) {
this.nextId}};
}
this.itemMap.set(this.nextId, new TodoItem(this.nextId, task));
return this.nextId;
} getTodoById(id: number): TodoItem {
return this.itemMap.get(id);
}
getTodoItems(includeComplete: boolean): TodoItem[] {
return [...this.itemMap.values()]
.filter(item => includeComplete || !item.complete);
} markComplete(id: number, complete: boolean) {
const todoItem = this.getTodoById(id);
if (todoItem) {
todoItem.complete = complete;
}
}
removeComplete() {
this.itemMap.forEach(item => {
if (item.complete) {
this.itemMap.delete(item.id);
}
})
} getItemCounts(): ItemCounts {
return {
total: this.itemMap.size,
incomplete: this.getTodoItems(false).length
};
}
}
The type keyword we used here is used to create a type alias. This is a convenient way to assign a name to a shape type. Our type alias describes objects that have two number properties, named total and incomplete. We use this in our getItemCounts method. The JavaScript literal syntax to create an object whose shape matches the type alias. Let’s now update our index.ts
import { TodoItem } from "./todoItem";
import { TodoList } from "./todoList";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);console.clear();
console.log(`${list.userName}'s Todo List ` + `(${ list.getItemCounts().incomplete } items to do)`);
list.getTodoItems(true).forEach(item => item.printDetails());
Now let’s run our code.
$ tsc
$ node dist/index.js
// => should return
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)
Conclusion
That’s it for this post. In the next one we will finish up our application which will allow us to use some third party libraries for making our CLI Todo App a little easier. See you in the next one.