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
- 100% immutable, no magic, no overwhelming polymorphism or dynamic operators
- Fun to use
- Correctness and first-class typescript typings
- Tiny and performant (
space-lift
weights7.2 kB
minified for roughly the same scope aslodash
(70.0 kB
) ANDimmerjs
(15.6 kB
)) and no amount of tree shaking can save you from heavy abstractions imported by all modules. - Small set of functions, configurable with lambdas
- Cover 95% of frontend data transformation needs without becoming a bloated lib just to cover the remaining 5%
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'
lift
is the main attraction and is used to wrap an Array, Object, Map or Set to give it extra functionalitiesupdate
can update an Object, Array, Map or Set without modifying the originalrange
is a factory function for Arrays of numbersis
is a helper used to determine if an instance is of a particular type (e.gis.array([]) === true
)createUnion
creates a bunch of useful things when working with discriminated unions.createEnum
creates a bunch of useful things when working with a string based enum.identity
the identity functionnoop
a function that does nothingResult
,Ok
,Err
are used to work with computation that may failImmutable
a helper type that will recursively make a treeReadonly
.
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()
API
Array
- append
- appendAll
- compact
- count
- collect
- distinct
- drop
- dropRight
- filter
- first
- flatMap
- flatten
- reduce
- get
- groupBy
- grouped
- insert
- last
- map
- removeAt
- reverse
- sort
- take
- takeRight
- toSet
- updateAt
- pipe
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]
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]
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]
Array.count
Counts the items satisfying a predicate.
import {lift} from 'space-lift'
const count = lift([1, 2, 3]).count(n => n > 1) // 2
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']
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()
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]
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]
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]
Array.first
Returns the first element in this Array or undefined.
import {lift} from 'space-lift'
const first = lift([1, 2, 3]).first() // 1
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]
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]
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
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
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()
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()
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]
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'
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']
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']
Array.reverse
Reverses the Array.
import {lift} from 'space-lift'
const updated = lift(['1', '2', '3']).reverse().value() // ['3', '2', '1']
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']
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']
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']
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'])
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']
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]'
Object
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}
Object.isEmpty
Returns whether this object contains no keys.
import {lift} from 'space-lift'
const isEmpty = lift({a: 1, b: 2}).isEmpty() // false
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']
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' }
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] }
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}'
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}
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]
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]]
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]])
Map
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]])
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]])
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$'}]])
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]])
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' }
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' }
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]])
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]'
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]])
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()
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] ]
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 }
Set
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])
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])
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])
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])
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])
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])
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])
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]'
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]
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:
- Tiny implementation (The entirety of
space-lift
is way smaller thanimmerjs
) - The
Array
draft has special methods to update it as the traditional mutable Array API in JavaScript is awful. - Instead of eagerly creating tons of costly
Proxies
(also calleddrafts
), they are created only when strictly needed (look for: will create a draft in the documentation below). drafts
are only created for values of typeObject
,Array
,Map
orSet
.update
should never have a returned value and will prevent it at the type level.- Remember that if you iterate through keys, values, etc drafts will NOT be created by default. Call one of the draft creating methods within the loop to perform the updates conditionally.
- As long as you keep accessing drafts, the update can be done at any level of a tree.
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
})
update for Map
All regular methods are available.
get
will will create a draft for the returned value.setDefaultValue
will create a draft
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'
})
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' }))
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.
update for Array
Most Array methods are available but some are removed to make working with Arrays more pleasant:
splice
: Replaced byinsert
,removeIf
.unshift
: Replaced bypreprend
.shift
: Replaced byremoveIf
.pop
: Replaced byremoveIf
.push
: Replaced byappend
.map
is not removed butupdateIf
is added as the conceptual, mutable equivalent.
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.
- Accessing an Array element by index will create a draft (be careful with this if you somehow end up manually iterating the Array)
updateIf
will create a draft for each item satisfying its predicate.
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'
}
)
})
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'
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')
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.
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'}
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
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:
Array.map
Array.flatMap
Array.updateAt
pipe