Home

Awesome

space-lift "Lift your values into space for infinite possibilities"

Note: Starting from version 1.0.0, space-lift no longer contains the Option and Result monads. You can find these at space-monad

Rich Array, Object, Map, Set wrapper

Design goals

spacelift lib size

<a name="howtouse"></a>

How to use

Here's everything that can be imported from space-lift:

import {
  lift,
  update,
  range,
  is,
  createUnion,
  createEnum,
  identity,
  noop,
  Result,
  Ok,
  Err,
  Immutable
} from 'space-lift'

<a name="examples"></a>

Some Examples

Update an object inside an Array

import { update } from 'space-lift'

const people = [
  { id: 1, name: 'jon' },
  { id: 2, name: 'sarah' },
  { id: 3, name: 'nina' }
]

const updatedPeople = update(people, draft => {
  draft.updateIf(
    p => p.id === 2,
    personDraft => {personDraft.name = 'Nick'})
})

Sort on two fields

import lift from 'space-lift'

const people = [
  { first: 'jon',   last: 'haggis' },
  { first: 'sarah', last: 'john' },
  { first: 'nina',  last: 'pedro' }
]

// This will create an Array sorted by first name, then by last name
const sortedPeople = lift(people)
  .sort(p => p.first, p => p.last)
  .value()

<a name="api"></a>

API

<a name="api.array"></a>

Array

<a name="array.append"></a>

Array.append

Appends one item at the end of the Array.

import {lift} from 'space-lift'
const updated = lift([1, 2, 3]).append(4).value() // [1, 2, 3, 4]

<a name="array.appendAll"></a>

Array.appendAll

Appends an Iterable of items at the end of the Array.

import {lift} from 'space-lift'
const updated = lift([1, 2, 3]).appendAll([4, 5]).value() // [1, 2, 3, 4, 5]

<a name="array.compact"></a>

Array.compact

Filters all the falsy elements out of this Array.
All occurences of false, null, undefined, 0, "" will be removed.

import {lift} from 'space-lift'
const updated = lift([1, null, 2, 3, undefined]).compact().value() // [1, 2, 3]

<a name="array.count"></a>

Array.count

Counts the items satisfying a predicate.

import {lift} from 'space-lift'
const count = lift([1, 2, 3]).count(n => n > 1) // 2

<a name="array.collect"></a>

Array.collect

Maps this Array's items, unless void or undefined is returned, in which case the item is filtered.
This is effectively a filter + map combined in one.

import {lift} from 'space-lift'
const count = lift([1, 2, 3]).collect(n => {
  if (n === 1) return;
  return `${n*10}`
}).value() // ['20', '30']

<a name="array.distinct"></a>

Array.distinct

Creates an array without any duplicate item.
If a key function is passed, items will be compared based on the result of that function;
if not, they will be compared using strict equality.

import {lift} from 'space-lift'

const people = [{id: 1, name: 'Alexios'}, {id: 2, name: 'Bob'}, {id: 1, name: 'Alessia'}]

// [{id: 1, name: 'Alexios'}, {id: 2, name: 'Bob'}]
const deduped = lift(people).distinct(p => p.id).value()

<a name="array.drop"></a>

Array.drop

Drops the first 'count' items from this Array.

import {lift} from 'space-lift'
const updated = lift([1, 2, 3]).drop(2).value() // [3]

<a name="array.dropRight"></a>

Array.dropRight

Drops the last 'count' items from this Array.

import {lift} from 'space-lift'
const updated = lift([1, 2, 3]).dropRight(2).value() // [1]

<a name="array.filter"></a>

Array.filter

Filters this array by aplying a predicate to all items and refine its type.

import {lift} from 'space-lift'
const filtered = lift([1, 2, 3]).filter(n => n > 1).value() // [2, 3]

<a name="array.first"></a>

Array.first

Returns the first element in this Array or undefined.

import {lift} from 'space-lift'
const first = lift([1, 2, 3]).first() // 1

<a name="array.flatMap"></a>

Array.flatMap

Maps this Array to an Array of Array | ArrayWrapper using a mapper function then flattens it.

import {lift} from 'space-lift'
const mapped = lift([1, 2, 3]).flatMap(n => [n + 1, n + 2]).value() // [2, 3, 3, 4, 4, 5]

<a name="array.flatten"></a>

Array.flatten

Flattens this Array of Arrays.

import {lift} from 'space-lift'
const flattened = lift([1, [2], [3, 4]]).flatten().value() // [1, 2, 3, 4]

<a name="array.reduce"></a>

Array.reduce

Reduces this Array into a single value, using a starting value.

import {lift} from 'space-lift'
const count = lift([1, 2, 3]).reduce(0, (count, n) => count + n) // 6

<a name="array.get"></a>

Array.get

Returns the item found at the provided index or undefined.

import {lift} from 'space-lift'
const secondItem = lift([1, 2, 3]).get(1) // 2

<a name="array.groupBy"></a>

Array.groupBy

Creates a Map where keys are the results of running each element through a discriminator function.
The corresponding value of each key is an array of the elements responsible for generating the key.

import {lift} from 'space-lift'
const people = [
  { age: 10, name: 'jon' },
  { age: 30, name: 'momo' },
  { age: 10, name: 'kiki' },
  { age: 28, name: 'jesus' },
  { age: 29, name: 'frank' },
  { age: 30, name: 'michel' }
]

// Map<number, Array<{age: number, name: string}>>
const peopleByAge = lift(people).groupBy(p => p.age).value()

<a name="array.grouped"></a>

Array.grouped

Creates a new Array where each sub array contains at most 'bySize' elements.

import {lift} from 'space-lift'
const numbers = [1, 2, 3, 4, 5, 6, 7]

// [[1, 2], [3, 4], [5, 6], [7]]
const groupedNumbers = lift(numbers).grouped(2).value()

<a name="array.insert"></a>

Array.insert

Inserts an item at a specified index.

import {lift} from 'space-lift'
const updated = lift(['1', '2', '3']).insert(1, '20').value() // [1, 20, 2, 3]

<a name="array.last"></a>

Array.last

Returns the item found at the last index or undefined.

import {lift} from 'space-lift'
const last = lift(['1', '2', '3']).last() // '3'

<a name="array.map"></a>

Array.map

Maps this Array using a mapper function.

import {lift} from 'space-lift'
const mapped = lift(['1', '2', '3']).map(str => '0' + str).value() // ['01', '02', '03']

<a name="array.removeAt"></a>

Array.removeAt

Removes the item found at the specified index.

import {lift} from 'space-lift'
const updated = lift(['1', '2', '3']).removeAt(1).value() // ['1', '3']

<a name="array.reverse"></a>

Array.reverse

Reverses the Array.

import {lift} from 'space-lift'
const updated = lift(['1', '2', '3']).reverse().value() // ['3', '2', '1']

<a name="array.sort"></a>

Array.sort

Sorts the Array in ascending order, using one or more iterators specifying which field to compare.
For strings, localCompare is used.
The sort is stable if the browser uses a stable sort (all modern engines do)

import {lift} from 'space-lift'

const people = [
  { name: 'Jesse', creationDate: 2 },
  { name: 'Walt', creationDate: 1 },
  { name: 'Mike', creationDate: 4 },
  { name: 'Skyler', creationDate: 3 }
]

const sorted = lift(people)
  .sort(p => p.creationDate)
  .map(p => p.name)
  .value() // ['Walt', 'Jesse', 'Skyler', 'Mike']

<a name="array.take"></a>

Array.take

Takes the first 'count' items from this Array.

import {lift} from 'space-lift'
const updated = lift(['1', '2', '3']).take(2).value() // ['1', '2']

<a name="array.takeRight"></a>

Array.takeRight

Takes the last 'count' items from this Array.

import {lift} from 'space-lift'
const updated = lift(['1', '2', '3']).takeRight(2).value() // ['2', '3']

<a name="array.toSet"></a>

Array.toSet

Converts this Array to a Set.

import {lift} from 'space-lift'
const set = lift(['1', '2', '2', '3']).toSet().value() // Set(['1', '2', '3'])

<a name="array.updateAt"></a>

Array.updateAt

Updates an item at the specified index.

import {lift} from 'space-lift'
const updated = lift(['1', '2', '2', '3']).updateAt(1, '20').value() // ['1', '20', '2', '3']

<a name="array.pipe"></a>

Array.pipe

Pipes this Array with an arbitrary transformation function.

import {lift} from 'space-lift'
const updated = lift([1, 0, 3]).pipe(JSON.stringify) // '[1, 0, 3]'

<a name="api.object"></a>

Object

<a name="object.add"></a>

Object.add

Adds a new key/value to this object. This creates a new type.
To add a nullable key to an object while preserving its type, use update instead.

import {lift} from 'space-lift'

const updated = lift({a: 1, b: 2}).add(c, 3).value() // {a: 1, b: 2, c: 3}

<a name="object.isEmpty"></a>

Object.isEmpty

Returns whether this object contains no keys.

import {lift} from 'space-lift'

const isEmpty = lift({a: 1, b: 2}).isEmpty() // false

<a name="object.keys"></a>

Object.keys

Creates an Array of all this object's keys, in no particular order.
If the keys are a subtype of string, the Array will be typed with the proper key union type.

import {lift} from 'space-lift'

const isEmpty = lift({a: 1, b: 2}).keys().value() // ['a', 'b']

<a name="object.mapValue"></a>

Object.mapValue

Maps one of this Object values, by key.
This is similar to remove('key').add('key', newValue) but is less error prone.
This can change the type of the object.

import {lift} from 'space-lift'

const mappedValues = lift({
  a: 1,
  b: 2
})
.mapValue('b', num => `${num * 2}`)
.value() // { a: 1, b: '4' }

<a name="object.mapValues"></a>

Object.mapValues

Maps this Object values using a mapper function.
This is mostly useful for objects with a single value type.

import {lift} from 'space-lift'

const mappedValues = lift({
  chan1: [1, 2, 3],
  chan2: [10, 11, 12]
})
.mapValues(numbers => numbers.map(n => n * 2))
.value() // { chan1: [2, 4, 6], chan2: [20, 22, 24] }

<a name="object.pipe"></a>

Object.pipe

Pipes this Object with an arbitrary transformation function.

import {lift} from 'space-lift'

const updated = lift({a: 1}).pipe(JSON.stringify) // '{"a": 1}'

<a name="object.remove"></a>

Object.remove

Removes a key/value from this object and return a new object (and type)
To delete a (nullable) key from an object while preserving its type, use "update()" instead.

import {lift} from 'space-lift'

const updated = lift({a: 1, b: 2, c: 3}).remove('c').value() // {a: 1, b: 2}

<a name="object.values"></a>

Object.values

Creates an Array with all these object's values.

import {lift} from 'space-lift'

const updated = lift({a: 1, b: 2, c: 3}).values().value() // [1, 2, 3]

<a name="object.toArray"></a>

Object.toArray

Converts this Object to an Array of tuples.
Similar to Object.entries() but retains the type of keys.

import {lift} from 'space-lift'

const updated = lift({a: 1, b: 2, c: 3}).toArray().value() // [['a', 1], ['b', 2], ['c', 3]]

<a name="object.toMap"></a>

Object.toMap

Transforms this Object to a Map where the keys are the string typed keys of this Object.

import {lift} from 'space-lift'

const updated = lift({a: 1, b: 2, c: 3}).toMap().value() // Map([['a', 1], ['b', 2], ['c', 3]])

<a name="api.map"></a>

Map

<a name="map.set"></a>

Map.set

Sets a new key/value.

import {lift} from 'space-lift'

const map = new Map([
  ['a', 1],
  ['b', 2]
])

const updated = lift(map).set('c', 3).value() // Map([['a', 1], ['b', 2], ['c', 3]])

<a name="map.delete"></a>

Map.delete

Deletes a key/value.

import {lift} from 'space-lift'

const map = new Map([
  ['a', 1],
  ['b', 2]
])

const updated = lift(map).delete('b').value() // Map([['a', 1]])

<a name="map.collect"></a>

Map.collect

Maps this Map's keys and values, unless void or undefined is returned, in which case the entry is filtered.
This is effectively a filter + map combined in one.

import {lift, update} from 'space-lift'

const map = new Map([
  [1, { id: 1, name: 'aa' }],
  [2, { id: 2, name: 'bb' }]
])

const updated = lift(map).collect((key, value) => {
  if (key === 2) return
  return [
    key * 10,
    update(value, v => { v.name = `${v.name}$` })
  ]
}).value() // Map([[10, {id: 2, name: 'bb$'}]])

<a name="map.filter"></a>

Map.filter

Filters this Map's keys and values by aplying a predicate to all values and refine its type.

import {lift} from 'space-lift'

const map = new Map([
  ['a', 1],
  ['b', 2]
])

const updated = lift(map).filter((key, value) => key === 1).value() // Map([['a', 1]])

<a name="map.first"></a>

Map.first

Returns the first element in this Map or undefined.

import {lift} from 'space-lift'

const map = new Map([
  [1, { id: 1, name: 'Walter' }],
  [2, { id: 2, name: 'Jesse' }]
])

const first = lift(map).first() // { id: 1, name: 'Walter' }

<a name="map.last"></a>

Map.last

Returns the last element in this Map or undefined.

import {lift} from 'space-lift'

const map = new Map([
  [1, { id: 1, name: 'Walter' }],
  [2, { id: 2, name: 'Jesse' }]
])

const first = lift(map).last() // { id: 2, name: 'Jesse' }

<a name="map.mapValues"></a>

Map.mapValues

Maps this map's values.

import {lift} from 'space-lift'

const map = new Map([
  ['a', 1],
  ['b', 2]
])

const updated = lift(map).filter(value => value * 2).value() // Map([['a', 2], ['b', 4]])

<a name="map.pipe"></a>

Map.pipe

Pipes this Map with an arbitrary transformation function.

import {lift} from 'space-lift'

const map = new Map([
  ['a', 1],
  ['b', 2]
])

const yay = lift(map).pipe(m => m.toString()) // '[object Map]' 

<a name="map.setDefaultValue"></a>

Map.setDefaultValue

If this key is missing, set a default value.

import {lift} from 'space-lift'

const map = new Map([
  ['a', 1],
  ['b', 2]
])

const map = lift(map)
  .setDefaultValue('c', 3)
  .setDefaultValue('b', 10)
  .value() // Map([['a', 1], ['b', 2], ['c', 3]])

<a name="map.updateValue"></a>

Map.updateValue

Same as update(map, draft => draft.updateValue, exposed here for convenience and readability as it's often used immediately after setDefaultValue.

import {lift} from 'space-lift'

const map = new Map([
  ['a', {name: 'a'}],
  ['b', {name: 'b'}]
])

const map = lift(map).updateValue('b', draft => { draft.name = 'c' }).value()

<a name="map.toArray"></a>

Map.toArray

Transforms this Map into an Array of [key, value] tuples.

import {lift} from 'space-lift'

const map = new Map([
  ['a', 1],
  ['b', 2]
])

const array = lift(map).toArray().value() // [ ['a', 1], ['b', 2] ]

<a name="map.toObject"></a>

Map.toObject

Transforms this Map into an Object.
Only available if this Map's keys are a subtype of string or number.

import {lift} from 'space-lift'

const map = new Map([
  ['a', 1],
  ['b', 2]
])

const array = lift(map).toObject().value() // { 'a': 1, 'b': 2 }

<a name="api.set"></a>

Set

<a name="set.add"></a>

Set.add

Adds a new value to this Set.

import {lift} from 'space-lift'

const set = new Set([1, 2, 3])

const updated = lift(set).add(4).add(3).value() // Set([1, 2, 3, 4])

<a name="set.addAll"></a>

Set.addAll

Adds all items from the passed iterable to this Set.

import {lift} from 'space-lift'

const set = new Set([1, 2, 3])

const updated = lift(set).addAll([4, 5, 3]).value() // Set([1, 2, 3, 4, 5])

<a name="set.delete"></a>

Set.delete

Deletes one value from this Set.

import {lift} from 'space-lift'

const set = new Set([1, 2, 3])

const updated = lift(set).delete(2).value() // Set([1, 3])

<a name="set.collect"></a>

Set.collect

Maps this Set's items, unless void or undefined is returned, in which case the item is filtered.
This is effectively a filter + map combined in one.

import {lift} from 'space-lift'

const set = new Set([1, 2, 3])

const updated = lift(set).collect(num => {
  if (num === 2) return
  return num * 2
}).value() // Set([2, 6])

<a name="set.filter"></a>

Set.filter

Filters this Set's items by aplying a predicate to all values and refine its type.

import {lift} from 'space-lift'

const set = new Set([1, 2, 3])

const updated = lift(set).filter(num => num !== 2).value() // Set([1, 3])

<a name="set.intersection"></a>

Set.intersection

Returns the Set of all items of this Set that are also found in the passed Set.

import {lift} from 'space-lift'

const set = new Set([1, 2, 3])
const otherSet = new Set([2, 3, 4])

const intersection = lift(set).intersection(otherSet).value() // Set([2, 3])

<a name="set.difference"></a>

Set.difference

Returns the Set of all items of this Set that are not found in the passed Set.

import {lift} from 'space-lift'

const set = new Set([1, 2, 3])
const otherSet = new Set([2, 3, 4])

const diff = lift(set).difference(otherSet).value() // Set([1])

<a name="set.pipe"></a>

Set.pipe

Pipes this Set with an arbitrary transformation function.

import {lift} from 'space-lift'

const set = new Set([1, 2, 3])

const yay = lift(set).pipe(s => s.toString()) // '[object Set]' 

<a name="set.toArray"></a>

Set.toArray

Transforms this Set into an Array. The insertion order is kept.

import {lift} from 'space-lift'

const set = new Set([1, 2, 3])

const array = lift(set).toArray().value() // [1, 2, 3]

<a name="api.update"></a>

update

update is your go-to function to perform immutable updates on your Objects, Array, Map and Set, using a mutable API. If you know immerjs, it's very similar (great idea!) but with different design constraints in mind:

<a name="api.update.object"></a>

update for Object

Accessing a draft object property is the only Object operation that will create a draft

Adding/updating an Object property

import {update} from 'space-lift'

const obj: { a: 1; b?: number } = { a: 1 }

const updated = update(obj, draft => {
  draft.b = 20
})

Deleting an Object property

import {update} from 'space-lift'

const obj: { a: 1; b?: number } = { a: 1, b: 20 }

const updated = update(obj, draft => {
  delete draft.b
})

<a name="api.update.map"></a>

update for Map

All regular methods are available.

Map - Updating an existing value

import {update} from 'space-lift'

const map = new Map([
  [1, { id: 1, name: 'jon' }],
  [2, { id: 2, name: 'Julia' }]
])

const updated = update(map, draft => {
  const value = draft.get(2)
  if (value) return
  
  value.name = 'Bob'
})

<a name="api.update.map.updateValue"></a>

Map - Using updateValue

If the key is found, run the drafted value through an update function. For primitives, the update function must return a new value whereas for objects, the drafted value can be modified directly.

import {update} from 'space-lift'

const map = new Map([
  [1, { id: 1, name: 'jon' }],
  [2, { id: 2, name: 'Julia' }]
])

const updated = update(map, draft =>
  draft.updateValue(2, value => { value.name = 'Julia' }))

<a name="api.update.set"></a>

update for Set

All regular Set methods are available.
None of the Set draft methods will create a draft as a Set never hands value over.
Still, it's useful to update an immutable Set whether it's found nested in a tree or not and Sets are most of the time only useful for primitives values that wouldn't be drafted.

<a name="api.update.array"></a>

update for Array

Most Array methods are available but some are removed to make working with Arrays more pleasant:

As a result, the interface of a draft Array is not fully compatible with Array/ReadonlyArray and you must use toDraft if you want to assign a regular Array to a draft Array.

Array - using updateIf

import {update} from 'space-lift'

const arr = [
  { id: 1, name: 'Jon' },
  { id: 3, name: 'Julia' }
]

const updated = update(arr, draft => {
  draft.updateIf(
    (item, index) => item.id === 3,
    item => {
      item.name = 'Bob'
    }
  )
})

<a name="api.enum"></a>

createEnum

Creates a type safe string enumeration from a list of strings, providing:
the list of all possible values, an object with all enum keys and the derived type of the enum in a single declaration.

  import { createEnum } from 'space-lift/es/enum'

  const color = createEnum('green', 'orange', 'red')

  // We can use the derived type
  export type Color = typeof color.T

  // We can list all enum values as a Set.
  color.values // Set(['green', 'orange', 'red'])

  // We can access each value of the enum directly if that's useful
  export const Color = color.enum

  const redish: Color = 'red'
  const greenish: Color = Color.green
  const orange: 'orange' = Color.orange
  orange // 'orange'

<a name="api.union"></a>

createUnion

Creates a type-safe union, providing: derived types, factories and type-guards in a single declaration.

  import { createUnion } from 'space-lift'

  // Let's take the example of a single input Form that can send a new message or edit an existing one.
  // createUnion() gives you 3 tools:
  // T: the derived type for the overall union
  // is: a typeguard function for each state
  // Lastly, the returned object has a key acting as a factory for each union member
  const formState = createUnion({
    creating: () => ({}),
    editing: (msgId: string) => ({ msgId }),
    sendingCreation: () => ({}),
    sendingUpdate: (msgId: string) => ({ msgId }),
  });

  // The initial form state is 'creating'
  let state: typeof formState.T = formState.creating() // { type: 'creating' }

  // If the user wants to edit an existing message, we have to store the edited message id. Lets update our state.
  onClickEdit(msgId: string) {
    state = formState.editing(msgId) // { type: 'editing', msgId: 'someId' }
  }

  // In edition mode, we could want to get the message and change the send button label
  if (formState.is('editing')(state)) {
    getMessage(state.msgId) // thanks to the typeguard function, we know msgId is available in the state
    buttonLabel = 'Update message'
  }

  // If needed, we can also access the derived type of a given state
  type EditingType = typeof formState.editing.T
  const editingObj: EditingType = formState.editing('someId')

<a name="api.result"></a>

Result

A Result is the result of a computation that may fail. An Ok represents a successful computation, while an Err represent the error case.

<a name="Result"></a>

Importing Result

Here's everything that can be imported to use Results:

import { Result, Ok, Err } from 'space-lift'

const ok = Ok(10) // {ok: true, value: 10}
const err = Err('oops') // {ok: false, error: 'oops'}

<a name="api.toDraft"></a>

toDraft

TS currently has a limitation where this library must type its getter the same as its setters. Thus, if you want to assign an entirely new value that contains a type not compatible with its drafted type (so anything but primitives and objects) you will need to use toDraft:

import {update, toDraft} from 'space-lift'

const updated = update({arr: [1, 2, 3]}, draft => {
  draft.arr = toDraft([4, 5, 6])
})

This limitation might be fixed one day: TS ticket

<a name="autounwrap"></a>

Auto unwrap

Most of the time, you will have to call .value() to read your value back.
Because it's distracting to write .value() more than once per chain, some operators will automatically unwrap values returned from their iterators (like Promise->then). These operators are: