Your issue stems from the way a class is instantiated.
Instead of:
let PROJECTS: Project[] = [
{ projectId: 1, description: "Sample", startDate: new Date("2016-12-12"), endDate: new Date("2017-1-13") },
{ projectId: 2, description: "Sample 2", startDate: new Date("2016-12-12"), endDate: new Date("2017-1-13") }
];
you should use:
let PROJECTS: Project[] = [
new Project(1, "Sample", new Date("2016-12-12"), new Date("2017-1-13") ),
new Project(2, "Sample 2", new Date("2016-12-12"), new Date("2017-1-13")
];
Explanation
Project
represents a class. getDaysRemaining()
is a member of that class. To create an instance of a class, you can utilize new Project(...)
.
{ projectId: 1, ... }
denotes an object that is not an instance of class Project
. Hence, all the members are listed as properties within curly braces.
You can determine if { projectId: 1, ... }
is an instance of Project
by using
{
project1: 1, ... } instanceof Project
, resulting in
false
.
ES6? TypeScript shares many syntax features with modern JavaScript language advancements. Class definitions in ES6 are quite similar to those in TypeScript, excluding types and modifiers since JavaScript lacks static typing.
class Project {
constructor(projectId, description, startDate, endDate) {
this.projectId = projectId;
this.description = description;
this.startDate = startDate;
this.endDate = endDate;
}
getDaysRemaining() {
var result = this.endDate.valueOf() - Date.now().valueOf();
result = Math.ceil(result / (1000 * 3600 * 24));
return result < 0 ? 0 : result;
}
}
In ES6, you can call new Project()
, leading to undefined
for all arguments. However, in TypeScript, this isn't feasible due to missing constructor arguments being checked at compile time. A competent IDE could also perform syntax checks at design time.
To allow optional arguments in TypeScript, you can define them with question marks near the parameters:
constructor(
public projectId?: number,
public description?: string,
public startDate?: Date,
public endDate?: Date
) { }
Common Issue - fetching data from a source like a web service
If you receive your Project
data from a web service, it will likely be deserialized into plain objects like { projectId: 1, ... }
. Converting these into Project
instances requires additional manual work.
An easy approach involves passing each property individually from one side to another using a factory method:
class Project implements IProject {
...
static create(data: IProject) {
return new Project(data.projectId, data.description, data.startDate, data.endDate);
}
...
}
You can use an interface (instead of any
) for your data to aid with typing and implementation (
class Project implements IProject
):
interface IProject {
projectId: number;
description: string;
startDate: Date;
endDate: Date;
}
Another possibility is utilizing optional arguments, although this may not suit every scenario:
class Project implements IProject {
...
constructor(
public projectId?: number,
public description?: string,
public startDate?: Date,
public endDate?: Date
) { }
static create(data: IProject) {
return Object.assign(new Project(), data);
}
...
}
In this case, the interface would feature optional parameters denoted by the question mark, in line with the above implementation:
interface IProject {
projectId?: number;
description?: string;
startDate?: Date;
endDate?: Date;
}
Compilation and Interfaces - During transpilation of TypeScript code to JavaScript, interfaces are removed since JavaScript doesn't support interfaces natively. Interfaces in TypeScript serve to provide additional type information during development.
Strive for SOLID Principles - S = Single Responsibility Principle
The current implementation where Project
handles both storing data and computing remaining days violates the Single Responsibility Principle. It's advisable to split these responsibilities.
Create an interface to represent Project
data structure:
interface IProject {
projectId: number;
description: string;
startDate: Date;
endDate: Date;
}
Move the getDaysRemaining()
function to a separate class such as:
class ProjectSchedulingService {
getDaysRemaining(project: IProject) {
var result = project.endDate.valueOf() - Date.now().valueOf();
result = Math.ceil(result / (1000 * 3600 * 24));
return result < 0 ? 0 : result;
}
}
This separation has benefits such as having clear data representation with IProject
, enabling additional functionalities in independent classes like ProjectSchedulingService
.
It also facilitates working with plain objects instead of instantiating more classes. By casting data to an interface from a web service, developers can operate in a typed manner.
Splitting business logic into multiple classes allows for easier definition of dependencies, maintaining business semantics, mocking dependencies for unit testing, and ensuring individual units remain testable.
In frameworks like Angular, employing multiple injectable services enhances flexibility compared to a monolithic service. This aids in adhering to Interface Segregation Principle.
While these changes may seem minor initially, they contribute significantly to code maintenance over time. Refactoring early on prevents complexity issues, enhances testability, and promotes better code organization.