About 13 minutes length
If your code is filled with if/else
or try/catch
statements maybe it could be time to clean it up.
One of the most effective ways to get a cleaner code is to use the functional programming paradigm.
The benefits of using functions for conditional flows are many and the greatest one is the possibility to reuse conditions all over your codebase making it D.R.Y. (aka the dont repeat yourself).
There are dozens of libraries written both in JavaScript or TypeScript for functional programming and this time we will give a look a tiinvo.
You can install tiinvo with both npm
or yarn
.
npm i tiinvo# or if you use yarnyarn add tiinvo
Try catch are used to prevent your code crashing when an uncaught Error
is thrown.
A normal try/catch expression usually looks like this:
function isString(input: unknown): input is string {return typeof input === 'string';}function thisWillThrow(input: unknown): string | never {if (!isString(input)) {throw new TypeError('input must be a string');}return input;}export async function handler(context) {try {const param = thisWillThrow(context.req.body.foo);return {status: 200,body: {param,},};} catch (error) {return {status: 500,body {error,},};}}
Too many curly braces indeed.
Using tiinvo, you can first avoid the try/catch construct, invoking your function safely, then you can decide what to do with your Result
.
Let's rewrite it
import { TryCatch } from 'tiinvo';function createResponse(status, body) {return {body,status,}}function isString(input: unknown): input is string {return typeof input === 'string';}function thisWillThrow(input: unknown): string | never {if (!isString(input)) {throw new TypeError('input must be a string');}return input;}export async function handler(context) {return TryCatch(thisWillThrow, context.req.body.foo).mapOrElse(error => createResponse(500, { error }),param => createResponse(200, { param }),);}
Cleaner. Note that TryCatch
can call your function with the needed signature arguments.
You can also handle async
functions with TryCatchAsync
.
import { TryCatchAsync } from 'tiinvo';async function catchy() {throw new Error('💥💥💥');}export async function maybeExplodes() {return (await TryCatchAsync(catchy)).mapOrElse(() => 'exploded',() => 'wow is safe',);}
Either
The if/else statements are used to control the execution flow of your code. There are different constructs for controlling code flow, but the most used is the Either
construct, which represents a value that could be considered falsy (left) or truthy (right).
To make an example, give a look at the code below.
// file a.ts// could possibly throw, forcing us to use a try/catchexport function doStuff(num: number): number | never {// if statementif (num % 2 !== 0) {throw new Error('number is not even');}return num;}// file b.tsimport { doStuff } from './a.ts';doStuff(2);doStuff(4);// throws, stops execution. To avoid it, we should use a try/catch, but not todaydoStuff(5);doStuff(6);
Rewriting the example above using tiinvo will look like this
// file a.tsimport { Either, Err, Left, Ok, Right } from 'tiinvo';// yep I know, a really silly examplefunction isEven(num: number): Either<number, number> {return num % 2 === 0 ? Right(num) : Left(num);}export function doStuff(num: number): Result<number, Error> {return isEven(num).fold(() => Err('number is not even'),num => Ok(num),)}// file b.tsimport { doStuff } from './b.ts';doStuff(2); // returns Ok(2)doStuff(4); // returns Ok(4)doStuff(5); // returns Err() so we can handle without a try catch and without blocking executiondoStuff(6); // returns Ok(6)
Error handling is one of the worst parts of programming. You have to catch them all® using the try/catch
syntax, which sometimes is a lot messy.
To avoid this, you can use the Result
data type, which handles both an Err
if something did go wrong or an Ok<T>
if something went correctly.
// a.tsexport function isEven(num: number): number | null {return num % 2 === 0 ? num : null;}// b.tsimport { isEven } from './a.ts';export async function getCount() {try {const result = await fetch('/my/url');const json = await result.json();if (isEven(json.count)) {return { count: json.count };} else {return { count: 0 };}} catch (error) {return { count: 0 };}}
You can rewrite like this
// a.tsimport { Some } from 'tiinvo';export function isEven(num: number): Some<number> {return Some(num % 2 === 0 ? num : null)}// b.tsimport { TryCatchAsync } from 'tiinvo';import { isEven } from './a.ts';function count(num: number) {return { count: num };}export async function getCount() {const result = await TryCatchAsync(fetch, '/my/url');const json = await TryCatchAsync(result.json);return json.mapOrElse(() => count(0),num => isEven(num).mapOr(count(0), val => count(val)));}
Option
for optional valuesOption
is similar to the Maybe
monad, but is used to express a value that is optional, therefore it's name. The implementation of Option
is taken from rust language std::option. Unlike Maybe
, which treats every falsy value like Nothing
, Option
flatterns undefined, null, NaN or invalid Dates as None
.
import { Option } from 'tiinvo';export interface IUser {name?: string;surname?: string;email?: string;}export type IMappedUser = Record<IUser, Option<string>>;// maps IUser to IMappedUserexport function map(user: IUser): IMappedUser {return Object.keys(user).reduce((collector, key) => ({...collector,[key]: Option(user[key])}),{});}export function isValid(mappedUser: IMappedUser): boolean {return mappedUser.email.and(mappedUser.name).and(mappedUser.surname).isSome();}isValid(map({ email: 'john.doe@gmail.com' })); // falseisValid(map({ email: 'john.doe@gmail.com', name: 'john' })); // falseisValid(map({ email: 'john.doe@gmail.com', name: 'john', surname: 'doe' })); // true
Do you search for a cleaner code in TypeScript? You can start by using TypeGuards!
Read this storyWhat if we are making a great mistake? Finding value where there is some.
Read this storyAll my links: