a shallow river in northeastern Italy, just south of Ravenna

[A]synchronous Functional Programming - Data Types

Updated October 9, 2026 by Richard Tong, King of Technology at CLOUŢ

/assets/monad.png

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.

1

You 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.

true

Simply 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.stringify

  • Map 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...of loops, while objects use for...in loops

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:

  1. Identity Law: applying the identity function a => a to a functor is equivalent to not having applied a function.

assert.equivalent(
  myFunctor.map(identity),
  myFunctor
)
>
  1. Composition Law: applying two functions in sequence using .map is equivalent to applying their composition in a single .map operation.

assert.equivalent(
  myFunctor.map(f).map(g),
  myFunctor.map(compose(g, f))
)
>

The following built-in data types are considered to be functors:

  • array

  • set

  • map

  • generator

  • async generator

  • object

Filterable

The filterable algebraic structure identifies data types with the .filter method. Data types implementing .filter must conform to the following laws:

  1. Distributivity Law: applying two predicate functions in sequence using consecutive calls to .filter is 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)
)
>
  1. Identity Law: applying a predicate function that always returns true is equivalent to not having applied a function.

assert.equivalent(
  myFilterable.filter(() => true),
  myFilterable
)
>
  1. 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:

  • array

  • set

  • map

  • generator

  • async generator

  • object

Foldable

The foldable algebraic structure identifies data types with the .reduce method. Data types implementing .reduce must conform to the following law:

  1. A given reducing operation is equivalent to two chained reducing operations with .reduce where 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:

  • array

  • set

  • map

  • generator

  • async generator

  • object

Semigroup

The semigroup algebraic structure identifies data types with the .concat method. Data types implementing .concat must conform to the following law:

  1. 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:

  • array

  • string

  • set

  • binary

  • object

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:

  1. Left Identity: wrapping a value in a monad and then calling the monad's .flatMap or .chain with 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)
)
>
  1. 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
)
>
  1. 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:

  • array

  • string

  • set

  • generator

  • async generator

  • object

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.

Transducers Crash Course

Updated October 9, 2026 by Richard Tong, King of Technology at CLOUŢ

A crash course on Rubico transducers.

[A]synchronous Functional Programming - Intro

Updated October 9, 2026 by Richard Tong, King of Technology at CLOUŢ

https://rubico.land/assets/rubico-logo-3-3.jpg

An introduction to the [A]synchronous Functional Programming paradigm.

[A]synchronous Functional Programming - Handling HTTP

Updated October 9, 2026 by Richard Tong, King of Technology at CLOUŢ

/assets/HTTP_logo.png

Handling HTTP in [A]synchronous Functional Programming.

[A]synchronous Functional Programming - Statements vs Expressions

Updated October 9, 2026 by Richard Tong, King of Technology at CLOUŢ

Statements vs Expressions in [A]synchronous Functional Programming.