Using TaskEither with fp-ts in your TypeScript code (Part 4)

Michael
8 min readJul 1, 2023

--

(fp-ts series 1: Container)

  1. Option
  2. Either
  3. TaskEither (part 1) — Basic introduction
  4. TaskEither (part 2) — Dependent Promises
  5. TaskEither (part 3) — Independent Promises
  6. 👇🏻 TaskEither (part 4) — More examples

Quick Review on TaskEither

In the previous article TaskEither (part 1), you can gain more understanding on the basic of how to use TaskEither to wrap a Promise value , and examples are also provided to illustrate the concepts of TaskEither.

In TaskEither (part 2), we focus on solving Dependent Promises using Do Notation in TaskEither.

In TaskEither (part 3), we focus on solving Independent Promises using Applicative and Monadic ways in TaskEither.

In this article, more examples are provided.

Example — creating an endpoint on Express.js

Suppose we have an findOrder endpoint on our backend NodeJS server using Express.js. This endpoint aims to support finding orders from an order service. To do so, we first need to fetch a token from an auth service.

Next, we receive an array of Order from the order service, which contains an array of ProductId in each Order object. Then, we map the array of ProductId into an array of Product by asking the product service.

Finally, we return the an array of Order , which contains an array of Product in each Order object.

Let's dive into the codes!

Fetching auth token

Let's create a function to fetch the auth token.

/**
* Function that fetch the authToken
*/
const fetchAuthToken = taskEither.tryCatchK(
// Mock the token
({authUrl}) => Promise.resolve('DEMO_AUTH_TOKEN'),
reason =>
new Error(`Failed to fetch authToken due to Error ${String(reason)}`)
);

For demo purpose, we will just return a dummy token.
(Note: You should also try test it withPromise.reject)

Fetching orders

Next, let's fetch an array of Order from the order service.

type Order = {
readonly id: string;
readonly amount: number;
readonly productIds: readonly {
id: Product['id'];
}[];
};

/**
* Function that fetch the orders
*/
const fetchOrders = taskEither.tryCatchK(
// Mock the orders
({url, authToken, count, date}: FetchOrdersInput) => {
const demoOrders: Order[] = [
{
id: '1',
amount: 100,
productIds: [
{
id: 'PRODUCT_A',
},
],
},
{
id: '2',
amount: 200,
productIds: [
{
id: 'PRODUCT_B',
},
{
id: 'PRODUCT_C',
},
],
},
];

// Mock the results
return Promise.resolve(demoOrders);
},
reason => new Error(`Failed to fetch orders due to Error ${String(reason)}`)
);

The array of Order only contains ProductIds in this stage, we need to further map them into an array of Product

Fetching Products

Now, let’s fetch an array of Product using ProductIds from the product service.

type FetchProductsInput = {
url: string;
authToken: string;
ids: string[];
};

/**
* Function that fetch the products
*/
const fetchProducts = taskEither.tryCatchK(
// Mock the Product
({url, authToken, ids}: FetchProductsInput) => {
const products = ids.map(id => {
switch (id) {
case 'PRODUCT_A': {
const mockProduct: Product = {
code: 'A',
id: 'PRODUCT_A',
unit: 1,
};
return mockProduct;
}
case 'PRODUCT_B': {
const mockProduct: Product = {
code: 'B',
id: 'PRODUCT_B',
unit: 2,
};
return mockProduct;
}
case 'PRODUCT_C': {
const mockProduct: Product = {
code: 'C',
id: 'PRODUCT_C',
unit: 3,
};
return mockProduct;
}
default: {
const mockProduct: Product = {
code: 'DEFAULT',
id: 'PRODUCT_DEFAULT',
unit: 1,
};
return mockProduct;
}
}
});
return Promise.resolve(products);
},
reason => new Error(`Failed to fetch product due to Error ${String(reason)}`)
);

Creating the endpoint

Now, we have all the functions we need to fetch the orders. Let’s combine use them in the endpoint.

Suppose there are two parameters inside the req.bodycount and date . They are used to filter the orders by date and limit the number of orders.

Let's start with checking the count parameter using Either.from Predicate, and taskEither.mapLeft to return a formatted Error. We use taskEither.apS for the count because we will use it when fetching the orders later on.

enum FindOrderResponseErrors {
InvalidCount = 'Invalid Count',
}

enum GeneralErrors {
InternalServerError = 'An unexpected server error was found.',
AuthTokenError = 'Unable to fetch the auth token.',
}

type ErrorResult = {
status: number;
name: FindOrderResponseErrors | GeneralErrors | Error['name'];
description: string;
};

const {count, date} = req.body;

// Suppose the authUrl, orderUrl and productUrl is stored in a env file
const authUrl = `https://YOUR_URL/auth`;
const orderUrl = `https://YOUR_URL/order`;
const productUrl = `https://YOUR_URL/product`;
const ordersResultTE = pipe(
taskEither.Do,
// Check count
taskEither.apS(
'count',
pipe(
taskEither.fromEither(
pipe(
count,
either.fromPredicate(
_count => _count > 0,
__count => new Error(`Order ${__count} is less than 0`)
)
)
),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: FindOrderResponseErrors.InvalidCount,
description: error.message,
status: 400,
};
})
)
),
// Fetch Auth Token ...
// Fetch Order ...
// Fetch Products ...
);

Next, we fetch the auth token. Again, we use taskEither.apS for the authToken because we need that to fetch the orders and products.

    const ordersResultTE = pipe(
taskEither.Do,
taskEither.apS(
'count',
pipe(
taskEither.fromEither(
pipe(
count,
either.fromPredicate(
_count => _count > 0,
__count => new Error(`Order ${__count} is less than 0`)
)
)
),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: FindOrderResponseErrors.InvalidCount,
description: error.message,
status: 400,
};
})
)
),
taskEither.apS(
'authToken',
pipe(
fetchAuthToken({authUrl}),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: GeneralErrors.AuthTokenError,
description: error.message,
status: 400,
};
})
)
),
// Fetch Order...
// Fetch Products ...
);

Now that we have the auth token, we can use it to fetch the orders.

    const ordersResultTE = pipe(
taskEither.Do,
taskEither.apS(
'count',
pipe(
taskEither.fromEither(
pipe(
count,
either.fromPredicate(
_count => _count > 0,
__count => new Error(`Order ${__count} is less than 0`)
)
)
),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: FindOrderResponseErrors.InvalidCount,
description: error.message,
status: 400,
};
})
)
),
taskEither.apS(
'authToken',
pipe(
fetchAuthToken({authUrl}),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: GeneralErrors.AuthTokenError,
description: error.message,
status: 400,
};
})
)
),
taskEither.chain(({count, authToken}) => {
return pipe(
fetchOrders({url: orderUrl, authToken, count, date}),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: GeneralErrors.AuthTokenError,
description: error.message,
status: 400,
};
}),
// Fetch Products for each order ...
);
})
);

After fetching the orders, we want to map the ProductIds into Productsby fetching the products:

const ordersResultTE = pipe(
taskEither.Do,
taskEither.apS(
'count',
pipe(
taskEither.fromEither(
pipe(
count,
either.fromPredicate(
_count => _count > 0,
__count => new Error(`Order ${__count} is less than 0`)
)
)
),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: FindOrderResponseErrors.InvalidCount,
description: error.message,
status: 400,
};
})
)
),
taskEither.apS(
'authToken',
pipe(
fetchAuthToken({authUrl}),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: GeneralErrors.AuthTokenError,
description: error.message,
status: 400,
};
})
)
),
taskEither.chain(({count, authToken}) => {
return pipe(
fetchOrders({url: orderUrl, authToken, count, date}),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: GeneralErrors.AuthTokenError,
description: error.message,
status: 400,
};
}),
taskEither.chain(orders => {
return pipe(
orders,
array.map(order =>
pipe(
fetchProducts({
ids: order.productIds.map(({id}) => id),
url: productUrl,
authToken,
}),
taskEither.map(products => {
const {productIds, ...orderWithoutProductIds} = order;
const ordersResult: OrderResult = {
...orderWithoutProductIds,
products,
};
return ordersResult;
})
)
),
taskEither.sequenceArray,
taskEither.mapLeft<Error, ErrorResult>(error => ({
name: GeneralErrors.InternalServerError,
description: error.message,
status: 500,
}))
);
})
);
})
);

The Complete version of the code (Normal function verison):

import {pipe} from 'fp-ts/function';
import * as taskEither from 'fp-ts/TaskEither';
import * as either from 'fp-ts/Either';
import * as array from 'fp-ts/Array';

type FindOrdersRequest = {
body: {
count: number;
date: Date;
};
};

export enum GeneralErrors {
InternalServerError = 'An unexpected server error was found.',
AuthTokenError = 'Unable to fetch the auth token.',
}

export enum FindOrderResponseErrors {
InvalidCount = 'Invalid Count',
}

type ErrorResult = {
status: number;
name: FindOrderResponseErrors | GeneralErrors | Error['name'];
description: string;
};

type Product = {
readonly id: string;
readonly code: string;
readonly unit: number;
};

type Order = {
readonly id: string;
readonly amount: number;
readonly productIds: readonly {
id: Product['id'];
}[];
};

type OrderResult = {
readonly id: Order['id'];
readonly amount: Order['amount'];
products: Product[];
};

type FetchOrdersInput = {
url: string;
authToken: string;
count: number;
date: Date;
};

/**
* Function that fetch the orders
*/
const fetchOrders = taskEither.tryCatchK(
// Mock the orders
({url, authToken, count, date}: FetchOrdersInput) => {
const demoOrders: Order[] = [
{
id: '1',
amount: 100,
productIds: [
{
id: 'PRODUCT_A',
},
],
},
{
id: '2',
amount: 200,
productIds: [
{
id: 'PRODUCT_B',
},
{
id: 'PRODUCT_C',
},
],
},
];

// Mock the results
return Promise.resolve(demoOrders);
},
reason => new Error(`Failed to fetch orders due to Error ${String(reason)}`)
);

type FetchProductsInput = {
url: string;
authToken: string;
ids: string[];
};

/**
* Function that fetch the products
*/
const fetchProducts = taskEither.tryCatchK(
// Mock the Product
({url, authToken, ids}: FetchProductsInput) => {
const products = ids.map(id => {
switch (id) {
case 'PRODUCT_A': {
const mockProduct: Product = {
code: 'A',
id: 'PRODUCT_A',
unit: 1,
};
return mockProduct;
}
case 'PRODUCT_B': {
const mockProduct: Product = {
code: 'B',
id: 'PRODUCT_B',
unit: 2,
};
return mockProduct;
}
case 'PRODUCT_C': {
const mockProduct: Product = {
code: 'C',
id: 'PRODUCT_C',
unit: 3,
};
return mockProduct;
}
default: {
const mockProduct: Product = {
code: 'DEFAULT',
id: 'PRODUCT_DEFAULT',
unit: 1,
};
return mockProduct;
}
}
});
return Promise.resolve(products);
},
reason => new Error(`Failed to fetch product due to Error ${String(reason)}`)
);

/**
* Function that fetch the authToken
*/
const fetchAuthToken = taskEither.tryCatchK(
// Mock the token
({authUrl}) => Promise.resolve('DEMO_AUTH_TOKEN'),
reason =>
new Error(`Failed to fetch authToken due to Error ${String(reason)}`)
);

const findOrders = async (req: FindOrdersRequest) => {
try {
const {count, date} = req.body;

// Suppose the authUrl, orderUrl and productUrl is stored in a env file
const authUrl = `https://YOUR_URL/auth`;
const orderUrl = `https://YOUR_URL/order`;
const productUrl = `https://YOUR_URL/product`;
const ordersResultTE = pipe(
taskEither.Do,
taskEither.apS(
'count',
pipe(
taskEither.fromEither(
pipe(
count,
either.fromPredicate(
_count => _count > 0,
__count => new Error(`Order ${__count} is less than 0`)
)
)
),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: FindOrderResponseErrors.InvalidCount,
description: error.message,
status: 400,
};
})
)
),
taskEither.apS(
'authToken',
pipe(
fetchAuthToken({authUrl}),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: GeneralErrors.AuthTokenError,
description: error.message,
status: 400,
};
})
)
),
taskEither.chain(({count, authToken}) => {
return pipe(
fetchOrders({url: orderUrl, authToken, count, date}),
taskEither.mapLeft<Error, ErrorResult>(error => {
return {
name: GeneralErrors.AuthTokenError,
description: error.message,
status: 400,
};
}),
taskEither.chain(orders => {
return pipe(
orders,
array.map(order =>
pipe(
fetchProducts({
ids: order.productIds.map(({id}) => id),
url: productUrl,
authToken,
}),
taskEither.map(products => {
const {productIds, ...orderWithoutProductIds} = order;
const ordersResult: OrderResult = {
...orderWithoutProductIds,
products,
};
return ordersResult;
})
)
),
taskEither.sequenceArray,
taskEither.mapLeft<Error, ErrorResult>(error => ({
name: GeneralErrors.InternalServerError,
description: error.message,
status: 500,
}))
);
})
);
})
);

const result = await ordersResultTE();
return result;
} catch (error) {
return {
name: GeneralErrors.InternalServerError,
description: error,
status: 500,
};
}
};

The Express.js endpoint version:

import express from 'express';
const findOrders = async (req: FindOrdersRequest, res: express.Response) => {
try {
const {count, date} = req.body;

// Suppose the authUrl, orderUrl and productUrl is stored in a env file
const authUrl = `${process.env.AUTH_URL}/auth`;
const orderUrl = `${process.env.ORDER_URL}`;
const productUrl = `${process.env.PRODUCT_URL}`;
const ordersResultTE = pipe(
//...
);

const result = await ordersResultTE();
return pipe(
result,
either.match(
({status, ...error}) => {
res.status(status).json({error: error.name});
},
orders => {
res.status(200).send(orders);
}
)
);
} catch (error) {
return res.status(500).json({
name: GeneralErrors.InternalServerError,
description: error,
status: 500,
});
}
};

export default findOrders;

What’s Next:

Congratulations! You have made it to the end of the fp-ts series 1: Container series.

In the next series, we will discuss more on Functor, Applicative Functor, Monad, and the Monoid in depth.

More about fp-ts.

Thanks for reading 🫡
Hope you enjoy this article and gain something from it. I wish it is less daunting now for you to learn Functional Programming :)

Ka Hin

--

--

Michael

Software Engineer🧑🏻‍💻Master in Computer Science🎓. Helping people to learn and use Functional Programming in JS. F# and JS developer🧑🏻‍💻 FP-TS ➡️