Functions

Quiz

const numberArray = 
	['-10', '0', '10', '20', '30', 'NaN', 'NaN100'];

const parsedInts = 
	numberArray.map(parseInt);

Table of Contents

  • Use functions as callbacks

  • Function overloading

  • Function Signature

  • Higher-Order Function

1. Use functions as callbacks

e.g.1.1 Array.map

import { toReadableNumber } from 'some-library';
const readableNumbers = someNumbers.map(toReadableNumber);

// toReadableNumber(10000000)
// → '10,000,000'

e.g.1.1 Array.map

// We think of:
const readableNumbers = someNumbers.map(toReadableNumber);

// …as being like:
const readableNumbers = someNumbers.map((n) => toReadableNumber(n));

// …but it's more like:
const readableNumbers = someNumbers.map((item, index, arr) =>
  toReadableNumber(item, index, arr),
);

e.g.1.1 Array.map

// Not works
export function toReadableNumber(num, base = 10) {
	// ...
}

e.g.1.2 Web API

// A promise for the next frame:
const nextFrame = 
	() => new Promise(requestAnimationFrame);

e.g.1.2 Web API

// A promise for the next frame:
const nextFrame = 
	() => new Promise(requestAnimationFrame);
    
// It is actually ...
const nextFrame = () =>
  new Promise(
  	(resolve, reject) => requestAnimationFrame(resolve, reject)
  );

e.g.1.3 parseInt

const numberArray = 
	['-10', '0', '10', '20', '30', 'NaN', 'NaN100'];

const parsedInts = 
	numberArray.map(parseInt);

// Result
// → [-10, NaN, 2, 6, 12, NaN, NaN]

e.g.1.4 Option object

const controller = new AbortController();
const { signal } = controller;

el.addEventListener('mousemove', callback, { signal });
el.addEventListener('pointermove', callback, { signal });
el.addEventListener('touchmove', callback, { signal });

// Later, remove all three listeners:
controller.abort();

e.g.1.4 Option object

// Oops
const controller = new AbortController();
el.addEventListener(name, callback, controller);

// Ok
const controller = new AbortController();
const options = { signal: controller.signal };
el.addEventListener(name, callback, options);

e.g.1.5 Work with TypeScript!

function oneArg(arg1: string) {
  console.log(arg1);
}

oneArg('hello', 'world');
//              ^^^^^^^
// Expected 1 arguments, but got 2.

e.g.1.6 Work with TypeScript?

function twoArgCallback(cb: (arg1: string, arg2: string) => void) {
  cb('hello', 'world');
}

twoArgCallback(oneArg);

e.g.1.7 Work with TypeScript?

// Oops
function toReadableNumber(num: number, whatever: string): string {
  // Return num as string in a human readable form.
  // Eg 10000000 might become '10,000,000'
  return '';
}

const readableNumbers = [1, 2, 3].map(toReadableNumber);

// Ok
function toReadableNumber(num: number, whatever: number): string {
  // Return num as string in a human readable form.
  // Eg 10000000 might become '10,000,000'
  return '';
}

const readableNumbers = [1, 2, 3].map(toReadableNumber);

e.g.1.8 Is TypeScript wrong?

interface Options {
  reverse?: boolean;
}

function whatever({ reverse = false }: Options = {}) {
  console.log(reverse);
}

whatever({ reverse: true });

What about 'toString', 'constructor', 'valueOf', 'hasOwnProperty' ...

e.g.1.9 Is TypeScript wrong?

const numbers = [1, 2, 3];
const doubledNumbers = numbers.map(
  (n) => n * 2
);

What about other Web APIs ...

2. Function overloading

Free style API

$.style(element, "top", rect.top);

Free style API

$.style(element, "top", rect.top);

// 1 
$.style($$(".popup"), "top", rect.top);

// 2
$.style(element, {
	top: rect.top,
	right: rect.right,
	bottom: rect.bottom,
	left: rect.left
);
  
// 3
$.style($$(".popup"), {
	top: rect.top,
	right: rect.right,
	bottom: rect.bottom,
	left: rect.left
});

e.g.2.1 Overloading

$.style([element], {top: rect.top});

e.g.2.2 Overloading

let values = {
	top: rect.top,
	right: rect.right,
	bottom: rect.bottom,
	left: rect.left
};

for (let element of $$(".popup")) {
	for (let property in values) {
		$.style(element, property, values[property]);
	}
}

e.g.2.3 Overloading

for (let element of $$(".popup")) {
	Object.assign(element.style, {
		top: rect.top,
		right: rect.right,
		bottom: rect.bottom,
		left: rect.left
	});
}

e.g.2.4 Overloading

function style(subject, ...args) {
	if (Array.isArray(subject)) {
		subject.forEach(e => style(e, ...args));
	}
	else if ($.type(args[0]) === "object" && args.length = 1) {
		for (let p in args[0]) {
			style(subject, p, args[0][p]);
		}
	}
	else {
		subject.style[args[0]] = args[1];
	}

	return subject;
}

In JS, overloading is typically implemented by inspecting the types and number of a function’s arguments in the function, and branching accordingly.

e.g.2.5 overload() option 1

export default function style(subject, ...args) {
	return overload(
      subject, 
      args, 
      (element, property, value) => {
		element.style[property] = value;
      }
    )
}

e.g.2.6 overload() option 2

export default overload(
  function style(element, property, value) {
	element.style[property] = value;
  }
);

e.g.2.7 overload() option 3

function style(element, property, value) {
	element.style[property] = value;
}

export default overload(style);

3. Function Signature

Get color function

enum AlertColor {
  Red = "red",
  Yellow = "yellow",
  Green = "green",
}
  
const getColorForStockAmount = (stock = 0): AlertColor => {
  if (stock <= 100) {
    return AlertColor.Red;
  }
  if (stock <= 500) {
    return AlertColor.Yellow;
  }
  return AlertColor.Green;
};

getColorForStockAmount(50); // returns red
getColorForStockAmount(250); // returns yellow
getColorForStockAmount(750); // returns green

e.g.3.1 Add more code

 if (stock <= 100) {
    return AlertColor.Red;
  }
+ if (stock <= 300) {           // Add line
+   return AlertColor.Orange;   // Add line
+ }                             // Add line
  if (stock <= 500) {
    return AlertColor.Yellow;
  }

e.g.3.1 Add more code (if)

enum AlertColor {
  Red = "red",
  Yellow = "yellow",
  Green = "green",
}
type Amount = {
  amount: number;
  color: AlertColor;
};
const getColorForStockAmount = (stock = 0): AlertColor => {
  const items: Amount[] = [
    { amount: 100, color: AlertColor.Red },
    { amount: 500, color: AlertColor.Yellow },
    { amount: Number.MAX_SAFE_INTEGER, color: AlertColor.Green },
  ];

  // .find() will return the first element that returns true for stock <= amount
  const item = items.find((item) => stock <= item.amount);

  return item?.color;
};

e.g.3.2 Add more code

Change the function parameter from a number to an object with options

/** @deprecated
 * Use { amount: number; updatedAt?: string; } object instead
 * */
const getColorForStockAmount = (stock = 0): AlertColor => {
  // ...
}

const getColorFromOptionsForStockAmount = (options: Options) => {
  // ...
}
const getColorForStockAmount: StockOptions = (options: number | Options) => {
  const { amount, updatedAt } =
    typeof options === "number" ? { amount: options, updatedAt: undefined } : options;

  const items: Amount[] = [
    { amount: 100, color: AlertColor.Red, updatedAt: new Date("2021-01-21") },
    { amount: 500, color: AlertColor.Yellow, updatedAt: new Date("2021-01-21") },
    { amount: 750, color: AlertColor.Green, updatedAt: new Date("2021-01-21") },
  ];

  const item = items.find((item) => {
    if (updatedAt) {
      return amount <= item.amount && updatedAt < item.updatedAt;
    }
    return amount <= item.amount;
  });

  return item?.color;
};

e.g.3.2 Add more code

const getColorForStockAmount: StockOptions = (options: number | Options) => {
  const { amount, updatedAt } =
    typeof options === "number" ? { amount: options, updatedAt: undefined } : options;

  const items: Amount[] = [
    { amount: 100, color: AlertColor.Red, updatedAt: new Date("2021-01-21") },
    { amount: 500, color: AlertColor.Yellow, updatedAt: new Date("2021-01-21") },
    { amount: 750, color: AlertColor.Green, updatedAt: new Date("2021-01-21") },
  ];

  const item = items.find((item) => {
    if (updatedAt) {
      return amount <= item.amount && updatedAt < item.updatedAt;
    }
    return amount <= item.amount;
  });

  return item?.color;
};

e.g.3.2 Add more code

const getColorForStockAmount: StockOptions = (options: number | Options) => {
  const { amount, updatedAt } =
    typeof options === "number" ? { amount: options, updatedAt: undefined } : options;

  const items: Amount[] = [
    { amount: 100, color: AlertColor.Red, updatedAt: new Date("2021-01-21") },
    { amount: 500, color: AlertColor.Yellow, updatedAt: new Date("2021-01-21") },
    { amount: 750, color: AlertColor.Green, updatedAt: new Date("2021-01-21") },
  ];

  const item = items.find((item) => {
    if (updatedAt) {
      return amount <= item.amount && updatedAt < item.updatedAt;
    }
    return amount <= item.amount;
  });

  return item?.color;
};

e.g.3.2 Add more code

// Define the interface for the function type with a list of overloads
interface StockOptions {
  // The new signature
  (options: Options): AlertColor | undefined;

  /** @deprecated
   * Use { amount: number; updatedAt?: string; } object instead
   * */
  (stock: number): AlertColor | undefined;
}

e.g.3.2 Add more code

4. Higher-Order Function

e.g.4.1 Function as an argument

function evaluatesToFive(num, fn) {
  return fn(num) === 5;
}

function divideByTwo(num) {
  return num / 2;
}

evaluatesToFive(10, divideByTwo);
// true

evaluatesToFive(20, divideByTwo);
// false

e.g.4.2 Return a function

function multiplyBy(num1) {
  return function(num2) {
    return num1 * num2;
  };
}

const multiplyByThree = multiplyBy(3);
const multiplyByFive = multiplyBy(5);

multipyByThree(10); // 30

multiplyByFive(10); // 50

e.g.4.3 As a validator

  1. Must be at least 18 years old
  2. Password must be at least 8 characters long
  3. Must agree to the Terms of Service

e.g.4.3 As a validator

const newUser = {
  age: 24,
  password: 'some long password',
  agreeToTerms: true,
};

function oldEnough(user) {
  return user.age >= 18;
}

function passwordLongEnough(user) {
  return user.password.length >= 8;
}

function agreeToTerms(user) {
  return user.agreeToTerms === true;
}

e.g.4.3 As a validator option 1

function validate(obj, ...tests) {
  for (let i = 0; i < tests.length; i++) {
    if (tests[i](obj) === false) {
      return false;
    }
  }
  return true;
}

e.g.4.3 As a validator option 1

const newUser1 = {
  age: 40,
  password: 'tncy4ty49r2mrx',
  agreeToTerms: true,
};

validate(newUser1, oldEnough, passwordLongEnough, agreeToTerms);
// true

const newUser2 = {
  age: 40,
  password: 'short',
  agreeToTerms: true,
};

validate(newUser2, oldEnough, passwordLongEnough, agreeToTerms);
// false

e.g.4.3 As a validator option 2

function createValidator(...tests) {
  return function(obj) {
    for (let i = 0; i < tests.length; i++) {
      if (tests[i](obj) === false) {
        return false;
      }
    }
    return true;
  };
}

e.g.4.3 As a validator option 2

const userValidator = createValidator(
  oldEnough,
  passwordLongEnough,
  agreeToTerms
);

userValidator(newUser1); // true
userValidator(newUser2); // false

Ref

  1. https://jakearchibald.com/2021/function-callback-risks/
  2. https://lea.verou.me/2021/02/mass-function-overloading-why-and-how/
  3. https://altrim.io/posts/deprecating-function-signature-in-typescript

Thanks