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
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%
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
.
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'})
})
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()
- 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
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]
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]
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]
Counts the items satisfying a predicate.
import {lift} from 'space-lift'
const count = lift([1, 2, 3]).count(n => n > 1) // 2
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']
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()
Drops the first 'count' items from this Array.
import {lift} from 'space-lift'
const updated = lift([1, 2, 3]).drop(2).value() // [3]
Drops the last 'count' items from this Array.
import {lift} from 'space-lift'
const updated = lift([1, 2, 3]).dropRight(2).value() // [1]
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]
Returns the first element in this Array or undefined.
import {lift} from 'space-lift'
const first = lift([1, 2, 3]).first() // 1
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]
Flattens this Array of Arrays.
import {lift} from 'space-lift'
const flattened = lift([1, [2], [3, 4]]).flatten().value() // [1, 2, 3, 4]
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
Returns the item found at the provided index or undefined.
import {lift} from 'space-lift'
const secondItem = lift([1, 2, 3]).get(1) // 2
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()
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()
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]
Returns the item found at the last index or undefined.
import {lift} from 'space-lift'
const last = lift(['1', '2', '3']).last() // '3'
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']
Removes the item found at the specified index.
import {lift} from 'space-lift'
const updated = lift(['1', '2', '3']).removeAt(1).value() // ['1', '3']
Reverses the Array.
import {lift} from 'space-lift'
const updated = lift(['1', '2', '3']).reverse().value() // ['3', '2', '1']
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']
Takes the first 'count' items from this Array.
import {lift} from 'space-lift'
const updated = lift(['1', '2', '3']).take(2).value() // ['1', '2']
Takes the last 'count' items from this Array.
import {lift} from 'space-lift'
const updated = lift(['1', '2', '3']).takeRight(2).value() // ['2', '3']
Converts this Array to a Set.
import {lift} from 'space-lift'
const set = lift(['1', '2', '2', '3']).toSet().value() // Set(['1', '2', '3'])
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']
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]'
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}
Returns whether this object contains no keys.
import {lift} from 'space-lift'
const isEmpty = lift({a: 1, b: 2}).isEmpty() // false
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']
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' }
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] }
Pipes this Object with an arbitrary transformation function.
import {lift} from 'space-lift'
const updated = lift({a: 1}).pipe(JSON.stringify) // '{"a": 1}'
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}
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]
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]]
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]])
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]])
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]])
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$'}]])
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]])
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' }
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' }
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]])
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]'
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]])
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()
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] ]
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 }
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])
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])
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])
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])
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])
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])
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])
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]'
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
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.
Accessing a draft object property is the only Object
operation that will create a draft
import {update} from 'space-lift'
const obj: { a: 1; b?: number } = { a: 1 }
const updated = update(obj, draft => {
draft.b = 20
})
import {update} from 'space-lift'
const obj: { a: 1; b?: number } = { a: 1, b: 20 }
const updated = update(obj, draft => {
delete draft.b
})
All regular methods are available.
get
will will create a draft for the returned value.setDefaultValue
will create a draft
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'
})
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' }))
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.
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.
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'
}
)
})
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'
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 Result
is the result of a computation that may fail. An Ok
represents a successful computation, while an Err
represent the error case.
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'}
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
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