WizJS

A practical, opinionated toolbox for everyday code, with Python's itertools, functools and collections in JavaScript at its core: 18 lazy generators, cache and partial, Counter and DefaultDict — almost all under their Python names. New: BBTreeMap, an ordered map built after a Rust B-tree, with lookups up to 2.75× faster than a classic B-tree's. Every area is its own package subpath, so you can require lang/checks on its own and leave the rest behind. Zero runtime dependencies; plain CommonJS, tested on Node.

npm install @wizhut_tech/wizjs

Python's itertools, Counter and defaultdict in JavaScript

  1. 1itertools → lang/itertools

    The same names in the same argument order, and just as lazy: every function but take is a generator, so nothing runs until you read from it — with for…of, spread or Array.from. Tuples come back as arrays.

    zip, range and enumerate, which are builtins in Python, live here too.

    Python

    import itertools
    
    evens = itertools.count(0, 2)
    list(itertools.islice(evens, 4))
    # [0, 2, 4, 6]
    list(itertools.pairwise('abc'))
    # [('a', 'b'), ('b', 'c')]

    JavaScript, WizJS

    const itertools = require(
      '@wizhut_tech/wizjs/lang/itertools'
    )
    
    const evens = itertools.count(0, 2)
    Array.from(itertools.islice(evens, 4))
    // → [0, 2, 4, 6]
    Array.from(itertools.pairwise('abc'))
    // → [['a', 'b'], ['b', 'c']]
  2. 2Counter → lang/collections

    Built the same way, from a string, an array or any other iterable. Square brackets become get, which answers 0 for anything unseen without adding it, and most_common is spelled mostCommon.

    total and a lazy elements are there too; update, subtract and Counter arithmetic are not.

    Python

    from collections import Counter
    
    c = Counter('banana')
    c.most_common(2)
    # [('a', 3), ('n', 2)]
    c['z']
    # 0

    JavaScript, WizJS

    const { Counter } = require(
      '@wizhut_tech/wizjs/lang/collections'
    )
    
    const c = new Counter('banana')
    c.mostCommon(2)
    // → [['a', 3], ['n', 2]]
    c.get('z')
    // → 0
  3. 3defaultdict → DefaultDict

    Also in lang/collections, and it takes a factory the same way — Array for a list of groups. get fills a missing key from the factory, as square brackets do in Python, and peek reads without filling, like Python's d.get.

    Without a factory, a missing key gives undefined rather than a KeyError.

    Python

    from collections import defaultdict
    
    groups = defaultdict(list)
    for w in ['ant', 'bee', 'ape']:
        groups[w[0]].append(w)
    dict(groups)
    # {'a': ['ant', 'ape'], 'b': ['bee']}

    JavaScript, WizJS

    const { DefaultDict } = require(
      '@wizhut_tech/wizjs/lang/collections'
    )
    
    const groups = new DefaultDict(Array)
    for (const w of ['ant', 'bee', 'ape'])
      groups.get(w[0]).push(w)
    Object.fromEntries(groups.entries())
    // → { a: ['ant', 'ape'], b: ['bee'] }
  4. 4functools → lang/functools

    partial, reduce and cache keep their Python names, and cache answers to memoize too. A decorator becomes a wrapper: pass the function to cache and keep what comes back.

    compose and once come on top — Python's functools has neither.

    Python

    from functools import cache
    
    @cache
    def fib(n):
        if n < 2:
            return n
        return fib(n - 1) + fib(n - 2)
    
    fib(50)
    # 12586269025

    JavaScript, WizJS

    const { cache } = require(
      '@wizhut_tech/wizjs/lang/functools'
    )
    
    const fib = cache((n) =>
      n < 2 ? n : fib(n - 1) + fib(n - 2))
    
    fib(50)
    // → 12586269025

What's there, and what isn't

Every name on the left is Python's. Where WizJS has it, the right says what it is called and whether it is lazy; where it does not, the row says so.

itertools → lang/itertools
PythonWizJS
count cycle repeat compress dropwhile takewhile filterfalse starmap islice pairwise batched Same names lazyArguments in the same order
chain chain lazyNo from_iterable: spread the lists instead, chain(...lists)
groupby groupby lazyEach group is an array
zip_longest zip_longest lazyfillvalue goes last, in an object: { fillvalue: 0 }
product product lazyNo repeat: pass the iterable once per repeat
zip range enumerate Same names lazyPython builtins, kept with itertools
list(islice(it, n)) take(n, it) eagerReturns an array
accumulate Not in WizJS — arrays.accumulate folds to a single value
tee permutations combinations Not in WizJS — product is the only combinatoric iterator
functools → lang/functools
PythonWizJS
partial reduce Same names partial presets positional arguments only
cache cache Also called memoize. Unbounded, like Python's; fn.cache.clear() empties it
lru_cache cmp_to_key wraps Not in WizJS
collections → lang/collections
PythonWizJS
Counter Counter most_common is mostCommon. No update, subtract or arithmetic; add(item, n) adds to one count
defaultdict DefaultDict d[key] is get(key), which fills a missing key; Python's d.get(key) is peek(key)
deque namedtuple OrderedDict ChainMap Not in WizJS

Where it differs from Python

Most of it behaves exactly as in Python — groupby still wants sorted input, and mostCommon breaks ties in the order items first appeared. These are the places JavaScript changes the answer.

  • Tuples are arrays. pairwise, zip and mostCommon hand back arrays, and zip_longest fills gaps with undefined, where Python uses None.
  • Keys follow Map rules. 1 and '1' are two keys, NaN is one, and objects and arrays match by identity rather than by value — in Counter, DefaultDict and cache alike. A Counter built from a plain object has string keys.
  • get fills a missing key. On a DefaultDict, get(key) is Python's d[key]; for Python's d.get(key, default), use peek(key, default).
  • repeat(x, 0) never stops. A count of zero or less repeats forever, where Python's yields nothing.
  • Stopping early closes a generator. Hand one to take, islice or takewhile and it is finished once they stop short; Python's islice leaves it where it stopped.

One library, organised by what you're trying to do

Pull in everything

The root import still hands back the whole nested object — handy at the REPL, or in scripts where one require is simpler.

const wizjs = require('@wizhut_tech/wizjs')

wizjs.lang.checks.isNil(undefined)         // → true
wizjs.lang.checks.isObject({ a: 1 })       // → true
wizjs.math.numbers.clamp(42, 0, 10)        // → 10

New — import just the area you need

Every namespace area answers to its own subpath, so you load that area and the few it builds on — not the whole library. Needs Node 14.13 or newer.

const { isNil, isEmpty } =
  require('@wizhut_tech/wizjs/lang/checks')
const { clamp } =
  require('@wizhut_tech/wizjs/math/numbers')

// …/<namespace>/<area> — not the whole library
if (isNil(value) || isEmpty(value)) {
  return defaultValue
}

clamp(value, 0, 10)

Functional composition

functools goes past Python's with compose and once.

const { compose } =
  require('@wizhut_tech/wizjs/lang/functools')

// right to left: the last one runs first
const slugify = compose(
  s => s.replace(/\s+/g, '-'),
  s => s.trim(),
  s => s.toLowerCase()
)

slugify('  Hello World  ')   // → 'hello-world'

Object helpers

pick, omit, and a path-safe get that hands back your default instead of throwing.

const { pick, get } =
  require('@wizhut_tech/wizjs/lang/objects')

const user = {
  id: 7, name: 'Ada', password: '…',
  address: { city: 'London' },
}

pick(user, ['id', 'name'])
// → { id: 7, name: 'Ada' }
get(user, 'address.city')
// → 'London'
get(user, 'address.zip', 'n/a')
// → 'n/a'

BBTreeMap: an ordered map, after a Rust B-tree

A Map that keeps its keys sorted, so you can walk them in order, take a range or ask for the first and the last. It follows bbtree, Xiangpeng Hao's Rust B-tree: each key becomes a number that sorts the same way, the prefix a node's keys share is skipped, and wide nodes are searched in a straight line. Then we measured which of its claims still hold in JavaScript.

const { BBTreeMap } =
  require('@wizhut_tech/wizjs/lang/btree')

const prices = new BBTreeMap([
  ['pear', 2], ['apple', 1], ['fig', 4],
])
prices.set('kiwi', 3)

[...prices.keys()]
// → ['apple', 'fig', 'kiwi', 'pear']
[...prices.range('b', 'l')]
// → [['fig', 4], ['kiwi', 3]]
prices.first()
// → ['apple', 1]
Nanoseconds per lookup, one million entries
KeysBBTreeMapClassic B-treesorted-btreeSkip listMap
Random integers2025432833,58279
UUIDs3587737483,120150
Article titles4197827643,175109
JSON keys4307467343,254140
URL paths9111,0029663,139167

Lower is faster. Half hits, half misses, on an Apple M4 Max with Node 26. The classic B-tree has Rust's standard node size, sorted-btree is the most used B-tree on npm, and Map keeps no order.

  • 2.75× faster lookups than a classic B-tree with Rust's standard node size, for integers at ten million entries, and 2.35× for UUIDs: about what the Rust original reports.
  • 14–18× faster than a skip list for integers, and keys inserted in ascending order go in at about 20 ns each, three times the classic tree's speed.
  • Less than half the memory for number keys: 29 bytes an entry at ten million, against the classic tree's 72.
  • What JavaScript cannot carry over. It has no SIMD and no prefetch, so asking for 64 keys at once gains nothing. String keys take more memory than a plain B-tree's, and URL paths, which differ only far past a shared prefix, come out even.
  • No need for order? Use a Map. A hash table is still 2.4–5.5× faster for lookups.

Every claim, measured: the full benchmark

Only the helpers that carry their weight

io.files
Small file helpers — loadFully reads a whole file in one call, no fs boilerplate.
lang.arrays
Practical array operations beyond the standard prototype — chunk, unique, flatten, compact, and accumulate for folding with named operators.
lang.checks
Type predicates: isNil, isEmpty, isString, isObject, isInteger and friends. Stricter than lodash's — arrays and functions are not objects here, and a number is never empty.
lang.flow
Control-flow helpers — if_exception runs a function, then your success callback, or your error callback with the error if either one throws.
lang.singleton
Register a factory under a name, then await getInstance anywhere: the first call builds the instance, and every later call gets the same one — even a call that arrives while it is still being built.
lang.functools
Python's functools in JS — partial, reduce and cache (also called memoize) — plus compose and once, which Python's does not have.
lang.itertools
Python's itertools as 18 lazy generators — chain, zip, range, enumerate, islice, groupby, product, batched, pairwise, and more — plus take, which returns an array. Works on any iterable.
lang.objects
pick, omit, and path-safe get — the three object helpers every codebase ends up writing.
lang.collections
The Python collections JS is missing — Counter with mostCommon and elements, and DefaultDict with a filling factory.
lang.btree
Ordered maps: BBTreeMap for string and number keys, built after a Rust B-tree, and the classic BTreeMap with a comparator for any other key. Walk the keys in order, take a range, ask for the first or the last.
math.numbers
Numeric utilities — clamp, true modulo, and the safe coercions toInteger and toNumber.

Questions Python developers ask about WizJS

Is there an itertools for JavaScript?
JavaScript has no itertools module, but WizJS has one: lang/itertools, with 18 lazy generators under their Python names — count, cycle, chain, islice, groupby, product, pairwise, batched and the rest, with zip, range and enumerate alongside — plus take, which returns an array. Wherever they take an iterable, any will do: arrays, strings, Maps, Sets or other generators.
How do I use Counter and defaultdict in JavaScript?
Require both from lang/collections. new Counter('banana') counts as Python's does; read a count with get, which gives 0 for anything unseen, and rank them with mostCommon(n). defaultdict is DefaultDict, so new DefaultDict(Array) is Python's defaultdict(list): get fills a missing key as square brackets do, and peek reads without filling.
Is there a SortedDict for JavaScript?
Python's SortedDict comes from the sortedcontainers package; in WizJS the nearest thing is BBTreeMap in lang/btree, a map that keeps string or number keys in order. It iterates in key order, range(lo, hi) walks the entries between two keys, and first() and last() stand in for peekitem(0) and peekitem(-1). For keys of any other type, BTreeMap takes a comparator.
Can I use import instead of require?
Yes, in Node. WizJS is CommonJS, and Node lets an ES module import each area's functions by name: import { cache } from '@wizhut_tech/wizjs/lang/functools'. From the package root only the default import works — import wizjs from '@wizhut_tech/wizjs' — so take names from an area rather than the root.
Does WizJS come with TypeScript types?
No. WizJS is plain JavaScript and ships no type declarations, so TypeScript types its functions as any. Under strict, each area you import also needs a one-line declare module of your own before the import compiles.
How is it different from lodash?
It is small on purpose and not trying to become another lodash: Python's itertools, functools and collections under their Python names, plus a handful of everyday helpers — about 13 kB gzipped for the whole library, most of it the ordered maps. Its checks answer differently, too: isObject([]) and isEmpty(1) are false here and true in lodash. It is CommonJS, so don't count on tree-shaking; import one area to load less.
Does it work in the browser?
It is tested on Node only. Nothing outside io/files uses a Node API, but WizJS is CommonJS and no browser or edge runtime is part of its tests — if you bundle it for one, check it there before you rely on it.