@vinicunca/perkakas is a TypeScript utility library. Every helper is written in TypeScript, so types are part of the implementation rather than a later add-on. You can call the same function data-first (data as the first argument) or data-last (to drop into a pipe).
It is a fork of Remeda. The runtime API is the Remeda surface plus a few Perkakas-only helpers. The one rename to know: Remeda's purry is curry here.
Installation
pnpm add @vinicunca/perkakas
npm install @vinicunca/perkakas
yarn add @vinicunca/perkakas
bun add @vinicunca/perkakas
Import the functions you need. Tree-shaking drops the rest.
import { filter, map, pipe } from '@vinicunca/perkakas';
Data-first and data-last
Libraries like Lodash put data first (_.pick(obj, keys)). Ramda puts data last (R.pick(keys, obj)). Perkakas ships both on the same import. Data-first is the usual call. Data-last omits the data argument and returns a function — that is what you pass to pipe.
import { pick, pipe } from '@vinicunca/perkakas';
pick(obj, ['firstName', 'lastName']); // data-first
pipe(obj, pick(['firstName', 'lastName'])); // data-last
Data-first is kinder to autocomplete: your editor already knows the object's keys when you type the second argument.
import { filter, groupBy, pipe } from '@vinicunca/perkakas';
const DATA = [
{ name: 'john', age: 20, gender: 'm' },
{ name: 'marry', age: 22, gender: 'f' },
{ name: 'samara', age: 24, gender: 'f' },
{ name: 'bill', age: 33, gender: 'm' }
];
pipe(
DATA,
filter((x) => x.gender === 'f'),
groupBy((x) => x.age)
);
Pipe
pipe runs functions left to right. You read the transformation in the order it runs, without naming every intermediate value.
import { filter, map, pipe } from '@vinicunca/perkakas';
pipe(
[1, 2, 3, 4],
filter((n) => n % 2 === 0),
map((n) => n * 2)
); // => [4, 8]
Any function that takes one argument works in a pipe, not only Perkakas helpers. Wrap other calls in an arrow if they are not data-last: ($) => validate($, CONFIG).
A headless variant, piped, builds a reusable pipeline without the initial data.
Lazy evaluation
When consecutive lazy helpers sit in a pipe in their data-last form (map, filter, take, unique, and others tagged Lazy on the Functions page), Perkakas walks items one at a time. Later steps can stop early so the rest of the input is never visited.
import { map, pipe, take, unique } from '@vinicunca/perkakas';
pipe(
[1, 2, 2, 3, 3, 4, 5, 6],
map((x) => {
console.log('iterate', x);
return x;
}),
unique(),
take(3)
); // => [1, 2, 3]
// Console:
// iterate 1
// iterate 2
// iterate 2
// iterate 3
To force eager evaluation of a lazy helper, call it data-first inside an arrow: ($) => map($, callback) instead of map(callback).
Type guards
Functions in the Guard category are type predicates. After isString(value) (or filter(isString)), TypeScript narrows the value to string.
import { filter, isString } from '@vinicunca/perkakas';
isString('ok'); // => true
isString(1); // => false
filter(['a', 1, 'b'], isString); // => ['a', 'b']
hasProphasSubObjectisArrayisBigIntisBooleanisDateisDeepEqualisDefinedisEmptyisEmptyishisErrorisFunctionisIncludedInisNonNullisNonNullishisNotisNullishisNumberisObjectTypeisPlainObjectisPromiseisShallowEqualisStrictEqualisStringisSymbolisTruthyTypes that stay strict
Helpers are written so mapped tuples stay tuples, and filters keep the narrowed type. You often do not need extra libraries just to get the type you already expected.
import { map } from '@vinicunca/perkakas';
const DATA = [1, 2, 3] as const;
DATA.map((x) => x.toString());
// => string[]
map(DATA, (x) => x.toString());
// => [string, string, string]