Welcome to Data Types in [A]synchronous Functional Programming. In this article we will discuss the data types used for the [A]synchronous Functional Programming paradigm in JavaScript.
Primitive Data Types
Primitive data types are fundamental, indivisible building blocks for data representation in all programming. For [A]synchronous Functional Programming, we will consider six primitive data types: number, string, boolean, binary, symbol, and nullish.
Number
The number primitive data type represents integer numbers like 1 and also floating-point numbers like 1.2. To create a number in JavaScript you only need to write a number literal.
1You may also use the Number constructor to create a number. You can use the Number constructor to convert other types like strings to numbers.
Number('3') // 3
String
The string primitive data type represents strings like 'abc' or 'Hello World!'. Strings are useful for storing textual data, which is pretty much the entire internet aside from numbers. To create a string in JavaScript you can write a string literal.
'Hello World!'You may also use the String constructor to create a string. You can use the String constructor to convert other types like numbers to strings.
String(3) // '3'
Boolean
The boolean primitive data type represents the logical values true or false. To create a boolean, you can write a boolean literal.
trueSimply writing out the boolean value isn't so useful, however. Normally you would create booleans by using the logical operators ==, >, <, >=, <=, &&, or || on variables. Then you can use them with if statements to control code execution.
const myNumber = 3
const myCondition = myNumber > 2 // myCondition is a boolean
if (myCondition) {
// execute code
}
You can also use the Boolean constructor to create a boolean.
Boolean(0) // false
Binary
The binary primitive data type is useful for storing binary data. Some common forms of binary data are image data and video data. You can use one of the TypedArray constructors to create binary data types.
// Uint8Array is a TypedArray constructor
new Uint8Array([1, 2, 3])
In practice, you usually won't use constructors when working with binary data. Instead, you would access the binary data through an API.
const fileReader = new FileReader()
fileReader.onload = function (event) {
// event.target.result is binary data
}
fileReader.readAsArrayBuffer(myFile)
Symbol
The symbol primitive data type represents unique and immutable values, and is primarily used as identifiers for object properties.
const mySymbol1 = Symbol('description')
const mySymbol2 = Symbol('description')
mySymbol1 == mySymbol2 // false
// mySymbol1 is unique from mySymbol2
Object properties defined with symbols are non-enumerable, and won't be discoverable with standard object iteration methods like for...in loops or Object.keys.
Some useful built-in symbols are Symbol.iterator and Symbol.asyncIterator. These symbols, when used to define properties on objects, implement special protocols for iteration. See iterable protocol and async iterable protocol
Nullish
The nullish data type represents the absence of a meaningful value and encopasses two values: null and undefined. Both of these values are very similar in that they both express the absence of a meaningful value, but they are used differently in practice. Generally, you would use null to express the intentional absence of an object value, while you wouldn't normally have to use undefined, though it is sometimes useful to return undefined from a function.
function myFunction(arg) {
// check is arg is null or undefined
if (arg == null) {
return undefined
}
// continue with function knowing arg is a meaningful value
}
// declare myVar2 without initializing
let myVar2
console.log(myVar2) // undefined
Primitive Versus Reference Data Types
As a rule of thumb, anything that isn't a primitive data type is a reference data type. While primitive data types store actual values (numbers, strings) directly in memory, reference data types store references (memory addresses) to objects. The rest of the data types discussed in this article fall under reference data types.
Collection Data Types
Collection data types are structures that can hold multiple values and multiple types of values, including primitives and other collection data types. The collection data types are fundamental to general programming as well as [A]synchronous Functional Programming, because we often need to think about data in terms of groups. For this article we will consider four essential collection data types: array, object, set, and map.
Array
The array data type is an ordered collection of elements that can be accessed through a numerical index. You can create an array by writing an array literal, or by using the Array constructor.
[1, 2, 3] // array literal
new Array(1, 2, 3) // array constructor
const myArray = ['a', 'b', 'c']
myArray[0] // 'a', accessed at index 0 of myArray
myArray[1] // 'b', accessed at index 1 of myArray
myArray[2] // 'c', accessed at index 2 of myArray
You can also create arrays using static methods on the Array constructor.
Array.from('foo') // ['f', 'o', 'o']
Array.of('foo', 2, 'bar', true) // ['foo', 2, 'bar', true]
Several array instance methods such as .slice and .map also create new arrays.
const myArray = [1, 2, 3, 4, 5]
myArray.slice(0, 3) // [1, 2, 3]
myArray.map(n => n * 2) // [2, 4, 6, 8, 10]
To insert an item into an array, use the .push instance method.
To remove an item from an array, use the .splice instance method.
To iterate through the elements of an array, use a for...of loop.
Object
The object data type is an unordered collection of elements that is accessed by string or symbol key, as opposed to numerical index for arrays. You can create an object by writing an object literal.
{ a: 1, b: 'foo' }You can also use the Object constructor to create an object, though this is less common.
new Object()
To insert an element into an object, use property accessor syntax. Property accessor syntax has two forms: dot notation and bracket notation.
To remove an item from an object, use property accessor syntax with the delete keyword.
To iterate through the enumerable properties of an object, use a for...in loop.
Set
The set data type is a unique collection of elements that is ordered by insertion order. Value equality (what determines the elements' uniqueness) is determined by the SameValueZero algorithm. Although there isn't a way to access an element of a set like there is for arrays and objects, you can tell if a set has an element by using the set's .has method.
To create a set, use the Set constructor.
new Set([1, 2, 3])
To insert an element into a set, use the .add instance method.
To remove an element from a set, use the .delete instance method.
To iterate through the elements of a set, use a for...of loop.
Map
The map data type is a collection of elements ordered by insertion order that can be accessed using keys of any data type. Maps are similar to objects in many regards but with a few crucial differences:
In scenarios involving frequent insertions and deletions of elements, maps are more performant than objects.
Maps need to be first converted to plain objects before they can be serialized, e.g. via
JSON.stringifyMap keys can be any value (including functions, objects, or any primitive), while object keys can only be strings or symbols.
Maps are iterable with
for...ofloops, while objects usefor...inloops
Use the Map constructor to create a map. The Map constructor accepts an array of arrays representing the map's entries.
const m = new Map([
['a', 1],
['b', 2],
['c', 3],
])
To insert an element into a map, use the .set instance method.
To remove an element from a map, use the .delete instance method.
To iterate through the elements of a map, use a for...of loop.
Which Collection Data Structure Is Right for Me?
When thinking about which collection data structure to use for your data, always choose the data structure that most naturally models your data. Arrays are good for lists of data, while objects and maps are good for relational data. Use sets over arrays when you need to be able to easily remove an element from your data.
Iterable Data Types
Iterable data types are data types that can be iterated over. Specifically, all iterable data types implement the iterable protocol. The collection data types excluding object (array, map, and set) are all built-in data types that implement the iterable protocol. Iterables can be consumed with a for...of loop.
const myArray = [1, 2, 3]
myArray[Symbol.iterator]() // Array Iterator
for (const item of myArray) {
// myArray is iterable
}
const myMap = new Map([['a', 1], ['b', 2], ['c', 3]])
myMap[Symbol.iterator]() // MapIterator
for (const [key, value] of myMap) {
// myMap is iterable
}
const mySet = new Set([1, 2, 3])
mySet[Symbol.iterator]() // SetIterator
for (const value of mySet) {
// mySet is iterable
}
Iterable Protocol
The iterable protocol is implemented on classes and objects under the method [Symbol.iterator](). The method returns an object that conforms to the iterator protocol. An object implements the iterator protocol by implementing the synchronous method next.
type Iterator = {
next: (input? any)=>({ value: any, done: boolean })
}
type Iterable = {
[Symbol.iterator]: ()=>Iterator
}You can implement the iterable protocol on your own classes and objects.
Generators and Generator Functions
You can use generator functions to create generators, a kind of iterator. Generator functions use the function* () {} syntax and the yield keyword.
Generators implement the iterator protocol by default, so often it is simpler to use a generator function to implement the iterable protocol using the syntax * [Symbol.iterator]().
Asynchronous Data Types
Asynchronous data types are data types that represent asynchronous operations. For [A]synchronous Functional Programming we will only consider one asynchronous data type: the promise.
Promise
The promise data type represents an asynchronous operation that resolves to a single value or rejects with an error. Promise instances have a .then and a .catch method.
type SyncOrAsyncResolver = any=>Promise|any
type SyncOrAsyncErrorResolver = (Error|any)=>Promise|any
type Promise = {
then: (onFulfilled SyncOrAsyncResolver, onRejected SyncOrAsyncErrorResolver)=>Promise,
catch: (onRejected SyncOrAsyncErrorResolver)=>Promise
}The promise's .then method resolves the promise's resolved value and catches any errors rejected from the promise. Either of the resolvers provided to a promise's .then method may be asynchronous and return a promise.
const onFulfilled = resolvedValue => {
// resolvedValue is the resolved value of promise1
}
const onRejected = error => {
// error is the rejected error of promise1
}
const promise2 = promise1.then(onFulfilled, onRejected)
// promise2 is a promise returned from promise1.then(...)
The promise's .catch method catches any errors rejected from a promise.
myPromise.catch(error => {
// error is rejected from myPromise
})
To create a promise, you can use the Promise constructor.
You can also use the .resolve and .reject methods on the Promise object to create promises.
In practice, you usually do not have to create promises. Instead, most asynchronous APIs will return a promise.
Async/Await
The async function syntax permits the use of the await keyword that enables an imperative style of code to handle promises. You can use the await keyword from an async function to access the resolved value or rejected error of a promise.
Asynchronous Iterable Data Types
Asynchronous iterable data types combine asynchronous data types with iterable data types. All asynchronous iterable data types implement the async iterable protocol. The only built-in data types that implement this protocol are AsyncGenerators. Async iterables are consumable with a for await...of loop.
Async Iterable Protocol
The async iterable protocol is implemented on classes and objects under the method [Symbol.asyncIterator](). The method returns an object that conforms to the async iterator protocol. An object implements the async iterator protocol by implementing the asynchronous method next.
type AsyncIterator = {
next: (input? any)=>Promise<{ value: any, done: boolean }>
}
type AsyncIterable = {
[Symbol.asyncIterator]: ()=>AsyncIterator
}You can implement the async iterable protocol on your own classes and objects.
Async Generators and Async Generator Functions
Async generator functions use the async function* () {} syntax and yield keyword and always return an async iterable AsyncGenerator object.
Async generators implement the async iterator protocol by default, so often it is simpler to use an async generator function to implement the async iterable protocol using the syntax async * [Symbol.asyncIterator]().
Algebraic Structures
Algebraic structures are special classes of data types that are identified by the presence of a specific method. For [A]synchronous Functional Programming, we will consider five algebraic structures: functor, filterable, foldable, semigroup, and monad.
Functor
The functor algebraic structure identifies data types with the .map method. Data types implementing .map must conform to the functor laws:
Identity Law: applying the identity function
a => ato a functor is equivalent to not having applied a function.
assert.equivalent(
myFunctor.map(identity),
myFunctor
)
Composition Law: applying two functions in sequence using
.mapis equivalent to applying their composition in a single.mapoperation.
assert.equivalent(
myFunctor.map(f).map(g),
myFunctor.map(compose(g, f))
)
The following built-in data types are considered to be functors:
arraysetmapgeneratorasync generatorobject
Filterable
The filterable algebraic structure identifies data types with the .filter method. Data types implementing .filter must conform to the following laws:
Distributivity Law: applying two predicate functions in sequence using consecutive calls to
.filteris equivalent to executing both predicate functions in a logical AND expression with a single call to.filter.
assert.equivalent(
myFilterable.filter(x => f(x) && g(x)),
myFilterable.filter(f).filter(g)
)
Identity Law: applying a predicate function that always returns true is equivalent to not having applied a function.
assert.equivalent(
myFilterable.filter(() => true),
myFilterable
)
Annihilation Law: given two distinct filterables, applying a predicate function that always returns false to both filterables produces equivalent results.
assert.equivalent(
myFilterableA.filter(() => false),
myFilterableB.filter(() => false)
)
The following built-in data types are considered to be filterables:
arraysetmapgeneratorasync generatorobject
Foldable
The foldable algebraic structure identifies data types with the .reduce method. Data types implementing .reduce must conform to the following law:
A given reducing operation is equivalent to two chained reducing operations with
.reducewhere the first reduce concatenates every item in the foldable onto an array and the second reduce takes the array and performs the given reducing operation.
assert.equivalent(
myFoldable.reduce(reducer),
myFoldable
.reduce((accumulator, item) => accumulator.concat([item]) , [])
.reduce(reducer)
)
The following built-in data types are considered to be foldables:
arraysetmapgeneratorasync generatorobject
Semigroup
The semigroup algebraic structure identifies data types with the .concat method. Data types implementing .concat must conform to the following law:
Associativity: the grouping of items between concatenation operations on a semigroup does not affect the final result.
assert.equivalent(
mySemigroup.concat(a).concat(b, c),
mySemigroup.concat(a, b).concat(c)
)
The following built-in data types are considered to be semigroups:
arraystringsetbinaryobject
Monad
The monad algebraic structure identifies data types with the .flatMap or .chain methods. Data types implementing .flatMap or .chain must conform to the monad laws:
Left Identity: wrapping a value in a monad and then calling the monad's
.flatMapor.chainwith a function is equivalent to directly applying the function to the value, given the function returns a monad.
assert.equivalent(
MyMonad.of(a).flatMap(f),
f(a)
)
Right Identity: given a monad, chaining a function that wraps a value in a monad should result in the given monad.
assert.equivalent(
myMonad.flatMap(MyMonad.of),
myMonad
)
Associativity: the order of execution of chaining monadic operations on a monad does not affect the final result.
// f and g are functions that return a monad
assert.equivalent(
myMonad.flatMap(f).flatMap(g),
myMonad.flatMap(x => f(x).flatMap(g))
)
The following built-in data types are considered to be monads:
arraystringsetgeneratorasync generatorobject
Conclusion
This concludes Data Types in [A]synchronous Functional Programming.
If you are interested in getting started with Rubico and [A]synchronous Functional Programming, please visit Rubico's home page: rubico.land.


