Solution:
Here are the steps that will help you create all the functions listed in the activity problem statement.
The toTitleCase function will process a string and capitalize the first letter of each word, but will make all the other letters lowercase.
Test cases for this function are as follows:
"war AND peace" => "War And Peace"
"Catcher in the Rye" => "Catcher In The Rye"
"tO kILL A mOCKINGBIRD" => "To Kill A MockingBird"
Here are the steps to help you write this function:
function toTitleCase (input:string) : string {
// split the string into an array on every occurrence of
// the space character const words = input.split(" ");
const titleWords = []; // loop through each word for (const word of words) {
// take the first character using `slice` and convert it to uppercase const initial = word.slice(0, 1).toLocaleUpperCase(); // take the rest of the character using `slice` and convert them to lowercase const rest = word.slice(1).toLocaleLowerCase(); // join the initial and the rest and add them to the resulting array titleWords.push(`${initial}${rest}`);
// join all the processed words const result = titleWords.join(" "); return result;}
console.log(`toTitleCase("war AND peace"):`);console.log(toTitleCase("war AND peace")); console.log(`toTitleCase("Catcher in the Rye"):`);console.log(toTitleCase("Catcher in the Rye"));console.log(`toTitleCase("tO kILL A mOCKINGBIRD"):`);console.log(toTitleCase("tO kILL A mOCKINGBIRD"));
toTitleCase("war AND peace"):War And Peace toTitleCase("Catcher in the Rye"):Catcher In The Rye toTitleCase("tO kILL A mOCKINGBIRD"):To Kill A Mockingbird
Here are the steps to help you write this function:
"War and Peace" => 3
"catcher-in-the-rye" => 4
"for_whom the-bell-tolls" => 5
function countWords (input: string): number {
const words = input.split(/[ _-]/);
return words.length;
}
console.log(`countWords("War and Peace"):`);
console.log(countWords("War and Peace"));
console.log(`countWords("catcher-in-the-rye"):`);
console.log(countWords("catcher-in-the-rye"));
console.log(`countWords("for_whom the-bell-tolls"):`);
console.log(countWords("for_whom the-bell-tolls"));
The toWords function will return all the words that are within a string. Words are delimited by spaces, dashes (-), or underscores (_).
Test cases for this function are as follows:
"War and Peace" => [War, and, peace]
"catcher-in-the-rye" => [catcher, in, the, rye]
"for_whom the-bell-tolls" => [for, whom, the, bell, tolls]
This function is very similar to the previous one we developed. The significant difference is that we need to return not only the number of words but also the actual words themselves. So, instead of a number, this function will return an array of strings:
function toWords (input: string): string[] {
const words = input.split(/[ _-]/);
// return the words that were split return words;}
console.log(`toWords("War and Peace"):`);console.log(toWords("War and Peace")); console.log(`toWords("catcher-in-the-rye"):`);console.log(toWords("catcher-in-the-rye"));console.log(`toWords("for_whom the-bell-tolls"):`);console.log(toWords("for_whom the-bell-tolls"));
toWords("War and Peace"):[ 'War', 'and', 'Peace' ]toWords("catcher-in-the-rye"):[ 'catcher', 'in', 'the', 'rye' ]toWords("for_whom the-bell-tolls"):[ 'for', 'whom', 'the', 'bell', 'tolls' ]
repeat will take a string and a number and return that same string repeated that number of times.
Test cases for this function are as follows:
„War", 3 => „WarWarWar"
„rye", 1 => „rye"
„bell", 0 => „"
Here are the steps to help you write this function:
function repeat (input: string, times: number): string {
There are many ways to implement this function, and we'll illustrate one approach. We can create an array with the required number of elements, and then use the array's fill method to fill it with the value of the string. In that way, we will have an array of times elements, and each element will have the input value:
// create a new array that with length of `times` // and set each element to the value of the `input` string const instances = new Array(times).fill(input);
// join the elements of the array together const result = instances.join(""); return result;}
console.log(`repeat("War", 3 ):`);console.log(repeat("War", 3 )); console.log(`repeat("rye", 1):`);console.log(repeat("rye", 1));console.log(`repeat("bell", 0):`);console.log(repeat("bell", 0));
repeat("War", 3 ):WarWarWar repeat("rye", 1):rye repeat("bell", 0):
isAlpha returns true if the string only has alpha characters (that is, letters). Test cases for this function are as follows:
"War and Peace" => false
"Atonement" => true
"1Q84" => false
Here are the steps to help you write this function:
function isAlpha (input: string): boolean {
// regex that will match any string that only has upper and //lowercase letters const alphaRegex = /^[a-z]*$/i
// test our input using the regex const result = alphaRegex.test(input); return result;}
console.log(`isAlpha("War and Peace"):`);console.log(isAlpha("War and Peace")); console.log(`isAlpha("Atonement"):`);console.log(isAlpha("Atonement"));console.log(`isAlpha("1Q84"):`);console.log(isAlpha("1Q84"));
isAlpha("War and Peace"):false isAlpha("Atonement"):true isAlpha("1Q84"):false
isBlank returns true if the string is blank, that is, it consists only of whitespace characters.
Test cases for this function are as follows:
"War and Peace" => false
" " => true
"" => true
Here are the steps to help you write this function:
function isBlank (input: string): boolean {
// test if the first character of our input is an empty space while (input[0] === " ") {// successively slice off the first character of the input input = input.slice(1); }
// the loop will stop on the first character that is not a //space.// if we're left with an empty string, we only have spaces in // the input const result = input === ""; return result;
console.log(`isBlank("War and Peace"):`);console.log(isBlank("War and Peace")); console.log(`isBlank(" "):`);console.log(isBlank(" "));console.log(`isBlank(""):`);console.log(isBlank(""));
isBlank("War and Peace"):false isBlank(" "):true isBlank(""):true
Note that there are multiple ways to implement all the preceding functions. The code shown is just one way to implement them, and these implementations are mostly for illustrative purposes. For example, a proper string utility library will need to have much more robust and extensive test suites.
Solution:
In this activity, we'll be building a TypeScript application named heat map log system that will track the baseball pitch data and ensure the integrity of the data. Perform the following steps to implement this activity:
cd activity-starter
npm install
You will now see the following files in the activity-starter directory:
declare module "HeatMapTypes" {
export interface Pitcher {
batterHotZones: Array<Array<number>>;
pitcherHotZones: Array<Array<number>>;
coordinateMap?: Array<any>;
}
}
The preceding code in the editor looks like this:
/// <reference path="./types/HeatMapTypes.d.ts"/>
import hmt = require('HeatMapTypes');
import _ = require('lodash');
export let data: hmt.Pitcher;
data = {
batterHotZones: [[12.2, -3], [10.2, -5], [3, 2]],
pitcherHotZones: [[3, 2], [-12.2, 3], [-10.2, 5]],
};
export const findMatch = (batterHotZones, pitcherHotZones) => {
return _.intersectionWith(batterHotZones, pitcherHotZones, _.isEqual);
};
data.coordinateMap = findMatch(data.batterHotZones, data.pitcherHotZones);
console.log(data.coordinateMap);
tsc heat_map_data.ts
node heat_map_data.js
Once we run the preceding commands, the following output is displayed in the terminal:
[[3,2]]
In the preceding output, the common values from both the attributes are fetched and then printed. In this case, the common values are [3, 2].
data = {
batterHotZones: [[12.2, -3], [10.2, -5], [3, 2]],
pitcherHotZones: [[3, 2], [-12.2, 3], [10.2, -5]],
};
tsc heat_map_data.ts
node heat_map_data.js
Once we run the preceding commands, the following output is displayed in the terminal:
[[10.2, -5], [3, 2]]
In the preceding output, the common values are [10.2, -5] and [3, 2]. Finally, we built a heat map log system that will track the baseball pitch data and ensure the integrity of the data.
Solution:
npx ts-node index.ts Not implemented!
So we have work to do. Several functions are not yet implemented. Let's start by looking at flights.ts. There is a partial implementation there as we have an interface called Flights that describes the attributes of a flight, a list of available flights implementing that interface, and even a method to fetch the flights, called getDestinations. We need to implement logic to check to see whether the number of seats we want to book are still available, logic that can hold seats while we confirm a reservation, and logic that converts those seats held into reserved seats once our payment has been processed.
export const checkAvailability = (
flight: Flight,
seatsRequested: number
): boolean => seatsRequested <= flight.seatsRemaining - flight.seatsHeld;
export const holdSeats = (flight: Flight, seatsRequested: number): Flight => {
if (flight.seatsRemaining - flight.seatsHeld < seatsRequested) {
throw new Error('Not enough seats remaining!');
}
flight.seatsHeld += seatsRequested;
return flight;
};
export const reserveSeats = (
flight: Flight,
seatsRequested: number
): Flight => {
if (flight.seatsHeld < seatsRequested) {
throw new Error('Seats were not held!');
}
flight.seatsHeld -= seatsRequested;
flight.seatsRemaining -= seatsRequested;
return flight;
};
That should do it for flights.ts. However, our program still won't run until we implement bookings.ts.
const bookingsFactory = (bookingNumber: number) => (
flight: Flight,
seatsHeld: number
): Booking => ({
bookingNumber: bookingNumber++,
flight,
paid: false,
seatsHeld,
seatsReserved: 0,
});
Our factory takes bookingNumber as an argument to initialize this value and then increments the number each time it creates a new booking. We assign some default values to the booking to confirm to the Booking interface already provided in the module.
const createBooking = bookingsFactory(1);
export const startBooking = (
flight: Flight,
seatsRequested: number
): Booking => {
if (checkAvailability(flight, seatsRequested)) {
holdSeats(flight, seatsRequested);
return createBooking(flight, seatsRequested);
}
throw new Error('Booking not available!');
};
export const processPayment = (booking: Booking): Booking => {
booking.paid = true;
return booking;
};
export const completeBooking = (booking: Booking): Booking => {
reserveSeats(booking.flight, booking.seatsHeld);
booking.seatsReserved = booking.seatsHeld;
booking.seatsHeld = 0;
return booking;
};
npx ts-node index.ts
Booked to Lagos {
bookingNumber: 1,
flight: {
destination: 'Lagos',
flightNumber: 1,
seatsHeld: 0,
seatsRemaining: 29,
time: '5:30'
},
paid: true,
seatsHeld: 0,
seatsReserved: 1
}
//...
Istanbul flight {
destination: 'Istanbul',
flightNumber: 7,
seatsHeld: 0,
seatsRemaining: 0,
time: '14:30'
}
Booking not available!
Solution:
test('get destinations', () => {
expect(destinations).toHaveLength(7);
});
We could test each of the individual destinations and their properties as well.
test('checking availability', () => {
const destinations = getDestinations();
expect(checkAvailability(destinations[0], 3)).toBeTruthy();
expect(checkAvailability(destinations[1], 5)).toBeFalsy();
expect(checkAvailability(destinations[2], 300)).toBeFalsy();
expect(checkAvailability(destinations[3], 3)).toBeTruthy();
});
The first destination has at least three seats available. The second does not have five available, and so on.
test('hold seats', () => {
expect.assertions(3);
flight = holdSeats(flight, 3);
expect(flight.seatsHeld).toBe(3);
flight = holdSeats(flight, 13);
expect(flight.seatsHeld).toBe(16);
try {
holdSeats(flight, 15);
} catch (e) {
expect(e.message).toBe('Not enough seats remaining!');
}
});
Note that in order to ensure that the catch block was reached, we're expecting three assertions in this test. Without that, the test would still turn green even if, for some reason, the last call to holdSeats didn't throw an error.
test('reserve seats', () => {
expect.assertions(3);
flight = reserveSeats(flight, 3);
expect(flight.seatsRemaining).toBe(27);
flight = reserveSeats(flight, 13);
expect(flight.seatsRemaining).toBe(14);
try {
reserveSeats(flight, 1);
} catch (e) {
expect(e.message).toBe('Seats were not held!');
}
});
This test runs through a few scenarios, including another error condition. In some cases, it might be appropriate to put error conditions in separate tests. A good rule of thumb for this is that each of your tests should be easy to comprehend and maintain. If any module or function gets to be too big, just break it up.
describe('bookings tests', () => {
test('create a booking', () => {
const booking = startBooking(destinations[0], 3);
expect(booking).toEqual({
bookingNumber: 1,
flight: destinations[0],
paid: false,
seatsHeld: 3,
seatsReserved: 0,
});
});
test('pay for a booking', () => {
let booking = startBooking(destinations[0], 3);
booking = processPayment(booking);
expect(booking.paid).toBe(true);
});
test('complete a booking', () => {
let booking = startBooking(destinations[0], 3);
booking = processPayment(booking);
booking = completeBooking(booking);
expect(booking.paid).toBe(true);
expect(booking.seatsReserved).toBe(3);
});
});
npm test
> jest --coverage --testRegex="^((?!-solution).)*\.test\.tsx?$"
PASS ./bookings.test.ts
PASS ./flights.test.ts
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 97.14 | 83.33 | 100 | 96.97 |
bookings.ts | 94.74 | 50 | 100 | 94.44 | 34
flights.ts | 100 | 100 | 100 | 100 |
-------------|---------|----------|---------|---------|-------------------
Test Suites: 2 passed, 2 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: 1.782 s
Ran all test suites.
The tests passed! But we haven't hit 100% line coverage yet. We can actually open up the coverage report, which will be inside the coverage/lcov-report directory in the root of our project. The coverage tool (Istanbul) that comes bundled with Jest will produce an HTML report that we can open in any browser. This will show us the exact piece of code that hasn't been covered:
describe('error scenarios', () => {
test('booking must have availability', () => {
expect.assertions(1);
try {
startBooking(destinations[6], 8);
} catch (e) {
expect(e.message).toBe('Booking not available!');
}
});
});
There's no particular need to have a new describe block, but in this case, it might make the code a bit cleaner. Use describe and test blocks for readability and maintenance.
npm test
> jest --coverage --testRegex="^((?!-solution).)*\.test\.tsx?$" PASS ./bookings-solution.test.ts
PASS ./flights-solution.test.ts
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
bookings.ts | 100 | 100 | 100 | 100 |
flights.ts | 100 | 100 | 100 | 100 |
-------------|---------|----------|---------|---------|-------------------
Test Suites: 2 passed, 2 total
Tests: 8 passed, 8 total
Snapshots: 0 total
Time: 0.694 s, estimated 1 s
Ran all test suites.
We've hit our goal of 100% line coverage!
Solution:
In this activity, we'll be building a user authentication system that will pass login data to a backend API to register and sign users into our baseball scorecard application. Perform the following steps to implement this activity:
The activity-solution directory contains the completed solution code, and the activity-starter directory provides the basic start code to work with.
npm install
You will now see the following files in the activity-starter directory:
interface ILogin{
email: string;
password:string;
}
export class Login{
email: string;
password: string;
constructor(args: ILogin){
this.email = args.email;
this.password = args.password;
}
}
interface IAuth{
user: Login;
source: string;
}
export default class Auth{
user: Login;
source: string;
constructor(args: IAuth){
this.user = args.user;
this.source = args.source;
}
}
validUser(): string{
const { email, password } = this.user;
if(email === "[email protected]" && password === "secret123"){
return `Validating user…User is authenticated: true`;
} else {
return `Validating user…User is authenticated: false`;
}
}
const goodUser = new Login({
email: "[email protected]",
password: "secret123"
});
const badUser = new Login({
email: "[email protected]",
password: "whoops"
});
const authAttemptFromGoodUser = new Auth({
user: goodUser,
source: "Google"
});
console.log(authAttemptFromGoodUser.validUser());
const authAttemptFromBadUser = new Auth({
user: badUser,
source: "Google"
});
console.log(authAttemptFromBadUser.validUser());
tsc auth.ts
node auth.js
Once we run the preceding commands, the following output is displayed in the terminal:
Validating user…User is authenticated: true
Validating user…User is authenticated: false
In the preceding output, the validUser() function returns a true value when the correct details of user and password are passed. When incorrect details are passed, the function returns a false value.
Solution:
interface UserObj {
email: string
loginAt?: number
token?: string
}
interface UserClass {
user: UserObj
getUser(): UserObj
login(user: UserObj, password: string):UserObj
}
class User implements UserClass {
user:UserObj
getUser(): UserObj {
return this.user
}
login(user: UserObj, password: string): UserObj {
// set props user object
return this.user = user
}
}
const newUserClass:UserClass = new User()
const newUser: UserObj = {
email: "[email protected]",
loginAt: new Date().getTime(),
token: "123456"
}
console.log(
newUserClass.login(newUser, "password123")
)
console.log(
newUserClass.getUser()
)
The expected output is as follows:
{ email: '[email protected]', loginAt: 1614068072515, token: '123456' }
{ email: '[email protected]', loginAt: 1614068072515, token: '123456' }
This user management class is a central location where you can isolate all your application's user-related functions and rules. The rules you have crafted by using interfaces to implement your code will ensure that your code is better supported, easier to work with, and bug-free.
Solution:
class Motor {
private name: string
wheels: number
bodyType: string
constructor(name: string, wheels: number, bodyType: string) {
this.name = name
this.wheels = wheels
this.bodyType = bodyType
}
protected getName(): string {
return this.name
}
buildMotor() {
return {
wheels: this.wheels,
bodyType: this.bodyType,
name: this.name
}
}
}
class Car extends Motor {
rideHeight: number
constructor(name: string, wheels: number, bodyType: string, rideHeight: number) {
super(name, wheels, bodyType)
this.rideHeight = rideHeight
}
_buildMotor() {
return {
...super.buildMotor,
rideHeight: this.rideHeight
}
}
}
class Truck extends Motor {
offRoad: boolean
constructor(name: string, wheels: number, bodyType: string, offRoad: boolean) {
super(name, wheels, bodyType)
this.offRoad = offRoad
}
_buildMotor() {
return {
wheels: this.wheels,
bodyType: this.bodyType,
offRoad: this.offRoad
}
}
}
class Suv extends Truck {
roofRack: boolean
thirdRow: boolean
constructor(name: string, wheels: number, bodyType: string,
offRoad: boolean, roofRack: boolean, thirdRow: boolean) {
super(name, wheels, bodyType, offRoad)
this.roofRack = roofRack;
this.thirdRow = thirdRow
}
}
const car: Car = new Car('blueBird', 4, 'sedan', 14)
const truck: Truck = new Truck('blueBird', 4, 'sedan', true)
const suv: Suv = new Suv('xtrail', 4, 'box', true, true, true)
console.log(car)
console.log(truck)
console.log(suv)
You will obtain the following output:
Car { name: 'blueBird', wheels: 4, bodyType: 'sedan', rideHeight: 14 }
Truck { name: 'blueBird', wheels: 4, bodyType: 'sedan', offRoad: true }
Suv {
name: 'xtrail',
wheels: 4,
bodyType: 'box',
offRoad: true,
roofRack: true,
thirdRow: true
}
In this activity, you created the bare minimum classes that we require for the web application. We have shown how we can build complexity, reuse, and extend application code with inheritance in TypeScript.
Solution:
type Motor = {
color: string;
doors: number;
wheels: number;
fourWheelDrive: boolean;
}
type Truck = {
doubleCab: boolean;
winch: boolean;
}
type PickUpTruck = Motor & Truck;
function TruckBuilder (truck: PickUpTruck): PickUpTruck {
return truck
}
const pickUpTruck: PickUpTruck = {
color: 'red',
doors: 4,
doubleCab: true,
wheels: 4,
fourWheelDrive: true,
winch: true
}
console.log (
TruckBuilder(pickUpTruck)
)
You should see the following output once you run the file:
{
color: 'red',
doors: 4,
doubleCab: true,
wheels: 4,
fourWheelDrive: true,
winch: true
}
Solution:
type LandPack = {
height: number,
weight: number,
type: "land",
label?: string };
type AirPack = {
height: number,
weight: number,
type : "air",
label?: string };
type ComboPack = LandPack | AirPack
class Shipping {
Process(pack: ComboPack) {
// check package type
if(pack.type === "land") {
return this.ToLand(pack);
} else {
return this.ToAir(pack);
}
}
ToAir(pack: AirPack): AirPack {
pack.label = "air cargo"
return pack;
}
ToLand(pack: LandPack): LandPack {
pack.label = "land cargo"
return pack;
}
}
const airPack: AirPack = {
height: 5,
weight: 10,
type: "air",
};
const landPack: LandPack = {
height: 5,
weight: 10,
type: "land",
};
const shipping = new Shipping;
console.log(
shipping.Process(airPack)
);
console.log(
shipping.Process(landPack)
);
Once you run the file, you will obtain the following output:
{ height: 5, weight: 10, type: 'air', label: 'air cargo' }
{ height: 5, weight: 10, type: 'land', label: 'land cargo' }
Solution:
interface PackageStatus {
[status: string]: boolean;}
type Package = {
packageStatus: PackageStatus,
barcode: number,
weight: number
}
class PackageProcess {
pack: Package
constructor(pack: Package) {
this.pack = pack;
}
Status () {
return this.pack.packageStatus;
}
UpdateStatus(status: string, state: boolean) {
this.pack.packageStatus[status] = state;
return this.Status();}
}
const pack: Package = {
packageStatus: {"shipped": false, "packed": true, "delivered": true},
barcode: 123456,
weight: 28
};
const processPack = new PackageProcess(pack)
console.log(processPack.Status());
console.log(
processPack.UpdateStatus("shipped", true)
);
Once you run the file, you should obtain the following output:
{ shipped: false, packed: true, delivered: true }
{ shipped: true, packed: true, delivered: true }
The first line in the preceding output displays the original pack status, whereas the second line displays the updated pack status.
Solution:
class Person {
constructor (public firstName: string,
public lastName: string,
public birthDate: Date) {
}
}
private _title: string;
public get title() {
return this._title;
}
public set title(value: string) {
this._title = value;
}
public getFullName() {
return `${this.firstName} ${this.lastName}`;
}
public getAge() {
// only sometimes accurate
const now = new Date();
return now.getFullYear() – this.birthDate.getFullYear();
}
const count = {};
type Constructable = { new (...args: any[]): {} };
function CountClass(counterName: string) {
return function <T extends Constructable>(constructor: T) {
// wrapping code here
}
}
const wrappedConstructor: any = function (...args: any[]) {
const result = new constructor(...args);
if (count[counterName]) {
count[counterName]+=1;
} else {
count[counterName]=1;
}
return result;
};
wrappedConstructor.prototype = constructor.prototype;
return wrappedConstructor;
function CountMethod(counterName: string) {
return function (target: any, propertyName: string,
descriptor: PropertyDescriptor) {
// method wrapping code here
}
}
if (descriptor.value) {
// method decoration code
}
if (descriptor.get) {
// get property accessor decoration code
}
if (descriptor.set) {
// set property accessor decoration code
}
// method decoration code
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
// counter management code here
return original.apply(this, args);
}
// get property accessor decoration code
const original = descriptor.get;
descriptor.get = function () {
// counter management code here
return original.apply(this, []);
}
// set property accessor decoration code
const original = descriptor.set;
descriptor.set = function (value: any) {
// counter management code here
return original.apply(this, [value]);
}
// counter management code
if (count[counterName]) {
count[counterName]+=1;
} else {
count[counterName]=1;
}
@CountClass('person')
class Person{
@CountMethod('person-full-name')
public getFullName() {
@CountMethod('person-age')
public getAge() {
@CountMethod('person-title')
public get title() {
const first = new Person("Brendan", "Eich", new Date(1961,6,4));
const second = new Person("Anders", "Hejlsberg ", new Date(1960,11,2));
const third = new Person("Alan", "Turing", new Date(1912,5,23));
const fname = first.getFullName();
const sname = second.getFullName();
const tname = third.getFullName();
const fage = first.getAge();
const sage = second.getAge();
const tage = third.getAge();
if (!first.title) {
first.title = "Mr."
}
if (!second.title) {
second.title = "Mr."
}
if (!third.title) {
third.title = "Mr."
}
console.log(count);
Once you run the file, you will obtain the following output on the console:
{
person: 3,
'person-full-name': 3,
'person-age': 3,
'person-title': 6
}
Solution:
interface Team {
score: number;
name: string;
}
class BasketBallGame {
private team1: Team;
private team2: Team;
constructor(teamName1: string, teamName2: string) {
this.team1 = { score: 0, name: teamName1 };
this.team2 = { score: 0, name: teamName2 };
}
getScore() {
return `${this.team1.score}:${this.team2.score}`;
}
updateScore(byPoints: number, updateTeam1: boolean) {
if (updateTeam1) {
this.team1.score += byPoints;
} else {
this.team2.score += byPoints;
}
}
}
type Constructable = { new (...args: any[]): {} };
function Authenticate(permission: string) {
return function <T extends Constructable>(constructor: T) {
const wrappedConstructor: any = function (...args: any[]) {
if (Reflect.hasMetadata("permissions", wrappedConstructor)) {
const permissions = Reflect.getMetadata("permissions",
wrappedConstructor) as string[];
if (!permissions.includes(permission)) {
throw Error(`Permission ${permission} not present`);
}
}
const result = new constructor(...args);
return result;
};
wrappedConstructor.prototype = constructor.prototype;
return wrappedConstructor;
};
}
Reflect.defineMetadata("permissions", ["canUpdateScore"], BasketBallGame);
@Authenticate("canUpdateScore")
class BasketBallGame {
function MeasureDuration() {
return function (target: any, propertyName: string,
descriptor: PropertyDescriptor) {
if (descriptor.value) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
const start = Date.now();
const result = original.apply(this, args);
const end = Date.now();
const duration = end-start;
if (Reflect.hasMetadata("durations", target, propertyName)) {
const existing = Reflect.getMetadata("durations",
target, propertyName) as number[];
Reflect.defineMetadata("durations", existing.concat(duration),
target, propertyName);
} else {
Reflect.defineMetadata("durations", [duration],
target, propertyName)
}
return result;
}
}
}
}
@MeasureDuration()
updateScore(byPoints: number, updateTeam1: boolean) {
function Audit(message: string) {
return function (target: any, propertyName: string,
descriptor: PropertyDescriptor) {
if (descriptor.value) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
const result = original.apply(this, args);
console.log(`[AUDIT] ${message} (${propertyName}) called with:`)
console.log("[AUDIT]", args);
console.log("[AUDIT] and returned result:")
console.log("[AUDIT]", result);
return result;
}
}
}
}
@MeasureDuration()
@Audit("Updated score")
updateScore(byPoints: number, updateTeam1: boolean) {
function OneTwoThree(target: any, propertyKey: string,
parameterIndex: number) {
if (Reflect.hasMetadata("one-two-three", target, propertyKey)) {
const existing = Reflect.getMetadata("one-two-three",
target, propertyKey) as number[];
Reflect.defineMetadata("one-two-three",
existing.concat(parameterIndex), target, propertyKey);
} else {
Reflect.defineMetadata("one-two-three",
[parameterIndex], target, propertyKey);
}
}
function Validate() {
return function (target: any, propertyKey:string,
descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
// validate parameters
if (Reflect.hasMetadata("one-two-three",
target, propertyKey)) {
const markedParams = Reflect.getMetadata("one-two-three",
target, propertyKey) as number[];
for (const marked of markedParams) {
if (![1,2,3].includes(args[marked])) {
throw Error(`The parameter at position ${marked} can only be 1, 2 or 3`);
}
}
}
return original.apply(this, args);
}
}
}
@MeasureDuration()
@Audit("Updated score")
@Validate()
updateScore(@OneTwoThree byPoints: number, updateTeam1: boolean) {
const game = new BasketBallGame("LA Lakers", "Boston Celtics");
game.updateScore(3, true);
game.updateScore(2, false);
game.updateScore(2, true);
game.updateScore(2, false);
game.updateScore(2, false);
game.updateScore(2, true);
game.updateScore(2, false);
When you run the file, the console should reflect the application of all decorators:
[AUDIT] Updated score (updateScore) called with arguments:
[AUDIT] [ 3, true ]
[AUDIT] and returned result:
[AUDIT] undefined
[AUDIT] Updated score (updateScore) called with arguments:
[AUDIT] [ 2, false ]
[AUDIT] and returned result:
[AUDIT] undefined
[AUDIT] Updated score (updateScore) called with arguments:
[AUDIT] [ 2, true ]
[AUDIT] and returned result:
[AUDIT] undefined
[AUDIT] Updated score (updateScore) called with arguments:
[AUDIT] [ 2, false ]
[AUDIT] and returned result:
[AUDIT] undefined
[AUDIT] Updated score (updateScore) called with arguments:
[AUDIT] [ 2, false ]
[AUDIT] and returned result:
[AUDIT] undefined
[AUDIT] Updated score (updateScore) called with arguments:
[AUDIT] [ 2, true ]
[AUDIT] and returned result:
[AUDIT] undefined
[AUDIT] Updated score (updateScore) called with arguments:
[AUDIT] [ 2, false ]
[AUDIT] and returned result:
[AUDIT] undefined
7:8
Solution:
In this activity, we will build a basic calculator that utilizes DI to evaluate mathematical expressions, as well as logging its output to either the console or a file:
export interface Operator {
readonly symbol: string;
evaluate(a: number, b: number): number;
}
You need to create this file in the src/interfaces folder and save it as operator.interface.ts.
import { Operator } from '../interfaces/operator.interface';
export class AddOperator implements Operator {
readonly symbol = '+';
public evaluate(a: number, b: number) {
return a + b;
}
}
The preceding code needs to be written in a file called add.operator.ts in srcoperators.
import { injectable } from 'inversify';
import { Operator } from '../interfaces/operator.interface';
@injectable()
export class AddOperator implements Operator {
readonly symbol = '+';
public evaluate(a: number, b: number) {
return a + b;
}
}
export const TYPES = {
AddOperator: Symbol.for('AddOperator'),
};
This code needs to be written in a new file saved in the src ypes folder. We have named this file index.ts.
import { injectable, inject } from 'inversify';
import { TYPES } from '../types';
import { AddOperator } from '../operators/add.operator';
@injectable()
export class Calculator {
constructor(@inject(TYPES.AddOperator) private addOperator: AddOperator) {}
evaluate(expression: string) {
const expressionParts = expression.match(/[d.]+|D+/g);
if (expressionParts === null) return null;
// for now, we're only going to support basic expressions: X+Y
const [operandA, operator, operandB] = expressionParts;
if (operator !== this.addOperator.symbol) {
throw new Error(`Unsupported operator. Expected ${this.addOperator.symbol}, received: ${operator}.`);
}
const result = this.addOperator.evaluate(Number(operandA), Number(operandB));
return result;
}
}
Here, we implement a Calculator class that has a single method – evaluate, which takes in an expression as a string, and returns the result for that expression. This code needs to be written in a new file called index.ts, saved in the src/calculator folder.
Note
The current implementation only supports expressions in the form of X+Y (where X and Y can be any numbers). We'll fix that later in the activity.
Calculator gets AddOperator in DI, and in order to evaluate the expression, it first runs through a regular expression to split it by numbers, and then it destructures the result array. Lastly, it uses the evaluate method of AddOperator to perform the final expression evaluation.
This means that the calculator's responsibility is only to destructure the expression into its individual parts, and then pass it off to AddOperator to handle the math evaluation logic. This demonstrates how using DI helps to retain the single responsibility principle of SOLID.
import { Container } from 'inversify';
import { Calculator } from './calculator/index';
import { Operator } from './interfaces/operator.interface';
import { AddOperator } from './operators/add.operator';
import { TYPES } from './types';
export const container = new Container();
container.bind<Operator>(TYPES.AddOperator).to(AddOperator);
container.bind(Calculator).toSelf();
import 'reflect-metadata';
import { Calculator } from './calculator/index';
import { container } from './ioc.config';
const calculator = container.get(Calculator);
try {
const result = calculator.evaluate('13+5');
console.log('result is', result);
} catch (err) {
console.error(err);
}
This is just using our previously defined IoC container and asking it for a Calculator instance. This is how we ask for instances of symbols explicitly in InversifyJS in an imperative API, which we need here, since we want to kick things off. Since InversifyJS is the one creating Calculator, it also looks at its constructor and sees that we've asked for a TYPES.AddOperator, which it then looks up in the IoC container again to resolve and gives that to the calculator's constructor.
Once you run this file, you should obtain the following output:
result is 18
Note that you can either run the code by executing npm start in the activity-starter folder or by executing npx ts-node main.ts in the src folder.
Note
If the AddOperator class were also to require dependencies using @inject, the same process described above would be repeated again to get them, and so on recursively until all dependencies have been resolved.
// operators/subtract.operator.ts
import { injectable } from 'inversify';
import { Operator } from '../interfaces/operator.interface';
@injectable()
export class SubtractOperator implements Operator {
readonly symbol = '-';
public evaluate(a: number, b: number) {
return a - b;
}
}
// operators/multiply.operator.ts
import { injectable } from 'inversify';
import { Operator } from '../interfaces/operator.interface';
@injectable()
export class MultiplyOperator implements Operator {
readonly symbol = '*';
public evaluate(a: number, b: number) {
return a * b;
}
}
// operators/divide.operator.ts
import { injectable } from 'inversify';
import { Operator } from '../interfaces/operator.interface';
@injectable()
export class DivideOperator implements Operator {
readonly symbol = '/';
public evaluate(a: number, b: number) {
return a / b;
}
}
Now, instead of creating an injection token for each Operator, injecting each one into Calculator, and then acting on each, we can create a more generic implementation of Calculator with the help of the @multiInject decorator. This decorator allows an injection token to be specified and an array of all implementations registered for that token to be obtained. This way, Calculator is not even coupled to an abstraction for any specific operator and only gets a dynamic list of operators, which can have any implementation as long as it conforms to the Operator interface.
export const TYPES = {
Operator: Symbol.for('Operator'),
};
This replaces our AddOperator symbol from earlier with a more generic one.
import { injectable, multiInject } from 'inversify';
import { Operator } from '../interfaces/operator.interface';
import { tryParseNumberString, tryParseOperatorSymbol } from "../utils/math";
import { TYPES } from '../types';
@injectable()
export class Calculator {
constructor(@multiInject(TYPES.Operator) private operators: Operator[]) {}
evaluate(expression: string) {
// same as before…
}
}
Note that in further steps, you will need to modify the preceding code to include two functions, tryParseNumberString and tryParseOperatorSymbol. Both these functions are created in the math.ts file placed in the src/utils folder.
import { Container } from 'inversify';
import { Calculator } from './calculator';
import { Operator } from './interfaces/operator.interface';
import { AddOperator } from './operators/add.operator';
import { DivideOperator } from './operators/divide.operator';
import { MultiplyOperator } from './operators/multiply.operator';
import { SubtractOperator } from './operators/subtract.operator';
import { TYPES } from './types';
export const container = new Container();
container.bind<Operator>(TYPES.Operator).to(AddOperator);
container.bind<Operator>(TYPES.Operator).to(SubtractOperator);
container.bind<Operator>(TYPES.Operator).to(MultiplyOperator);
container.bind<Operator>(TYPES.Operator).to(DivideOperator);
container.bind(Calculator).toSelf();
evaluate(expression: string) {
// ...
const parsedExpressionParts = expressionParts.map(part => {
const numberParseResult = tryParseNumberString(part);
if (numberParseResult.isNumberString) return numberParseResult.number;
const operatorParseResult = tryParseOperatorSymbol(part, this.operators);
if (operatorParseResult.isOperatorSymbol) return operatorParseResult.operator;
throw new Error(`Unexpected part: ${part}`);
});
}
This will give us back an array of numbers and operators.
Note
Try to implement tryParseNumberString and tryParseOperatorSymbol yourself. However, you can refer to utils/math.ts to help you complete this step.
evaluate(expression: string) {
// ...
const { result } = parsedExpressionParts.reduce<{ result: number; queuedOperator: Operator | null }>((acc, part) => {
if (typeof part === 'number') {
// this is the first number we've encountered, just set the result to that.
if (acc.queuedOperator === null) {
return { ...acc, result: part };
}
// there's a queued operator – evaluate the previous result with this and
// clear the queued one.
return {
queuedOperator: null,
result: acc.queuedOperator.evaluate(acc.result, part),
};
}
// this is an operator – queue it for later execution
return {
...acc,
queuedOperator: part,
};
}, { result: 0, queuedOperator: null });
return result;
}
// operators/index.ts
export * from './add.operator';
export * from './divide.operator';
export * from './multiply.operator';
export * from './subtract.operator';
// ioc.config.ts
import { Container } from 'inversify';
import { Calculator } from './calculator';
import { Operator } from './interfaces/operator.interface';
import * as Operators from './operators';
import { TYPES } from './types';
export const container = new Container();
Object.values(Operators).forEach(Operator => {
container.bind<Operator>(TYPES.Operator).to(Operator);
});
container.bind(Calculator).toSelf();
This means we now import an Operators object from the barrel file, which includes everything that's exposed there. We take the values of that barrel object and bind each one to TYPES.Operator, generically.
This means that adding another Operator object only requires us to create a new class that implements the Operator interface and add it to our operators/index.ts file. The rest of the code should work without any changes.
import 'reflect-metadata';
import { Calculator } from './calculator';
import { container } from './ioc.config';
const calculator = container.get(Calculator);
try {
const result = calculator.evaluate('13*10+20');
console.log('result is', result);
} catch (err) {
console.error(err);
}
When you run the main.ts file (using npx ts-node main.ts), you should obtain the following output:
result is 150
Note
The filesystem implementation will only work in a Node.js environment, since it will use some modules only available to it.
export interface Logger {
log(message: string, ...args: any[]): void;
warn(message: string, ...args: any[]): void;
error(message: string, ...args: any[]): void;
}
This will serve as the public API that the consumers wanting a logger can use, and that our implementations will need to adhere to.
import { injectable } from 'inversify';
import { Logger } from '../interfaces/logger.interface';
@injectable()
export class ConsoleLogger implements Logger {
log(message: string, ...args: any[]) {
console.log('[LOG]', message, ...args);
}
warn(message: string, ...args: any[]) {
console.warn('[WARN]', message, ...args);
}
error(message: string, ...args: any[]) {
console.error('[ERROR]', message, ...args);
}
}
This is a simple wrapper class around the console object that's built into browser engines and Node.js. It adheres to our Logger interface, and so allows consumers to depend on it. For the example, we've also added the type of the message to the beginning of the actual output.
// types/index.ts
export const TYPES = {
Operator: Symbol.for('Operator'),
Logger: Symbol.for('Logger'),
};
The updated code for the src/ioc.config.ts file is as follows:
// ioc.config.ts
import { Container } from 'inversify';
import { Calculator } from './calculator';
import { Logger } from './interfaces/logger.interface';
import { Operator } from './interfaces/operator.interface';
import { ConsoleLogger } from './logger/console.logger';
import * as Operators from './operators';
import { TYPES } from './types';
export const container = new Container();
Object.values(Operators).forEach(Operator => {
container.bind<Operator>(TYPES.Operator).to(Operator);
});
container.bind(Calculator).toSelf();
container.bind<Logger>(TYPES.Logger).to(ConsoleLogger);
import { injectable, multiInject, inject, optional } from 'inversify';
import { Operator } from '../interfaces/operator.interface';
import { TYPES } from '../types';
import { tryParseNumberString, tryParseOperatorSymbol } from '../utils/math';
import { Logger } from '../interfaces/logger.interface';
@injectable()
export class Calculator {
constructor(
@multiInject(TYPES.Operator) private operators: Operator[],
@inject(TYPES.Logger) @optional() private logger?: Logger
) {}
evaluate(expression: string) {
// ...
const { result } = parsedExpressionParts.reduce<{ result: number; queuedOperator: Operator | null }>( ... );
this.logger && this.logger.log(`Calculated result of expression: ${expression} to be: ${result}`);
return result;
}
}
Notice that we use the @optional decorator to indicate to InversifyJS that Calculator doesn't require a Logger to operate, but if it has one it can inject, Calculator can use it. This is also why it's marked as an optional argument in the constructor, and why we need to check whether it exists before calling the log method.
The output to the console when running it should be as follows:
[LOG] Calculated result of expression:13*10+20 is 150
Now, let's say we want to replace our console-based logger with a file-based one, which will persist across runs so that we can track the calculator's evaluation history.
import fs from 'fs';
import { injectable } from 'inversify';
import { Logger } from '../interfaces/logger.interface';
@injectable()
export class FileLogger implements Logger {
private readonly loggerPath: string = '/tmp/calculator.log';
log(message: string, ...args: any[]) {
this.logInternal('LOG', message, args);
}
warn(message: string, ...args: any[]) {
this.logInternal('WARN', message, args);
}
error(message: string, ...args: any[]) {
this.logInternal('ERROR', message, args);
}
private logInternal(level: string, message: string, ...args: any[]) {
fs.appendFileSync(this.loggerPath, this.logLineFormatter(level, message, args));
}
private logLineFormatter(level: string, message: string, ...args: any[]) {
return `[${level}]: ${message}${args} `;
}
}
For console-based logging, use this command:
container.bind<Logger>(TYPES.Logger).to(ConsoleLogger);
For file-based logging, use this command:
container.bind<Logger>(TYPES.Logger).to(FileLogger);
Make sure to import this logger correctly in the ioc.config.ts file.
The final output to the file is as follows:
Solution:
Let's build this type up, step by step:
type PartialPrimitive = string | number | boolean | symbol | bigint | Function | Date;
type DeepPartial<T> = T extends PartialPrimitive ? T : Partial<T>;
Next, we need to handle more complex structures – such as arrays, sets, and maps. These require using the infer keyword, and in addition to that, require some more "manual wiring" for each of these types.
type DeepPartial<T> =
T extends PartialPrimitive
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: Partial<T>;
This would've worked, but due to current limitations in TypeScript at the time of writing, this doesn't compile, since DeepPartial<T> circularly references itself:
To work around this, we'll create a helper type, DeepPartialArray<T>, and use it:
interface DeepPartialArray<T> extends Array<DeepPartial<T>> {}
type DeepPartial<T> =
T extends PartialPrimitive
? T
: T extends Array<infer U>
? DeepPartialArray<U>
: Partial<T>;
This works around the problem and compiles fine.
interface DeepPartialArray<T> extends Array<DeepPartial<T>> {}
interface DeepPartialSet<T> extends Set<DeepPartial<T>> {}
type DeepPartial<T> = T extends PartialPrimitive
? T
: T extends Array<infer U>
? DeepPartialArray<U>
: T extends Set<infer U>
? DeepPartialSet<U>
: Partial<T>;
interface DeepPartialArray<T> extends Array<DeepPartial<T>> {}
interface DeepPartialSet<T> extends Set<DeepPartial<T>> {}
interface DeepPartialMap<K, V> extends Map<DeepPartial<K>, DeepPartial<V>> {}
type DeepPartial<T> = T extends PartialPrimitive
? T
: T extends Array<infer U>
? DeepPartialArray<U>
: T extends Map<infer K, infer V>
? DeepPartialMap<K, V>
: T extends Set<infer U>
? DeepPartialSet<U>
: Partial<T>;
Note
This workaround is no longer needed as of TypeScript 3.7.
type DeepPartial<T> = T extends PartialPrimitive
? T
: T extends Array<infer U>
? DeepPartialArray<U>
: T extends Map<infer K, infer V>
? DeepPartialMap<K, V>
: T extends Set<infer U>
? DeepPartialSet<U>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
This completes the DeepPartial<T> implementation.
A great use case for the DeepPartial<T> type is in a server-side PATCH method handler, which updates a given resource with new data. In PATCH requests, all fields are usually optional:
import express from 'express';
const app = express();
app.patch('/users/:userId', async (req, res) => {
const userId = req.params.userId;
const userUpdateData: DeepPartial<User> = req.body;
const user = await User.getById(userId);
await user.update(userUpdateData);
await user.save();
res.status(200).end(user);
});
Notice that we use DeepPartial<User> to correctly type the body of the request, before passing it in the update method:
As can be seen in the preceding figure, due to the usage of DeepPartial<T>, the request's body is typed correctly, such that all fields are optional, including nested ones.
Solution:
const xhr = new XMLHttpRequest();
const url = getSearchUrl(value);
xhr.open('GET', url); xhr.send();
xhr.onload = function() { const data = JSON.parse(this.response) as SearchResultApi; }
if (data.results.length === 0) { clearResults(value); }
else { const resultMovie = data.results[0]; }
const movieXhr = new XMLHttpRequest();
const movieUrl = getMovieUrl(resultMovie.id);
movieXhr.open('GET', movieUrl); movieXhr.send();
movieXhr.onload = function () { const movieData: MovieResultApi = JSON.parse(this.response);
const peopleXhr = new XMLHttpRequest();
const peopleUrl = getPeopleUrl(resultMovie.id);
peopleXhr.open('GET', peopleUrl); peopleXhr.send();
const data = JSON.parse(this.response) as PeopleResultApi;
The people data has cast and crew properties. We'll only take the first six cast members, so first sort the cast property according to the order property of the cast members. Then slice off the first six cast members into a new array:
data.cast.sort((f, s) => f.order - s.order); const mainActors = data.cast.slice(0, 6);
const characters: Character[] = mainActors.map(actor => ({ name: actor.character, actor: actor.name, image: actor.profile_path }))
const directors = data.crew .filter(person => person.department === "Directing" && person.job === "Director") .map(person => person.name) const directedBy = directors.join(" & ");
const writers = data.crew .filter(person => person.department === "Writing" && person.job === "Writer") .map(person => person.name); const writtenBy = writers.join(" & ");
const movie: Movie = { id: movieData.id, title: movieData.title, tagline: movieData.tagline, releaseDate: new Date(movieData.release_date), posterUrl: movieData.poster_path, backdropUrl: movieData.backdrop_path, overview: movieData.overview, runtime: movieData.runtime,
characters: characters, directedBy: directedBy, writenBy: writtenBy }
showResults(movie);
You should see the following in your browser:
Solution:
const getJsonData = (url: string):Promise<any> => {}
const getJsonData = (url: string):Promise<any> => { return fetch(url) .then(response => response.json());}
const searchUrl = getSearchUrl(value);
return getJsonData(searchUrl)
return getJsonData(url) .then((data:SearchResultApi) => { }
.then((data:SearchResultApi) => { if (data.results.length === 0) { throw Error("Not found"); } return data.results[0]; })
}) .then(movieResult => { const movieUrl = getMovieUrl(movieResult.id); const peopleUrl = getPeopleUrl(movieResult.id); })
const movieUrl = getMovieUrl(movieResult.id); const peopleUrl = getPeopleUrl(movieResult.id); const dataPromise: Promise<MovieResultApi> = getJsonData(movieUrl); const peoplePromise: Promise<PeopleResultApi> = getJsonData(peopleUrl);
const resultPromise = Promise.all([dataPromise, peoplePromise]);
return resultPromise; })
}) .then(dataResult => { });
const [movieData, peopleData] = dataResult // we can actually let TypeScripts type inference pick out the types
peopleData.cast.sort((f, s) => f.order - s.order); const mainActors = peopleData.cast.slice(0, 6);
const characters :Character[] = mainActors.map(actor => ({ name: actor.character, actor: actor.name, image: actor.profile_path }))
const directors = peopleData.crew .filter(person => person.department === "Directing" && person.job === "Director") .map(person => person.name) const directedBy = directors.join(" & ");
const writers = peopleData.crew .filter(person => person.department === "Writing" && person.job === "Writer") .map(person => person.name); const writtenBy = writers.join(" & ");
const movie: Movie = { id: movieData.id, title: movieData.title, tagline: movieData.tagline, releaseDate: new Date(movieData.release_date), posterUrl: movieData.poster_path, backdropUrl: movieData.backdrop_path, overview: movieData.overview, runtime: movieData.runtime, characters: characters, directedBy: directedBy, writenBy: writtenBy }
return movie; });
Note that we did not do any UI interactions in our code. We just received a string, did some promise calls, and returned a value. The UI work can now be done in UI-oriented code. In this case, that's in the click event handler of the search button. We should simply add a then handler to the search call that will call the showResults method, and a catch handler that will call the clearResults method:
search(movieTitle) .then(movie => showResults(movie)) .catch(_ => clearResults(value));
The output should be the same as the previous activity.
Solution:
const getJsonData = (url: string):Promise<any> => {}
const getJsonData = (url: string):Promise<any> => { const response = await fetch(url); return response.json();}
const url = getSearchUrl(value);
const data: SearchResultApi = await getJsonData(url);
if (data.results.length === 0) { throw Error("Not found"); } const movieResult = data.results[0];
const movieUrl = getMovieUrl(movieResult.id); const peopleUrl = getPeopleUrl(movieResult.id);
const dataPromise: Promise<MovieResultApi> = getJsonData(movieUrl); const peoplePromise: Promise<PeopleResultApi> = getJsonData(peopleUrl);
const dataArray = await Promise.all([dataPromise, peoplePromise]);
const [movieData, peopleData] = dataArray;
peopleData.cast.sort((f, s) => f.order - s.order); const mainActors = peopleData.cast.slice(0, 6);
const characters :Character[] = mainActors.map(actor => ({ name: actor.character, actor: actor.name, image: actor.profile_path }))
const directors = peopleData.crew .filter(person => person.department === "Directing" && person.job === "Director") .map(person => person.name) const directedBy = directors.join(" & ");
const writers = peopleData.crew .filter(person => person.department === "Writing" && person.job === "Writer") .map(person => person.name); const writtenBy = writers.join(" & ");
const movie: Movie = { id: movieData.id, title: movieData.title, tagline: movieData.tagline, releaseDate: new Date(movieData.release_date), posterUrl: movieData.poster_path, backdropUrl: movieData.backdrop_path, overview: movieData.overview, runtime: movieData.runtime, characters: characters, directedBy: directedBy, writenBy: writtenBy }
return movie;
try { const movie = await search(movieTitle); showResults(movie); } catch { clearResults(movieTitle); }
The output should be the same as the previous activity.
Solution:
In this activity, we'll build a higher-order pipe function that accepts functions as arguments, and composes them from left to right, returning a function that accepts the arguments of the first function, and returns the type of the last function. When the returned function is run, it iterates over the given functions, feeding the return value of each function to the next one:
type UnaryFunction<T, R> = T extends void ? () => R : (arg: T) => R;
As mentioned, we'll only support functions accepting up to one argument, for simplicity.
Note that in order to deal with the special case of 0 arguments, we need to check whether T extends void and returns a parameterless function.
function pipe<R>(fn: UnaryFunction<void, R>): UnaryFunction<void, R>;
function pipe<T, R = T>(fn: UnaryFunction<T, R>): UnaryFunction<T, R> {
return fn;
}
Note that we require two overloads for the function, one for the special case of no parameters, and another for a single-parameter function.
function pipe<R>(fn: UnaryFunction<void, R>): UnaryFunction<void, R>;
function pipe<T, R = T>(fn: UnaryFunction<T, R>): UnaryFunction<T, R>;
function pipe<T, A, R>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, R>): UnaryFunction<T, R>;
function pipe<T, A, R>(fn1: UnaryFunction<T, A>, fn2?: UnaryFunction<A, R>) {
// TODO: Support two functions
}
The previous implementation no longer works, since we need to support both a single function, as well as multiple functions, so we can no longer just return fn. We'll add a naïve implementation for now and expand it to a more generic solution in the next steps.
function pipe<R>(fn: UnaryFunction<void, R>): UnaryFunction<void, R>;
function pipe<T, R = T>(fn: UnaryFunction<T, R>): UnaryFunction<T, R>;
function pipe<T, A, R>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, R>): UnaryFunction<T, R>;
function pipe<T, A, R>(fn1: UnaryFunction<T, A>, fn2?: UnaryFunction<A, R>) {
if (fn2 === undefined) {
return fn1;
}
return (arg: T) => {
return fn2(fn1(arg));
};
}
function pipe<R>(fn: UnaryFunction<void, R>): UnaryFunction<void, R>;
function pipe<T, R = T>(fn: UnaryFunction<T, R>): UnaryFunction<T, R>;
function pipe<T, A, R>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, R>): UnaryFunction<T, R>;
function pipe<T>(...fns: UnaryFunction<any, any>[]): UnaryFunction<any, any> {
return (arg: T) => {
return fns.reduce((prev, fn) => fn(prev), arg);
};
}
In the case of three functions:
function pipe<T, A, B, R>(
fn1: UnaryFunction<T, A>,
fn2: UnaryFunction<A, B>,
fn3: UnaryFunction<B, R>,
): UnaryFunction<T, R>;
In the case of four functions:
function pipe<T, A, B, C, R>(
fn1: UnaryFunction<T, A>,
fn2: UnaryFunction<A, B>,
fn3: UnaryFunction<B, C>,
fn4: UnaryFunction<C, R>,
): UnaryFunction<T, R>;
In the case of five functions:
function pipe<T, A, B, C, D, R>(
fn1: UnaryFunction<T, A>,
fn2: UnaryFunction<A, B>,
fn3: UnaryFunction<B, C>,
fn4: UnaryFunction<C, D>,
fn5: UnaryFunction<D, R>,
): UnaryFunction<T, R>;
In each overload, we have the first generic as T – this is the type of argument that the returned function will have, and R – the return type of the returned function. Between them we have A, B, C, and so on, as the interim return type/argument type of the second…second to last functions. For all the preceding steps, make sure to export the functions by adding export before the function keyword.
Finally, we can use our pipe function to compose any functions we want, while staying completely type-safe:
const composedFn = pipe(
(x: string) => x.toUpperCase(),
x => [x, x].join(','),
x => x.length,
x => x.toString(),
x => Number(x),
);
console.log('result is:', composedFn('hello'))
Running the this code should result in the following output:
result is: 11
Solution:
npm i
The only dependencies we're using here are http-server to power our web application and typescript to transpile our code. Now that our project is set up, let's quickly create an index.html file:
<html>
<head>
<title>The TypeScript Workshop - Activity 12.1</title>
<link href="styles.css" rel="stylesheet"></link>
</head>
<body>
<input type="text" placeholder="What promise will you make?" id="promise-input"> <button id="promise-save">save</button>
<div>
<table id="promise-table"></ul>
</div>
</body>
<script type="module" src="app.js"></script>
</html>
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
}
input {
width: 200;
}
table {
border: 1px solid;
}
td {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
Now we will create an app.ts file and create a very rough client library that implements a fetch abstraction similar to what we created in Chapter 3, Functions. Because TypeScript doesn't run natively in a web browser, we will need to use tsc to transpile our TypeScript code into JavaScript. There are some advanced tools such as webpack and Parcel that can help with this, but those are out of scope for this chapter so we will keep this simple and just use a single app.ts file.
interface PromiseModel {
id?: number;
desc: string;
}
const fetchClient = (url: string) => (resource: string) => (method: string) => (
body?: PromiseModel
) => {
return fetch(`${url}/${resource}`, {
body: body && JSON.stringify(body),
headers: { "Content-Type": "application/json" },
method,
});
};
const api = fetchClient("http://localhost:3000");
const resource = api("promise");
const getAction = resource("get");
const postAction = resource("post");
const deleteItem = (id: number) => {
const resource = api(`promise/${id}`);
resource("delete")().then(loadItems);
};
const loadItems = () => {
getAction().then((res) => res.json().then(renderList));
};
const saveItem = () => {
const input = document.getElementById("promise-input") as HTMLInputElement;
if (input.value) {
postAction({ desc: input.value }).then(loadItems);
input.value = "";
}
};
const renderList = (data: PromiseModel[]) => {
const table = document.getElementById("promise-table");
if (table) {
table.innerHTML = "";
let tr = document.createElement("tr");
["Promise", "Delete"].forEach((label) => {
const th = document.createElement("th");
th.innerText = label;
tr.appendChild(th);
});
table.appendChild(tr);
data.forEach((el) => {
table.appendChild(renderRow(el));
});
}
};
const renderRow = (el: PromiseModel) => {
const tr = document.createElement("tr");
const td1 = document.createElement("td");
td1.innerHTML = el.desc;
tr.appendChild(td1);
const td2 = document.createElement("td");
const deleteButton = document.createElement("button");
deleteButton.innerText = "delete";
deleteButton.onclick = () => deleteItem(el.id!);
td2.appendChild(deleteButton);
tr.appendChild(td2);
return tr;
};
document.getElementById("promise-save")?.addEventListener("click", saveItem);
loadItems();
interface PromiseModel {
id?: number;
desc: string;
}
const fetchClient = (url: string) => (resource: string) => (method: string) => (
body?: PromiseModel
) => {
return fetch(`${url}/${resource}`, {
body: body && JSON.stringify(body),
headers: { "Content-Type": "application/json" },
method,
});
};
const api = fetchClient("http://localhost:3000");
const resource = api("promise");
const getAction = resource("get");
const postAction = resource("post");
const deleteItem = (id: number) => {
const resource = api(`promise/${id}`);
resource("delete")().then(loadItems);
};
const loadItems = () => {
getAction().then((res) => res.json().then(renderList));
};
const saveItem = () => {
const input = document.getElementById("promise-input") as HTMLInputElement;
if (input.value) {
postAction({ desc: input.value }).then(loadItems);
input.value = "";
}
};
const renderList = (data: PromiseModel[]) => {
const table = document.getElementById("promise-table");
if (table) {
table.innerHTML = "";
let tr = document.createElement("tr");
["Promise", "Delete"].forEach((label) => {
const th = document.createElement("th");
th.innerText = label;
tr.appendChild(th);
});
table.appendChild(tr);
data.forEach((el) => {
table.appendChild(renderRow(el));
});
}
};
const renderRow = (el: PromiseModel) => {
const tr = document.createElement("tr");
const td1 = document.createElement("td");
td1.innerHTML = el.desc;
tr.appendChild(td1);
const td2 = document.createElement("td");
const deleteButton = document.createElement("button");
deleteButton.innerText = "delete";
deleteButton.onclick = () => deleteItem(el.id!);
td2.appendChild(deleteButton);
tr.appendChild(td2);
return tr;
};
document.getElementById("promise-save")?.addEventListener("click", saveItem);
loadItems();
It's not hard to see why view frameworks are popular; however, this should do the trick for putting together a full-stack application.
npx tsc -w.
This will transpile the TypeScript code in watch mode so that it restarts when changes are made.
Now navigate a web browser to http://localhost:8080/. You should see a form like the one that follows:
Note
If you don't see "Promise Delete" then it could be that your API from Exercise 6, Implementing a RESTful API backed by sqlite isn't running. Return to that exercise and follow the steps there.
You can add and delete promises. Here are some examples:
You should see the following promise saved:
Figure 12.14 and Figure 12.15 show some more examples:
Try to add to the application and make use of the API to get a single promise or update promises.
Solution:
Let's go over what needed to change in order to make this work:
const renderAll = async () => {
The refactored program is six lines shorter and eliminates nesting:
export class El {
constructor(private name: string) {}
render = () => {
return new Promise((resolve) =>
setTimeout(
() => resolve(`${this.name} is resolved`),
Math.random() * 1000
)
);
};
}
const e1 = new El('header');
const e2 = new El('body');
const e3 = new El('footer');
const renderAll = async () => {
console.log(await e1.render());
console.log(await e2.render());
console.log(await e3.render());
};
renderAll();
header is resolved
body is resolved
footer is resolved
Solution:
import firebase from 'firebase';const config = { apiKey: 'abc123', authDomain: 'blog-xxx.firebaseapp.com', projectId: 'https://blog-xxx.firebaseio.com', storageBucket: 'blog-xxx.appspot.com', messagingSenderId: '999', appId: '1:123:web:123abc',};firebase.initializeApp(config);export const auth = firebase.auth();export const db = firebase.firestore();
import firebase from 'firebase';
import React, { createContext, ReactNode, useEffect, useState } from 'react';
import { auth } from '../services/firebase';
interface ContextProps {
children: ReactNode;
}
export const UserContext = createContext<Partial<firebase.User | undefined>>(
{}
);
export const UserProvider = (props: ContextProps) => {
const [user, setUser] = useState<firebase.User>();
useEffect(() => {
auth.onAuthStateChanged((userAuth) => {
setUser(userAuth ?? undefined);
});
});
return (
<UserContext.Provider value={user}>{props.children}</UserContext.Provider>
);
};
export interface CommentModel { comment: string; timestamp: number; user: string;}export interface StoryModel { comments: CommentModel[]; id: string; link: string; title: string; user: string;}
With those interfaces created, we need to implement some methods in our provider, namely methods for adding comments and stories as well as a method that will fetch all the stories. To do that, we'll need to access a collection in our database. This can be done with a single line of code:
const storiesDB = db.collection('stories');
This code will create the collection if it doesn't exist. The storiesDB object we created has methods for fetching, adding, and updating documents from the collection. With those methods implemented, we add our stories data and the methods that handle the data to our provider value. This means that components that use StoriesContext will be able to call those methods or access that data.
Again, the solution to this somewhat complicated provider is available on GitHub.
We'll track the state of both the email and password fields using useState and an onChange event.
When the button is clicked, we should call a method on the auth object we exported from our Firebase service earlier to create a new user using the given email address and password.
18.118.164.151