TypeScript functional programming utilities

2021-04-02

Currently, I am brushing up on some TypeScript, and I’ve implemented a set of functions used in functional programming for practice. They are perhaps of interest to someone else, someone learning about functional programming or wanting to use it with TypeScript. Each definition includes a test case written with Jest demonstrating the feature. For all definitions I’ve aimed for clarity, not performance.

Implementations

For some tests I use this helper.

const str = <T>(x: T): string => JSON.stringify(x);

add

test('add', () => expect(add(1)(1)).toEqual(2));

const add = (a: number) => (b: number): number => a + b;

append

test('append', () => expect(str(append([1, 2], 3))).toBe(str([1, 2, 3])));

const append = <T>(list: T[], newElement: T): T[] => [
  ...list,
  newElement
];

apply


test('apply', () => expect(apply(Math.max, [1, 2, 3])).toBe(3));

 const apply = <T>(fn: (...xs: T[]) => T, xs: T[]): T => fn(...xs);

assoc

test('assoc', () =>
  expect(str(assoc('b', 2, { a: 1 }))).toEqual(str({ a: 1, b: 2 })));

const assoc = <T>(
  k: string,
  v: T,
  obj: Record<string, unknown>
): Record<string, unknown> => ({ ...obj, [k]: v });

assocPath

test('assocPath', () => {
  const someObject = {
    a: {
      b: {
        c: 3
      },
      d: 2
    },
    e: 1
  };
  
  const modifyPropC = assocPath(['a', 'b', 'c']);
  
  expect(str(modifyPropC(10, someObject))).toEqual(
    str({
      a: {
        b: {
          c: 10
        },
        d: 2
      },
      e: 1
    })
  );
});

const assocPath = <T>(path: string[]) => (
  v: T,
  obj: Record<string, unknown>
): Record<string, unknown> => {
  const setValue = (
    [head, ...tail]: string[],
    o: Record<string, unknown>
  ): Record<string, unknown> =>
    tail.length
      ? {
          ...o,
          [head]: setValue(tail, o[head] as Record<string, unknown>)
        }
      : {
          ...o,
          [head]: v
        };
  return setValue(path, obj);
};

compose

expect(compose(inc, inc, inc)(10)).toEqual(13);
expect(compose(add(20), add(10))(10)).toEqual(40);
expect(compose(map(inc), map(inc))([1, 2, 3])).toEqual([3, 4, 5]);
// warns corretly:
// expect(compose(inc, (x: string) => x + 'a', inc)(10)).toEqual(10);
expect(
  compose(
    (pos: { x: number; y: number }) => ({ ...pos, x: pos.x + 1 }),
    (pos: { x: number; y: number }) => ({ ...pos, x: pos.x + 10 })
  )({
    x: 1,
    y: 1
  })
).toEqual({ x: 12, y: 1 });
  
const compose = <T1, T2, T3, T4, T5, T6>(
  fn1: (a: T1) => T2,
  fn2?: (a: T2) => T3,
  fn3?: (a: T3) => T4,
  fn4?: (a: T4) => T5,
  fn5?: (a: T5) => T6
) => {
  // @ts-ignore
  const fns = Array.from(arguments).filter((f) => f !== undefined);
  return function (data: T1) {
    for (const fn of fns.reverse()) {
      data = fn(data);
    }
    return data;
  };
};

concat

test('concat', () => {
  expect(str(concat([1, 2], [3, 4]))).toEqual(str([1, 2, 3, 4]));
  expect(str(concat([1, 2], [3, 4]))).toEqual(str([1, 2, 3, 4]));
});

const concat = <T>(list1: T[], list2: T[]): T[] => [...list1, ...list2];

dec

test('dec', () => expect(dec(3)).toEqual(2));

const dec = (x: number): number => x - 1;

dissoc

test('dissoc', () =>
  expect(str(dissoc('c', { a: 1, b: 2, c: 3 }))).toEqual(str({ a: 1, b: 2 })));

const dissoc = <T extends Record<string, unknown>>(
  k: keyof T,
  obj: T
): T => {
  delete obj[k];
  return obj;
};

dissocPath

test('dissocPath', () => {
  expect(
    str(
      dissocPath(
        ['a', 'b', 'c'],
        { a: { b: { c: 2, d: 2 }, e: 2 } }
      )
    )
  ).toEqual(str({ a: { b: { d: 2 }, e: 2 } }));
  expect(
    str(
      dissocPath(
        ['a', 'b'], 
        { a: { b: { c: 2, d: 2 }, e: 2 } }
      )
    )
  ).toEqual(str({ a: { e: 2 } }));
});

const dissocPath = (
  path: string[],
  obj: Record<string, unknown>
): Record<string, unknown> => {
  const setValue = (
    [head, ...tail]: string[],
    o: Record<string, unknown>
  ): Record<string, unknown> => {
    if (!tail.length) {
      if (o && head in o) {
        delete o[head];
      }
      return o;
    }
    return setValue(tail, {
      ...o,
      [head]: setValue(tail, o[head] as Record<string, unknown>)
    });
  };

  return setValue(path, obj);
};

div

test('div', () => expect(div(2)(1)).toEqual(2));

const div = (a: number) => (b: number): number => a / b;

drop

test('drop', () =>
  expect(str(drop(2, [1, 2, 3, 4, 5]))).toEqual(str([3, 4, 5])));

const drop = <T>(n: number, list: T[]): T[] => list.slice(n);

dropLast

test('dropLast', () =>
  expect(str(dropLast(2, [1, 2, 3, 4, 5]))).toEqual(str([1, 2, 3])));

const dropLast = <T>(n: number, list: T[]): T[] =>
  list.slice(0, list.length - n);

dropWhile

const lesserThan3 = (x: number): boolean => x < 3;

test('dropWhile', () =>
  expect(
    str(dropWhile(lesserThan3, [1, 2, 3, 4, 5]))
  ).toEqual(str([3, 4, 5])));

const dropWhile = <T>(
  predicate: (x: T) => boolean,
  [head, ...tail]: T[]
): T[] => (predicate(head) ? dropWhile(predicate, tail) : [head, ...tail]);

endsWith

test('endsWith', () => {
  expect(endsWith('world', 'hello, world')).toBeTruthy();
  expect(endsWith('welt', 'hello, world')).toBeFalsy();
});

const endsWith = (str1: string, str2: string): boolean =>
  str1 === str2.slice(str2.length - str1.length);

every

test('every', () => {
  expect(every(isEven, [2, 2, 2])).toBe(true);
 expect(every(isEven, [1, 2, 3])).toBe(false)
});

function every<T>(
  fn: (x: T, i: number, arr: T[]) => boolean,
  list: T[]
): boolean {
  return list.reduce(
    (isSomeElementTrue: boolean, element: T, i: number, arr: T[]): boolean =>
      !isSomeElementTrue ? false : fn(element, i, arr),
    true
  );
}

flatten

test('flatten', () => {
  expect(
    str(
      flatten([
        [1, 2],
        [3, 4]
      ])
    )
  ).toEqual(str([1, 2, 3, 4]));
  expect(str(flatten([[[1, 2]], [[3, 4]]]))).toEqual(
    str([
      [1, 2],
      [3, 4]
    ])
  );
});

const flatten = <T>(li: T[][]): T[] =>
  li.reduce((acc, v) => acc.concat(v), []);

filter

test('filter', () => expect(filter(isEven, [1, 2, 3, 4, 5])).toEqual([2, 4]));

function filter<T>(
  fn: (x: T, i: number, arr: T[]) => boolean,
  list: T[]
): T[] | ((a: T[]) => T[]) {
  const _filter = (li: T[]) =>
    li.reduce(
      (acc: T[], x: T, i: number, arr: T[]) =>
        fn(x, i, arr) ? [...acc, x] : acc,
      []
    );
  return list === undefined ? (l: T[]): T[] => _filter(l) : _filter(list);
}

find

test('find', () => expect(find(isEven, [1, 2, 3, 4])).toEqual(2));

type NoContent = undefined;

function find<T>(
  fn: (x: T, i: number, arr: T[]) => boolean,
  list: T[]
): T | NoContent {
  return list.reduce(
    (firstFound: T | undefined, element: T, i: number, arr: T[]) =>
      !firstFound && fn(element, i, arr) ? element : firstFound,
    undefined
  );
}

findIndex

test('findIndex', () => expect(findIndex(isEven, [1, 2, 3, 4])).toEqual(1));

type NotFound = -1;

function findIndex<T>(
  fn: (x: T, i: number, arr: T[]) => boolean,
  list: T[]
): number | NotFound {
  return list.reduce(
    (firstFound: number, element: T, i: number, arr: T[]) =>
      firstFound === -1 && fn(element, i, arr) ? i : firstFound,
    -1
  );
}

flatten

test('flatten', () => {
  expect(
    str(
      flatten([
        [1, 2],
        [3, 4]
      ])
    )
  ).toEqual(str([1, 2, 3, 4]));
  expect(str(flatten([[[1, 2]], [[3, 4]]]))).toEqual(
    str([
      [1, 2],
      [3, 4]
    ])
  );
});

const flatten = <T>(li: T[][]): T[] =>
  li.reduce((acc, v) => acc.concat(v), []);

foldl

test('foldl', () => {
  expect(
    foldl((a: number, b: number): number => a + b, 0, [1, 2, 3, 4, 5])
  ).toEqual(15);
  expect(
    foldl(
      (acc: number[], v: number): number[] => [...acc, inc(v)],
      [],
      [1, 2, 3, 4, 5]
    )
  ).toEqual([2, 3, 4, 5, 6]);
  expect(
    foldl(
      (acc: { answer: number }, v: number): { answer: number } => ({
        answer: acc.answer + v
      }),
      { answer: 0 },
      [1, 2, 3]
    )
  ).toEqual({ answer: 6 });
});

type Reducer<U, T> = (acc: U, next: T) => U;

const foldl = <T, U>(
  reducer: Reducer<U, T>,
  result: U,
  list: T[]
): U => {
  while (list.length) {
    result = reducer(result, head(list));
    list.shift();
  }
  return result;
};

id

test('id', () => expect(id(3)).toEqual(3));

const id = <T>(x: T): T => x;

inc

test('inc', () => expect(inc(3)).toEqual(4));

const inc = (x: number): number => x + 1;

includes

test('includes', () => {
  expect(includes(3, [1, 2, 3, 4])).toBeTruthy();
  expect(includes(5, [1, 2, 3, 4])).toBeFalsy();
});

function includes<T>(el: T, list: T[]): boolean {
  return list.indexOf(el) >= 0 ? true : false;
}

isEmpty

test('isEmpty', () => {
  expect(isEmpty([1, 2])).toBeFalsy();
  expect(isEmpty([])).toBeTruthy();
});

const isEmpty = <T>(list: T[]): boolean => !list.length;

join

test('join', () =>
  expect(join(['hello,', 'world'], ' ')).toEqual('hello, world'));

const join = <T>(list: T[], delimiter = ''): string => {
  return list.reduce(
    (acc, v, i, arr) => `${acc}${v}${i !== arr.length - 1 ? delimiter : ''}`,
    ''
  );
};

len

test('len', () => expect(len([1, 2, 3])).toEqual(3));

const len = <T>(list: T[]): number =>
  list.reduce((listLength) => listLength + 1, 0);

map

test('map', () => {
  expect(str(map(add(10))([1, 2, 3]))).toEqual(str([11, 12, 13]));
  expect(str(map(add1)([1, 2, 3]))).toEqual(str([2, 3, 4]));
});

const map = <T, U>(fn: (x: T, i?: number, arr?: T[]) => U) => (
  list: T[]
): U[] => {
  const _map = <T, U>(fn: (x: T, i?: number, arr?: T[]) => U, _l: T[]): U[] =>
    _l.reduce(
      (acc: U[], x: T, i: number, arr: T[]): U[] => [...acc, fn(x, i, arr)],
      []
    );
  return _map(fn, list);
};

mult

test('mult', () => expect(mult(1)(2)).toEqual(2));

const mult = (a: number) => (b: number): number => a * b;

not

test('not', () => {
  expect(not(0)).toBeTruthy();
  expect(not(1)).toBeFalsy();
  expect(not(false)).toBeTruthy();
  expect(not(true)).toBeFalsy();
});

const not = (el: boolean | 0 | 1): boolean => !el;

once

test('once', () => {
  const add1ToValue = once(add1);
  let newValue;
  newValue = add1ToValue(0);
  newValue = add1ToValue(newValue);
  newValue = add1ToValue(newValue);
  expect(newValue).toEqual(1);
  const add1ToList = (l: number[]) => l.map(add1);
  const add1ToListOnce = once(add1ToList);
  let newArray;
  newArray = add1ToListOnce([1, 2, 3]);
  newArray = add1ToListOnce(newArray);
  newArray = add1ToListOnce(newArray);
  expect(str(newArray)).toEqual(str([2, 3, 4]));
});

const once = <T, U>(fn: (...as: T[]) => U): ((...vs: T[]) => U) => {
  let _v: U;
  return (...args: T[]) => {
    _v ||= fn(...args);
    return _v;
  };
};

pluck

test('pluck', () => expect(pluck({ a: 1, b: 2 }, 'a')).toBe(1));

const pluck = <T, U extends keyof T>(obj: T, k: U): T[U] => obj[k];

range

test('range', () => expect(str(range(2, 8))).toEqual(str([2, 3, 4, 5, 6, 7])));

const range = (a: number, b: number): number[] =>
  Array.from({ length: b - a }, (_, i) => i + a);

repeat

test('repeat', () => expect(
    str(repeat('*', 3))
  ).toEqual(str(['*', '*', '*']))
);

const repeat = <T>(el: T, n: number): T[] =>
  Array.from({ length: n }, () => el);

reverse

test('reverse', () => expect(str(reverse([1, 2, 3]))).toEqual(str([3, 2, 1])));

const reverse = <T>(list: T[]): T[] =>
  list.reduce((acc: T[], v: T) => [v, ...acc], []);

some

test('some', () => {
  expect(some(isEven, [1, 1, 1])).toBe(false);
  expect(some(isEven, [1, 2, 3])).toBe(true);
});

function some<T>(
  fn: (x: T, i: number, arr: T[]) => boolean,
  list: T[]
): boolean {
  return list.reduce(
    (isSomeElementTrue: boolean, element: T, i: number, arr: T[]): boolean =>
      !isSomeElementTrue ? fn(element, i, arr) : true,
    false
  );
}

startsWith

test('startsWith', () => {
  expect(startsWith('hello', 'hello, world')).toBeTruthy();
  expect(startsWith('bonjour', 'hello, world')).toBeFalsy();
});

const startsWith = (str1: string, str2: string): boolean =>
  str1 === str2.slice(0, str1.length);

subtr

test('subtr', () => expect(subtr(1)(1)).toEqual(0));

const subtr = (a: number) => (b: number): number => a - b;

sum

test('sum', () => expect(sum(1, 1, 1, 1, 1)).toEqual(5));

const sum = (...ns: number[]): number =>
  ns.reduce((acc, v) => acc + v, 0);

tail

test('tail', () => expect(tail(['a', 'b', 'c'])).toEqual(['b', 'c']));

const tail = <T>(li: T[]): T[] | undefined =>
  li.length > 0 ? li.slice(1) : [];

take

test('take', () => expect(str(take(2, [1, 2, 3, 4, 5]))).toEqual(str([1, 2])));

const take = <T>(n: number, list: T[]): T[] => list.slice(0, n);

tee

test('tee', () => {
  let isChanged1 = false;
  const mockFn = <T>(_: T) => (isChanged1 = true);
  expect(tee(mockFn)(2)).toEqual(2);
  expect(isChanged1).toBeTruthy();
  let isChanged2 = false;
  const mockFn2 = <T>(_: T) => (isChanged2 = true);
  expect(tee(mockFn2)([1, 2, 3])).toEqual([1, 2, 3]);
  expect(isChanged2).toBeTruthy();
});

const tee = <T>(fn: (x: T) => void) => (v: T): T => {
  fn(v);
  return v;
};

trim

test('trim', () => expect(trim('  hello, world!  ')).toEqual('hello, world!'));

const trim = (str: string): string => str.trim();

uniq

test('uniq', () =>
  expect(str(uniq([1, 1, 1, 1, 2, 3, 1, 1]))).toEqual(str([1, 2, 3])));

const uniq = <T>(list: T[]): T[] =>
  list.filter((v, i, a) => a.indexOf(v) === i);

zip

test('zip', () =>
  expect(str(zip(['a', 'b'], [1, 2]))).toEqual(
    str([
      ['a', 1],
      ['b', 2]
    ])
  ));

const zip = <T, U>(list1: T[], list2: U[]): [T, U][] =>
  list1.reduce(
    (acc: [T, U][], v: T, i: number): [T, U][] => [...acc, [v, list2[i]]],
    []
  );

About | Archive