This commit is contained in:
raon0211 2024-04-25 20:56:13 +09:00
commit 7203b56367
279 changed files with 16835 additions and 0 deletions

98
.circleci/config.yml Normal file
View File

@ -0,0 +1,98 @@
version: 2.1
parameters:
pull_request:
type: boolean
default: false
orbs:
slack: circleci/slack@4.5.0
commands:
setup:
steps:
- run:
name: Install yarn
command: |
COREPACK_PATH=$HOME/.local/bin
mkdir -p $COREPACK_PATH
eval "$(echo PATH=$COREPACK_PATH:\$PATH | tee -a $BASH_ENV)"
corepack enable --install-directory $COREPACK_PATH
yarn install
jobs:
lint:
docker:
- image: cimg/node:20.12
steps:
- checkout
- setup
- run:
name: Lint
command: yarn eslint -c .eslintrc.js $(git diff --name-only --diff-filter=ACMRUXB origin/main | grep -E "(.js$|.ts$|.tsx$)")
typecheck:
docker:
- image: cimg/node:20.12
steps:
- checkout
- setup
- run:
name: Typecheck
command: yarn tsc --noEmit
pre-pack:
docker:
- image: cimg/node:20.12
steps:
- checkout
- setup
- run:
name: Build
command: yarn build
check-peer:
docker:
- image: cimg/node:20.12
steps:
- checkout
- setup
- run:
name: Check Peer Dependency
command: ./.scripts/check-peer.sh || (echo "Peer Dependency 오류가 발생했습니다."; exit -1)
test:
docker:
- image: cimg/node:20.12
steps:
- checkout
- setup
- run:
name: vitest
command: yarn vitest --reporter=junit > ./.junit
- store_test_results:
path: ./.junit
- store_artifacts:
path: ./.junit
workflows:
main:
jobs:
- test:
filters:
branches:
ignore: main
- lint:
filters:
branches:
ignore: main
- typecheck:
filters:
branches:
ignore: main
- pre-pack:
filters:
branches:
ignore: main
- check-peer:
filters:
branches:
ignore: main

65
.eslintrc.js Normal file
View File

@ -0,0 +1,65 @@
module.exports = {
root: true,
env: {
es6: true,
node: true,
browser: true,
jest: true,
'shared-node-browser': true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: { jsx: true },
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['@typescript-eslint'],
rules: {
'no-implicit-coercion': 'error',
'no-warning-comments': [
'warn',
{
terms: ['TODO', 'FIXME', 'XXX', 'BUG'],
location: 'anywhere',
},
],
curly: ['error', 'all'],
eqeqeq: ['error', 'always', { null: 'ignore' }],
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-parameter-properties': 'off',
'@typescript-eslint/no-var-requires': 'warn',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'warn',
'@typescript-eslint/no-inferrable-types': 'warn',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/naming-convention': [
'error',
{ format: ['camelCase', 'UPPER_CASE', 'PascalCase'], selector: 'variable', leadingUnderscore: 'allow' },
{ format: ['camelCase', 'PascalCase'], selector: 'function' },
{ format: ['PascalCase'], selector: 'interface' },
{ format: ['PascalCase'], selector: 'typeAlias' },
],
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }],
'@typescript-eslint/member-ordering': [
'error',
{
default: [
'public-static-field',
'private-static-field',
'public-instance-field',
'private-instance-field',
'public-constructor',
'private-constructor',
'public-instance-method',
'private-instance-method',
],
},
],
},
};

38
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@ -0,0 +1,38 @@
Contributor Covenant Code of Conduct
Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
Our Standards
Examples of behavior that contributes to creating a positive environment include:
Using welcoming and inclusive language
Being respectful of differing viewpoints and experiences
Gracefully accepting constructive criticism
Focusing on what is best for the community
Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
The use of sexualized language or imagery and unwelcome sexual attention or advances
Trolling, insulting/derogatory comments, and personal or political attacks
Public or private harassment
Publishing others' private information, such as a physical or electronic address, without explicit permission
Other conduct which could reasonably be considered inappropriate in a professional setting
Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at frontend.devops@toss.im. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at http://contributor-covenant.org/version/1/4

78
.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,78 @@
# Contributing to es-toolkit
We welcome contribution from everyone in the community. All communications in this repository will be in English.
> Every contributor to es-toolkit should adhere to our Code of Conduct. Please read the [full text](./CODE_OF_CONDUCT.md) to understand what actions will and will not be tolerated.
## 1. Our Design Priniciples
Note that we value performance, simplicity of implementation, and detailed documentations. We do not aim for supporting a variety of features and options. Our goal is to provide a small set of performant and well-functioning utilities.
### 1.1 Performance
All functions es-toolkit provides should be more performant than or similar with that of alternative libraries provide.
We measure the performance of our library every time our code is edited. We are using [vitest's benchmark feature](https://vitest.dev/api/#bench). For our benchmark code, please refer to our [benchmark directory](https://github.com/toss/es-toolkit/tree/main/benchmarks).
When a new functionality is added, a benchmark code should be added. Please add screenshots of the benchmarks when opening a pull request for easy reference and history tracking.
### 1.2 Simplicity
We value implementation and interface simplicity over a variety of features for performance, code readability, and easy maintenance. Our functions will not provide complex options to suit every usecase.
In this manner, instead of having complex options of making full use of overloading, etc, to support edge cases, we aim to provide the most simplest interface and implementation for the most common 85% usecases.
### 1.3 Documentation
All of our functions should be documented in detail for easy reference. All functions should have the jsdoc and corresponding documents [in our documentation directory](https://github.com/toss/es-toolkit/tree/main/docs) for all of their features.
We use English as our primary language, but we aim to support Korean documents in our best effort. If you have difficulties writing Korean documents, please let our contributors know so that we can provide the corresponding Korean documents for you.
## 2. Issues
You can contribute to es-toolkit via:
- Improving our [docs](https://es-toolkit.slash.page)
- [Reporting a bug in our issues tab](https://github.com/toss/es-toolkit/issues/new/choose)
- [Requesting a new feature or package](https://github.com/toss/es-toolkit/issues/new/choose)
- [Having a look at our issue list](https://github.com/toss/es-toolkit/issues) to see what's to be fixed
## 3. Pull Requests
> [Opening a pull request](https://github.com/toss/es-toolkit/compare) <br/>
You can raise your own pull request. The title of your pull request should match the following format:
```
<type>[function names]: <description>
```
> We do not care about the number, or style of commits in your history, because we squash merge every PR into main. <br/>
> Feel free to commit in whatever style you feel comfortable with.
### 3.1 Type
**Type must be one of those**
if you changed shipped code :
- feat - for any new functionality additions
- fix - for any fixes that don't add new functionality
if you haven't changed shipped code :
- docs - if you only change documentation
- test - if you only change tests
other :
- chore - anything else
### 3.2 Function Names
The name of function that you made changes. (ex: debounce, throttle)<br/>
If you made changes across multiple packages, writing package scope is optional.
### 3.3 Description
A clear and concise description of what the pr is about.

View File

@ -0,0 +1,21 @@
name: Broken link checker
on:
schedule:
- cron: '0 5 * * 1-5'
workflow_dispatch:
inputs: { url: { description: 'URL to check', required: false, default: 'https://es-toolkit.slash.page' } }
jobs:
broken-link-checker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: corepack enable
- uses: actions/setup-node@v4
with:
cache: "yarn"
cache-dependency-path: "yarn.lock"
node-version-file: ".nvmrc"
- run: yarn install
- run: yarn blc ${{ github.event.inputs.url }} --ro

68
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,68 @@
name: Release
on:
push:
branches:
- "*"
tags:
- "v*.*.*"
jobs:
release:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
deployments: write
steps:
- name: Checkout
uses: actions/checkout@v4
- run: corepack enable
- uses: actions/setup-node@v4
with:
cache: "yarn"
cache-dependency-path: "yarn.lock"
node-version-file: ".nvmrc"
- if: github.ref_type == 'branch'
run: |
jq \
--arg build "$GITHUB_RUN_NUMBER" \
--arg commit "${GITHUB_SHA::8}" \
'.version = .version + "-dev." + $build + "+" + $commit' \
package.json > package.json.tmp
mv package.json.tmp package.json
- run: yarn install
- run: 'mkdir -p out && yarn pack --out out/%s-%v.tgz'
- id: extract-changelog
uses: dahlia/submark@5a5ff0a58382fb812616a5801402f5aef00f90ce
with:
input-file: CHANGELOG.md
heading-level: 2
heading-title-text: version ${{ github.ref_name }}
ignore-case: true
omit-heading: true
- run: "cat ${{ steps.extract-changelog.outputs.output-file }}"
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
body_path: ${{ steps.extract-changelog.outputs.output-file }}
repository: toss/es-toolkit
generate_release_notes: false
token: ${{ secrets.GITHUB_TOKEN }}
files: out/*.tgz
- if: |
github.event_name == 'push' &&
(github.ref_type == 'tag' || github.ref == 'refs/heads/main')
run: |
set -ex
npm config set //registry.npmjs.org/:_authToken "$NPM_AUTH_TOKEN"
npm whoami
if [[ "$GITHUB_REF_TYPE" = "tag" ]]; then
npm publish --access public *.tgz
else
npm publish --access public --tag dev *.tgz
fi
env:
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
working-directory: ${{ github.workspace }}/out/

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
.DS_Store
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.pnp.cjs
.pnp.loader.mjs
node_modules
coverage
dist
esm
.junit
out

1
.npmignore Normal file
View File

@ -0,0 +1 @@
.nvmrc

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
20

8
.scripts/check-peer.sh Executable file
View File

@ -0,0 +1,8 @@
OUT=$(yarn | grep -E "(YN0002|YN0059|YN0060)" | grep -E $1)
if [ -z "$OUT" ]; then
echo "No Peer Dependency Errors Found."
else
echo "$OUT"
echo "Some peer dependencies are incorrectly met; run yarn explain peer-requirements <hash> for details, where <hash> is the six-letter p-prefixed code"
exit 1
fi

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"arcanis.vscode-zipfs",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

10
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"search.exclude": {
"**/.yarn": true,
"**/.pnp.*": true
},
"eslint.nodePath": ".yarn/sdks",
"prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
"typescript.tsdk": ".yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

20
.yarn/sdks/eslint/bin/eslint.js vendored Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require eslint/bin/eslint.js
require(absPnpApiPath).setup();
}
}
// Defer to the real eslint/bin/eslint.js your application uses
module.exports = absRequire(`eslint/bin/eslint.js`);

20
.yarn/sdks/eslint/lib/api.js vendored Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require eslint
require(absPnpApiPath).setup();
}
}
// Defer to the real eslint your application uses
module.exports = absRequire(`eslint`);

View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require eslint/use-at-your-own-risk
require(absPnpApiPath).setup();
}
}
// Defer to the real eslint/use-at-your-own-risk your application uses
module.exports = absRequire(`eslint/use-at-your-own-risk`);

14
.yarn/sdks/eslint/package.json vendored Normal file
View File

@ -0,0 +1,14 @@
{
"name": "eslint",
"version": "8.57.0-sdk",
"main": "./lib/api.js",
"type": "commonjs",
"bin": {
"eslint": "./bin/eslint.js"
},
"exports": {
"./package.json": "./package.json",
".": "./lib/api.js",
"./use-at-your-own-risk": "./lib/unsupported-api.js"
}
}

5
.yarn/sdks/integrations.yml vendored Normal file
View File

@ -0,0 +1,5 @@
# This file is automatically generated by @yarnpkg/sdks.
# Manual changes might be lost!
integrations:
- vscode

20
.yarn/sdks/prettier/bin/prettier.cjs vendored Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require prettier/bin/prettier.cjs
require(absPnpApiPath).setup();
}
}
// Defer to the real prettier/bin/prettier.cjs your application uses
module.exports = absRequire(`prettier/bin/prettier.cjs`);

20
.yarn/sdks/prettier/index.cjs vendored Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require prettier
require(absPnpApiPath).setup();
}
}
// Defer to the real prettier your application uses
module.exports = absRequire(`prettier`);

7
.yarn/sdks/prettier/package.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"name": "prettier",
"version": "3.2.5-sdk",
"main": "./index.cjs",
"type": "commonjs",
"bin": "./bin/prettier.cjs"
}

20
.yarn/sdks/typescript/bin/tsc vendored Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/bin/tsc
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/bin/tsc your application uses
module.exports = absRequire(`typescript/bin/tsc`);

20
.yarn/sdks/typescript/bin/tsserver vendored Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/bin/tsserver
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/bin/tsserver your application uses
module.exports = absRequire(`typescript/bin/tsserver`);

20
.yarn/sdks/typescript/lib/tsc.js vendored Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsc.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsc.js your application uses
module.exports = absRequire(`typescript/lib/tsc.js`);

225
.yarn/sdks/typescript/lib/tsserver.js vendored Normal file
View File

@ -0,0 +1,225 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
const moduleWrapper = tsserver => {
if (!process.versions.pnp) {
return tsserver;
}
const {isAbsolute} = require(`path`);
const pnpApi = require(`pnpapi`);
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
const isPortal = str => str.startsWith("portal:/");
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
}));
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol
// before forwarding it to TS, and to add it back on all returned paths.
function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
// We also take the opportunity to turn virtual paths into physical ones;
// this makes it much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
// file instances instead of the real ones.
//
// We only do this to modules owned by the the dependency tree roots.
// This avoids breaking the resolution when jumping inside a vendor
// with peer dep (otherwise jumping into react-dom would show resolution
// errors on react).
//
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
if (resolved) {
const locator = pnpApi.findPackageLocator(resolved);
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
str = resolved;
}
}
str = normalize(str);
if (str.match(/\.zip\//)) {
switch (hostInfo) {
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
// VSCode only adds it automatically for supported schemes,
// so we have to do it manually for the `zip` scheme.
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
//
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
//
// 2021-10-08: VSCode changed the format in 1.61.
// Before | ^zip:/c:/foo/bar.zip/package.json
// After | ^/zip//c:/foo/bar.zip/package.json
//
// 2022-04-06: VSCode changed the format in 1.66.
// Before | ^/zip//c:/foo/bar.zip/package.json
// After | ^/zip/c:/foo/bar.zip/package.json
//
// 2022-05-06: VSCode changed the format in 1.68
// Before | ^/zip/c:/foo/bar.zip/package.json
// After | ^/zip//c:/foo/bar.zip/package.json
//
case `vscode <1.61`: {
str = `^zip:${str}`;
} break;
case `vscode <1.66`: {
str = `^/zip/${str}`;
} break;
case `vscode <1.68`: {
str = `^/zip${str}`;
} break;
case `vscode`: {
str = `^/zip/${str}`;
} break;
// To make "go to definition" work,
// We have to resolve the actual file system path from virtual path
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
case `coc-nvim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = resolve(`zipfile:${str}`);
} break;
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
// We have to resolve the actual file system path from virtual path,
// everything else is up to neovim
case `neovim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = `zipfile://${str}`;
} break;
default: {
str = `zip:${str}`;
} break;
}
} else {
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
}
}
return str;
}
function fromEditorPath(str) {
switch (hostInfo) {
case `coc-nvim`: {
str = str.replace(/\.zip::/, `.zip/`);
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
// So in order to convert it back, we use .* to match all the thing
// before `zipfile:`
return process.platform === `win32`
? str.replace(/^.*zipfile:\//, ``)
: str.replace(/^.*zipfile:/, ``);
} break;
case `neovim`: {
str = str.replace(/\.zip::/, `.zip/`);
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
return str.replace(/^zipfile:\/\//, ``);
} break;
case `vscode`:
default: {
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
} break;
}
}
// Force enable 'allowLocalPluginLoads'
// TypeScript tries to resolve plugins using a path relative to itself
// which doesn't work when using the global cache
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
// TypeScript already does local loads and if this code is running the user trusts the workspace
// https://github.com/microsoft/vscode/issues/45856
const ConfiguredProject = tsserver.server.ConfiguredProject;
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
this.projectService.allowLocalPluginLoads = true;
return originalEnablePluginsWithOptions.apply(this, arguments);
};
// And here is the point where we hijack the VSCode <-> TS communications
// by adding ourselves in the middle. We locate everything that looks
// like an absolute path of ours and normalize it.
const Session = tsserver.server.Session;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
let hostInfo = `unknown`;
Object.assign(Session.prototype, {
onMessage(/** @type {string | object} */ message) {
const isStringMessage = typeof message === 'string';
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
if (
parsedMessage != null &&
typeof parsedMessage === `object` &&
parsedMessage.arguments &&
typeof parsedMessage.arguments.hostInfo === `string`
) {
hostInfo = parsedMessage.arguments.hostInfo;
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
// The RegExp from https://semver.org/ but without the caret at the start
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
) ?? []).map(Number)
if (major === 1) {
if (minor < 61) {
hostInfo += ` <1.61`;
} else if (minor < 66) {
hostInfo += ` <1.66`;
} else if (minor < 68) {
hostInfo += ` <1.68`;
}
}
}
}
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
return typeof value === 'string' ? fromEditorPath(value) : value;
});
return originalOnMessage.call(
this,
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
);
},
send(/** @type {any} */ msg) {
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})));
}
});
return tsserver;
};
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsserver.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsserver.js your application uses
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));

View File

@ -0,0 +1,225 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
const moduleWrapper = tsserver => {
if (!process.versions.pnp) {
return tsserver;
}
const {isAbsolute} = require(`path`);
const pnpApi = require(`pnpapi`);
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
const isPortal = str => str.startsWith("portal:/");
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
}));
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol
// before forwarding it to TS, and to add it back on all returned paths.
function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
// We also take the opportunity to turn virtual paths into physical ones;
// this makes it much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
// file instances instead of the real ones.
//
// We only do this to modules owned by the the dependency tree roots.
// This avoids breaking the resolution when jumping inside a vendor
// with peer dep (otherwise jumping into react-dom would show resolution
// errors on react).
//
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
if (resolved) {
const locator = pnpApi.findPackageLocator(resolved);
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
str = resolved;
}
}
str = normalize(str);
if (str.match(/\.zip\//)) {
switch (hostInfo) {
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
// VSCode only adds it automatically for supported schemes,
// so we have to do it manually for the `zip` scheme.
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
//
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
//
// 2021-10-08: VSCode changed the format in 1.61.
// Before | ^zip:/c:/foo/bar.zip/package.json
// After | ^/zip//c:/foo/bar.zip/package.json
//
// 2022-04-06: VSCode changed the format in 1.66.
// Before | ^/zip//c:/foo/bar.zip/package.json
// After | ^/zip/c:/foo/bar.zip/package.json
//
// 2022-05-06: VSCode changed the format in 1.68
// Before | ^/zip/c:/foo/bar.zip/package.json
// After | ^/zip//c:/foo/bar.zip/package.json
//
case `vscode <1.61`: {
str = `^zip:${str}`;
} break;
case `vscode <1.66`: {
str = `^/zip/${str}`;
} break;
case `vscode <1.68`: {
str = `^/zip${str}`;
} break;
case `vscode`: {
str = `^/zip/${str}`;
} break;
// To make "go to definition" work,
// We have to resolve the actual file system path from virtual path
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
case `coc-nvim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = resolve(`zipfile:${str}`);
} break;
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
// We have to resolve the actual file system path from virtual path,
// everything else is up to neovim
case `neovim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = `zipfile://${str}`;
} break;
default: {
str = `zip:${str}`;
} break;
}
} else {
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
}
}
return str;
}
function fromEditorPath(str) {
switch (hostInfo) {
case `coc-nvim`: {
str = str.replace(/\.zip::/, `.zip/`);
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
// So in order to convert it back, we use .* to match all the thing
// before `zipfile:`
return process.platform === `win32`
? str.replace(/^.*zipfile:\//, ``)
: str.replace(/^.*zipfile:/, ``);
} break;
case `neovim`: {
str = str.replace(/\.zip::/, `.zip/`);
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
return str.replace(/^zipfile:\/\//, ``);
} break;
case `vscode`:
default: {
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
} break;
}
}
// Force enable 'allowLocalPluginLoads'
// TypeScript tries to resolve plugins using a path relative to itself
// which doesn't work when using the global cache
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
// TypeScript already does local loads and if this code is running the user trusts the workspace
// https://github.com/microsoft/vscode/issues/45856
const ConfiguredProject = tsserver.server.ConfiguredProject;
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
this.projectService.allowLocalPluginLoads = true;
return originalEnablePluginsWithOptions.apply(this, arguments);
};
// And here is the point where we hijack the VSCode <-> TS communications
// by adding ourselves in the middle. We locate everything that looks
// like an absolute path of ours and normalize it.
const Session = tsserver.server.Session;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
let hostInfo = `unknown`;
Object.assign(Session.prototype, {
onMessage(/** @type {string | object} */ message) {
const isStringMessage = typeof message === 'string';
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
if (
parsedMessage != null &&
typeof parsedMessage === `object` &&
parsedMessage.arguments &&
typeof parsedMessage.arguments.hostInfo === `string`
) {
hostInfo = parsedMessage.arguments.hostInfo;
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
// The RegExp from https://semver.org/ but without the caret at the start
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
) ?? []).map(Number)
if (major === 1) {
if (minor < 61) {
hostInfo += ` <1.61`;
} else if (minor < 66) {
hostInfo += ` <1.66`;
} else if (minor < 68) {
hostInfo += ` <1.68`;
}
}
}
}
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
return typeof value === 'string' ? fromEditorPath(value) : value;
});
return originalOnMessage.call(
this,
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
);
},
send(/** @type {any} */ msg) {
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})));
}
});
return tsserver;
};
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsserverlibrary.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsserverlibrary.js your application uses
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`));

20
.yarn/sdks/typescript/lib/typescript.js vendored Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript your application uses
module.exports = absRequire(`typescript`);

10
.yarn/sdks/typescript/package.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"name": "typescript",
"version": "5.4.5-sdk",
"main": "./lib/typescript.js",
"type": "commonjs",
"bin": {
"tsc": "./bin/tsc",
"tsserver": "./bin/tsserver"
}
}

5
CHANGELOG.md Normal file
View File

@ -0,0 +1,5 @@
# es-toolkit Changelog
## Version v1.0.0
Initial release. Released on May 31th, 2024.

31
README-ko_kr.md Normal file
View File

@ -0,0 +1,31 @@
![](./docs/public/og.png)
# es-toolkit &middot; [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/toss/slash/blob/main/LICENSE)
[English](./README.md) | 한국어
es-toolkit은 높은 성능과 작은 번들 사이즈, 강력한 타입을 자랑하는 현대적인 JavaScript 유틸리티 라이브러리예요.
- es-toolkit은 매일 사용하는 다양한 유틸리티 함수를 현대적인 구현으로 제공해요. 예를 들어, [debounce](https://es-toolkit.slash.page/reference/function/debounce.html), [delay](https://es-toolkit.slash.page/reference/promise/delay.html), [chunk](https://es-toolkit.slash.page/reference/array/chunk.html), [sum](https://es-toolkit.slash.page/reference/math/sum.html), [pick](https://es-toolkit.slash.page/reference/object/pick.html) 같은 함수들이 있어요.
- 설계할 때 퍼포먼스를 중요 원칙으로 두어, es-toolkit은 현대적인 JavaScript 환경에서 [2-3배 빠른 성능](https://es-toolkit.slash.page/ko/performance.html)을 보여요.
- es-toolkit은 트리 셰이킹을 기본으로 지원하며, 함수에 따라서는 다른 라이브러리와 비교했을 때 [최대 97% 작은 JavaScript 번들 사이즈](https://es-toolkit.slash.page/ko/bundle-size.html)를 제공해요.
- es-toolkit은 TypeScript 타입이 내장되어 있고, 직관적이고 정확한 타입을 추구해요. [isNotNil](https://es-toolkit.slash.page/ko/reference/predicate/isNotNil.html) 같은 사용하기 편리한 유틸리티 함수도 제공해요.
- es-toolkit은 100% 테스트 커버리지를 유지하면서, 높은 안정성을 자랑해요.
## 기여하기
커뮤니티에 있는 모든 분들에게 기여를 환영해요. 아래에 작성되어 있는 기여 가이드를 확인하세요.
[CONTRIBUTING](./.github/CONTRIBUTING.md)
## 라이선스
MIT © Viva Republica, Inc. 자세한 내용은 [LICENSE](./LICENSE)를 참고하세요.
<a title="Toss" href="https://toss.im">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://static.toss.im/logos/png/4x/logo-toss-reverse.png">
<img alt="Toss" src="https://static.toss.im/logos/png/4x/logo-toss.png" width="100">
</picture>
</a>

31
README.md Normal file
View File

@ -0,0 +1,31 @@
![](./docs/public/og.png)
# es-toolkit &middot; [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/toss/slash/blob/main/LICENSE)
English | [한국어](./README-ko_kr.md)
es-toolkit is a state-of-the-art, high-performance JavaScript utility library with a small bundle size and strong type annotations.
- es-toolkit offers a variety of everyday utility functions with modern implementations, such as [debounce](https://es-toolkit.slash.page/reference/function/debounce.html), [delay](https://es-toolkit.slash.page/reference/promise/delay.html), [chunk](https://es-toolkit.slash.page/reference/array/chunk.html), [sum](https://es-toolkit.slash.page/reference/math/sum.html), and [pick](https://es-toolkit.slash.page/reference/object/pick.html).
- Designed with performance in mind, es-toolkit achieves [2-3× better performance](https://es-toolkit.slash.page/performance.html) in modern JavaScript environments.
- es-toolkit supports tree shaking out of the box, and [reduces JavaScript code by up to 97%](https://es-toolkit.slash.page/bundle-size.html) compared to other libraries.
- es-toolkit includes built-in TypeScript support, with straightforward yet robust types. It also provides useful type guards such as [isNotNil](https://es-toolkit.slash.page/reference/predicate/isNotNil.html).
- es-toolkit is battle-tested with 100% test coverage, ensuring reliability and robustness.
## Contributing
We welcome contribution from everyone in the community. Read below for detailed contribution guide.
[CONTRIBUTING](./.github/CONTRIBUTING.md)
## License
MIT © Viva Republica, Inc. See [LICENSE](./LICENSE) for details.
<a title="Toss" href="https://toss.im">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://static.toss.im/logos/png/4x/logo-toss-reverse.png">
<img alt="Toss" src="https://static.toss.im/logos/png/4x/logo-toss.png" width="100">
</picture>
</a>

6
babel.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
presets: [
require.resolve('@babel/preset-env'),
require.resolve('@babel/preset-typescript'),
],
};

13
benchmarks/chunk.bench.ts Normal file
View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { chunk as chunkToolkit } from 'es-toolkit';
import { chunk as chunkLodash } from 'lodash';
describe('chunk', () => {
bench('es-toolkit', () => {
chunkToolkit([1, 2, 3, 4, 5, 6], 3);
})
bench('lodash', () => {
chunkLodash([1, 2, 3, 4, 5, 6], 3);
})
});

15
benchmarks/clamp.bench.ts Normal file
View File

@ -0,0 +1,15 @@
import { bench, describe } from 'vitest';
import { clamp as clampToolkit } from 'es-toolkit';
import { clamp as clampLodash } from 'lodash';
describe('clamp', () => {
bench('es-toolkit', () => {
clampToolkit(10, 5, 15)
clampToolkit(10, 5)
})
bench('lodash', () => {
clampLodash(10, 5, 15)
clampLodash(10, 5)
})
});

View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { difference as differenceToolkit } from 'es-toolkit';
import { difference as differenceLodash } from 'lodash';
describe('difference', () => {
bench('es-toolkit', () => {
differenceToolkit([1, 2, 3], [2]);
})
bench('lodash', () => {
differenceLodash([1, 2, 3], [2]);
})
});

View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { differenceBy as differenceByToolkit } from 'es-toolkit';
import { differenceBy as differenceByLodash } from 'lodash';
describe('differenceBy', () => {
bench('es-toolkit', () => {
differenceByToolkit([1.2, 2.3, 3.4], [1.2], Math.floor);
})
bench('lodash', () => {
differenceByLodash([1.2, 2.3, 3.4], [1.2], Math.floor);
})
});

View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { differenceWith as differenceWithToolkit } from 'es-toolkit';
import { differenceWith as differenceWithLodash } from 'lodash';
describe('differenceWith', () => {
bench('es-toolkit', () => {
differenceWithToolkit([1.2, 2.3, 3.4], [1.2], (x, y) => Math.floor(x) === Math.floor(y));
})
bench('lodash', () => {
differenceWithLodash([1.2, 2.3, 3.4], [1.2], (x, y) => Math.floor(x) === Math.floor(y));
})
});

13
benchmarks/drop.bench.ts Normal file
View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { drop as dropToolkit } from 'es-toolkit';
import { drop as dropLodash } from 'lodash';
describe('drop', () => {
bench('es-toolkit', () => {
dropToolkit([1, 2, 3, 4, 5, 6], 3);
})
bench('lodash', () => {
dropLodash([1, 2, 3, 4, 5, 6], 3);
})
});

View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { dropRight as dropRightToolkit } from 'es-toolkit';
import { dropRight as dropRightLodash } from 'lodash';
describe('dropRight', () => {
bench('es-toolkit', () => {
dropRightToolkit([1, 2, 3, 4, 5, 6], 3);
})
bench('lodash', () => {
dropRightLodash([1, 2, 3, 4, 5, 6], 3);
})
});

View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { dropRightWhile as dropRightWhileToolkit } from 'es-toolkit';
import { dropRightWhile as dropRightWhileLodash } from 'lodash';
describe('dropRightWhile', () => {
bench('es-toolkit', () => {
dropRightWhileToolkit([1.2, 2.3, 3.4], x => x < 2);
})
bench('lodash', () => {
dropRightWhileLodash([1.2, 2.3, 3.4], x => x < 2);
})
});

View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { dropWhile as dropWhileToolkit } from 'es-toolkit';
import { dropWhile as dropWhileLodash } from 'lodash';
describe('dropWhile', () => {
bench('es-toolkit', () => {
dropWhileToolkit([1.2, 2.3, 3.4], x => x < 2);
})
bench('lodash', () => {
dropWhileLodash([1.2, 2.3, 3.4], x => x < 2);
})
});

View File

@ -0,0 +1,29 @@
import { bench, describe } from 'vitest';
import { groupBy as groupByToolkit } from 'es-toolkit';
import { groupBy as groupByLodash } from 'lodash';
describe('groupBy', () => {
bench('es-toolkit', () => {
const array = [
{ category: 'fruit', name: 'apple' },
{ category: 'fruit', name: 'banana' },
{ category: 'vegetable', name: 'carrot' },
{ category: 'fruit', name: 'pear' },
{ category: 'vegetable', name: 'broccoli' }
];
groupByToolkit(array, item => item.category);
})
bench('lodash', () => {
const array = [
{ category: 'fruit', name: 'apple' },
{ category: 'fruit', name: 'banana' },
{ category: 'vegetable', name: 'carrot' },
{ category: 'fruit', name: 'pear' },
{ category: 'vegetable', name: 'broccoli' }
];
groupByLodash(array, item => item.category);
})
});

View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { intersection as intersectionToolkit } from 'es-toolkit';
import { intersection as intersectionLodash } from 'lodash';
describe('intersection', () => {
bench('es-toolkit', () => {
intersectionToolkit([1, 2, 3], [2, 4]);
})
bench('lodash', () => {
intersectionLodash([1, 2, 3], [2, 4]);
})
});

View File

@ -0,0 +1,19 @@
import { bench, describe } from 'vitest';
import { intersectionBy as intersectionByToolkit } from 'es-toolkit';
import { intersectionBy as intersectionByLodash } from 'lodash';
describe('intersectionBy', () => {
bench('es-toolkit', () => {
const array1 = [{ id: 1 }, { id: 2 }, { id: 3 }];
const array2 = [{ id: 2 }, { id: 4 }];
const mapper = item => item.id;
intersectionByToolkit(array1, array2, mapper);
})
bench('lodash', () => {
const array1 = [{ id: 1 }, { id: 2 }, { id: 3 }];
const array2 = [{ id: 2 }, { id: 4 }];
const mapper = item => item.id;
intersectionByLodash(array1, array2, mapper);
})
});

View File

@ -0,0 +1,19 @@
import { bench, describe } from 'vitest';
import { intersectionWith as intersectionWithToolkit } from 'es-toolkit';
import { intersectionWith as intersectionWithLodash } from 'lodash';
describe('intersectionWith', () => {
bench('es-toolkit', () => {
const array1 = [{ id: 1 }, { id: 2 }, { id: 3 }];
const array2 = [{ id: 2 }, { id: 4 }];
const areItemsEqual = (a, b) => a.id === b.id;
intersectionWithToolkit(array1, array2, areItemsEqual);
})
bench('lodash', () => {
const array1 = [{ id: 1 }, { id: 2 }, { id: 3 }];
const array2 = [{ id: 2 }, { id: 4 }];
const areItemsEqual = (a, b) => a.id === b.id;
intersectionWithLodash(array1, array2, areItemsEqual);
})
});

19
benchmarks/isNil.bench.ts Normal file
View File

@ -0,0 +1,19 @@
import { bench, describe } from 'vitest';
import { isNil as isNilToolkit } from 'es-toolkit';
import { isNil as isNilLodash } from 'lodash';
describe('isNil', () => {
bench('es-toolkit', () => {
isNilToolkit(null);
isNilToolkit(undefined);
isNilToolkit(123);
isNilToolkit([1, 2, 3]);
})
bench('lodash', () => {
isNilLodash(null);
isNilLodash(undefined);
isNilLodash(123);
isNilLodash([1, 2, 3]);
})
});

13
benchmarks/omit.bench.ts Normal file
View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { omit as omitToolkit } from 'es-toolkit';
import { omit as omitLodash } from 'lodash';
describe('omit', () => {
bench('es-toolkit', () => {
omitToolkit({ foo: 1, bar: 2, baz: 3 }, ['foo', 'bar'])
})
bench('lodash', () => {
omitLodash({ foo: 1, bar: 2, baz: 3 }, ['foo', 'bar'])
})
});

View File

@ -0,0 +1,17 @@
import { bench, describe } from 'vitest';
import { omitBy as omitByToolkit } from 'es-toolkit';
import { omitBy as omitByLodash } from 'lodash';
describe('omitBy', () => {
bench('es-toolkit', () => {
const obj = { a: 1, b: 'omit', c: 3 };
const shouldOmit = (value: number | string, key: string) => typeof value === 'string';
omitByToolkit(obj, shouldOmit);
})
bench('lodash', () => {
const obj = { a: 1, b: 'omit', c: 3 };
const shouldOmit = (value: number | string, key: string) => typeof value === 'string';
omitByLodash(obj, shouldOmit);
})
});

View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { partition as partitionToolkit } from 'es-toolkit';
import { partition as partitionLodash } from 'lodash';
describe('partition', () => {
bench('es-toolkit', () => {
partitionToolkit([1, 2, 3, 4, 5], x => x < 3);
})
bench('lodash', () => {
partitionLodash([1, 2, 3], x => x < 3);
})
});

13
benchmarks/pick.bench.ts Normal file
View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { pick as pickToolkit } from 'es-toolkit';
import { pick as pickLodash } from 'lodash';
describe('pick', () => {
bench('es-toolkit', () => {
pickToolkit({ foo: 1, bar: 2, baz: 3 }, ['foo', 'bar'])
})
bench('lodash', () => {
pickLodash({ foo: 1, bar: 2, baz: 3 }, ['foo', 'bar'])
})
});

13
benchmarks/round.bench.ts Normal file
View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { round as roundToolkit } from 'es-toolkit';
import { round as roundLodash } from 'lodash';
describe('round', () => {
bench('es-toolkit', () => {
roundToolkit(1.2345, 2);
})
bench('lodash', () => {
roundLodash(1.2345, 2);
})
});

View File

@ -0,0 +1,15 @@
import { bench, describe } from 'vitest';
import { sample as sampleToolkit } from 'es-toolkit';
import { sample as sampleLodash } from 'lodash';
describe('sample', () => {
bench('es-toolkit', () => {
const array = [1, 2, 3, 4, 5];
sampleToolkit(array);
})
bench('lodash', () => {
const array = [1, 2, 3, 4, 5];
sampleLodash(array);
})
});

View File

@ -0,0 +1,15 @@
import { bench, describe } from 'vitest';
import { shuffle as shuffleToolkit } from 'es-toolkit';
import { shuffle as shuffleLodash } from 'lodash';
describe('shuffle', () => {
bench('es-toolkit', () => {
const array = [1, 2, 3, 4, 5];
shuffleToolkit(array);
})
bench('lodash', () => {
const array = [1, 2, 3, 4, 5];
shuffleLodash(array);
})
});

13
benchmarks/sum.bench.ts Normal file
View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { sum as sumToolkit } from 'es-toolkit';
import { sum as sumLodash } from 'lodash';
describe('sum', () => {
bench('es-toolkit', () => {
sumToolkit([1, 2, 3]);
})
bench('lodash', () => {
sumLodash([1, 2, 3]);
})
});

13
benchmarks/take.bench.ts Normal file
View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { take as takeToolkit } from 'es-toolkit';
import { take as takeLodash } from 'lodash';
describe('take', () => {
bench('es-toolkit', () => {
takeToolkit([1, 2, 3, 4], 2);
})
bench('lodash', () => {
takeLodash([1, 2, 3, 4], 2);
})
});

View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { takeRight as takeRightToolkit } from 'es-toolkit';
import { takeRight as takeRightLodash } from 'lodash';
describe('takeRight', () => {
bench('es-toolkit', () => {
takeRightToolkit([1, 2, 3, 4], 2);
})
bench('lodash', () => {
takeRightLodash([1, 2, 3, 4], 2);
})
});

View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { takeRightWhile as takeRightWhileToolkit } from 'es-toolkit';
import { takeRightWhile as takeRightWhileLodash } from 'lodash';
describe('takeRightWhile', () => {
bench('es-toolkit', () => {
takeRightWhileToolkit([5, 4, 3, 2, 1], n => n < 4);
})
bench('lodash', () => {
takeRightWhileLodash([5, 4, 3, 2, 1], n => n < 4);
})
});

View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { takeWhile as takeWhileToolkit } from 'es-toolkit';
import { takeWhile as takeWhileLodash } from 'lodash';
describe('takeWhile', () => {
bench('es-toolkit', () => {
takeWhileToolkit([5, 4, 3, 2, 1], n => n < 4);
})
bench('lodash', () => {
takeWhileLodash([5, 4, 3, 2, 1], n => n < 4);
})
});

17
benchmarks/union.bench.ts Normal file
View File

@ -0,0 +1,17 @@
import { bench, describe } from 'vitest';
import { union as unionToolkit } from 'es-toolkit';
import { union as unionLodash } from 'lodash';
describe('union', () => {
bench('es-toolkit', () => {
const array1 = [1, 2, 3];
const array2 = [3, 4, 5];
const result = unionToolkit(array1, array2);
})
bench('lodash', () => {
const array1 = [1, 2, 3];
const array2 = [3, 4, 5];
const result = unionLodash(array1, array2);
})
});

View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { unionBy as unionByToolkit } from 'es-toolkit';
import { unionBy as unionByLodash } from 'lodash';
describe('unionBy', () => {
bench('es-toolkit', () => {
unionByToolkit([{ id: 1 }, { id: 2 }], [{ id: 2 }, { id: 3 }], x => x.id);
})
bench('lodash', () => {
unionByLodash([{ id: 1 }, { id: 2 }], [{ id: 2 }, { id: 3 }], x => x.id);
})
});

View File

@ -0,0 +1,19 @@
import { bench, describe } from 'vitest';
import { unionWith as unionWithToolkit } from 'es-toolkit';
import { unionWith as unionWithLodash } from 'lodash';
describe('unionWith', () => {
bench('es-toolkit', () => {
const array1 = [{ id: 1 }, { id: 2 }];
const array2 = [{ id: 2 }, { id: 3 }];
const areItemsEqual = (a, b) => a.id === b.id;
unionWithToolkit(array1, array2, areItemsEqual);
})
bench('lodash', () => {
const array1 = [{ id: 1 }, { id: 2 }];
const array2 = [{ id: 2 }, { id: 3 }];
const areItemsEqual = (a, b) => a.id === b.id;
unionWithLodash(array1, array2, areItemsEqual);
})
});

13
benchmarks/uniq.bench.ts Normal file
View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { uniq as uniqToolkit } from 'es-toolkit';
import { uniq as uniqLodash } from 'lodash';
describe('uniq', () => {
bench('es-toolkit', () => {
uniqToolkit([11, 2, 3, 44, 11, 2, 3]);
})
bench('lodash', () => {
uniqLodash([11, 2, 3, 44, 11, 2, 3]);
})
});

13
benchmarks/xor.bench.ts Normal file
View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { xor as xorToolkit } from 'es-toolkit';
import { xor as xorLodash } from 'lodash';
describe('xor', () => {
bench('es-toolkit', () => {
xorToolkit([1, 2, 3, 4], [3, 4, 5, 6]);
})
bench('lodash', () => {
xorLodash([1, 2, 3, 4], [3, 4, 5, 6]);
})
});

15
benchmarks/xorBy.bench.ts Normal file
View File

@ -0,0 +1,15 @@
import { bench, describe } from 'vitest';
import { xorBy as xorByToolkit } from 'es-toolkit';
import { xorBy as xorByLodash } from 'lodash';
describe('xorBy', () => {
bench('es-toolkit', () => {
const idMapper = obj => obj.id;
xorByToolkit([{ id: 1 }, { id: 2 }], [{ id: 2 }, { id: 3 }], idMapper);
})
bench('lodash', () => {
const idMapper = obj => obj.id;
xorByLodash([{ id: 1 }, { id: 2 }], [{ id: 2 }, { id: 3 }], idMapper);
})
});

View File

@ -0,0 +1,14 @@
import { bench, describe } from 'vitest';
import { xorWith as xorWithToolkit } from 'es-toolkit';
import { xorWith as xorWithLodash } from 'lodash';
describe('xorWith', () => {
bench('es-toolkit', () => {
xorWithToolkit([{ id: 1 }, { id: 2 }], [{ id: 2 }, { id: 3 }], (x, y) => x.id === y.id);
})
bench('lodash', () => {
const idMapper = obj => obj.id;
xorWithLodash([{ id: 1 }, { id: 2 }], [{ id: 2 }, { id: 3 }], (x, y) => x.id === y.id);
})
});

13
benchmarks/zip.bench.ts Normal file
View File

@ -0,0 +1,13 @@
import { bench, describe } from 'vitest';
import { zip as zipToolkit } from 'es-toolkit';
import { zip as zipLodash } from 'lodash';
describe('zip', () => {
bench('es-toolkit', () => {
zipToolkit([1, 2, 3, 4], [3, 4, 5, 6]);
})
bench('lodash', () => {
zipLodash([1, 2, 3, 4], [3, 4, 5, 6]);
})
});

View File

@ -0,0 +1,19 @@
import { bench, describe } from 'vitest';
import { zipWith as zipWithToolkit } from 'es-toolkit';
import { zipWith as zipWithLodash } from 'lodash';
describe('zipWith', () => {
bench('es-toolkit', () => {
const arr1 = [1, 2];
const arr2 = [3, 4];
const arr3 = [5, 6];
zipWithToolkit(arr1, arr2, arr3, (a, b, c) => `${a}${b}${c}`);
})
bench('lodash', () => {
const arr1 = [1, 2];
const arr2 = [3, 4];
const arr3 = [5, 6];
zipWithLodash(arr1, arr2, arr3, (a, b, c) => `${a}${b}${c}`);
})
});

1
docs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.vitepress/cache

View File

@ -0,0 +1,13 @@
import { defineConfig } from 'vitepress';
import { en } from './en.mts';
import { ko } from './ko.mts';
import { shared } from './shared.mts';
export default defineConfig({
...shared,
locales: {
root: { label: 'English', ...en },
ko: { label: '한국어', ...ko }
},
})

122
docs/.vitepress/en.mts Normal file
View File

@ -0,0 +1,122 @@
import { defineConfig, type DefaultTheme } from 'vitepress'
export const en = defineConfig({
lang: 'en',
description: 'A state-of-the-art, high-performance JavaScript utility library with a small bundle size and strong type annotations.',
themeConfig: {
nav: nav(),
sidebar: sidebar(),
editLink: {
pattern: 'https://github.com/toss/es-toolkit/edit/main/docs/:path',
text: 'Edit this page on GitHub'
},
footer: {
message: 'Released under the MIT License.',
copyright: `Copyright © ${new Date().getFullYear()} Viva Republica, Inc.`
}
}
})
function nav(): DefaultTheme.NavItem[] {
return [
{ text: 'Home', link: '/' },
{ text: 'Introduction', link: '/intro' },
{ text: 'Reference', link: '/reference/array/chunk' },
]
}
function sidebar(): DefaultTheme.Sidebar {
return [
{
text: 'Guide',
items: [
{ text: 'Introduction', link: '/intro' },
{ text: 'Installation', link: '/installation' },
{ text: 'Impact on Bundle Size', link: '/bundle-size' },
{ text: 'Performance', link: '/performance' },
]
},
{
text: 'Reference',
items: [
{
text: 'Array Utilities',
items: [
{ text: 'chunk', link: '/reference/array/chunk' },
{ text: 'difference', link: '/reference/array/difference' },
{ text: 'differenceBy', link: '/reference/array/differenceBy' },
{ text: 'differenceWith', link: '/reference/array/differenceWith' },
{ text: 'drop', link: '/reference/array/drop' },
{ text: 'dropWhile', link: '/reference/array/dropWhile' },
{ text: 'dropRight', link: '/reference/array/dropRight' },
{ text: 'dropRightWhile', link: '/reference/array/dropRightWhile' },
{ text: 'groupBy', link: '/reference/array/groupBy' },
{ text: 'intersection', link: '/reference/array/intersection' },
{ text: 'intersectionBy', link: '/reference/array/intersectionBy' },
{ text: 'intersectionWith', link: '/reference/array/intersectionWith' },
{ text: 'partition', link: '/reference/array/partition' },
{ text: 'sample', link: '/reference/array/sample' },
{ text: 'shuffle', link: '/reference/array/shuffle' },
{ text: 'take', link: '/reference/array/take' },
{ text: 'takeWhile', link: '/reference/array/takeWhile' },
{ text: 'takeRight', link: '/reference/array/takeRight' },
{ text: 'takeRightWhile', link: '/reference/array/takeRightWhile' },
{ text: 'union', link: '/reference/array/union' },
{ text: 'unionBy', link: '/reference/array/unionBy' },
{ text: 'unionWith', link: '/reference/array/unionWith' },
{ text: 'uniq', link: '/reference/array/uniq' },
{ text: 'xor', link: '/reference/array/xor' },
{ text: 'xorBy', link: '/reference/array/xorBy' },
{ text: 'xorWith', link: '/reference/array/xorWith' },
{ text: 'zip', link: '/reference/array/zip' },
{ text: 'zipWith', link: '/reference/array/zipWith' },
]
},
{
text: 'Function Utilities',
items: [
{ text: 'debounce', link: '/reference/function/debounce' },
{ text: 'throttle', link: '/reference/function/throttle' },
{ text: 'once', link: '/reference/function/once' },
]
},
{
text: 'Math Utilities',
items: [
{ text: 'clamp', link: '/reference/math/clamp' },
{ text: 'round', link: '/reference/math/round' },
{ text: 'sum', link: '/reference/math/sum' },
]
},
{
text: 'Object Utilities',
items: [
{ text: 'omit', link: '/reference/object/omit' },
{ text: 'omitBy', link: '/reference/object/omitBy' },
{ text: 'pick', link: '/reference/object/pick' },
{ text: 'pickBy', link: '/reference/object/pickBy' },
]
},
{
text: 'Predicates',
items: [
{ text: 'isNil', link: '/reference/predicate/isNil' },
{ text: 'isNotNil', link: '/reference/predicate/isNotNil' },
{ text: 'isNull', link: '/reference/predicate/isNull' },
{ text: 'isUndefined', link: '/reference/predicate/isUndefined' },
]
},
{
text: 'Promise Utilities',
items: [
{ text: 'delay', link: '/reference/promise/delay' },
]
}
]
}
]
}

149
docs/.vitepress/ko.mts Normal file
View File

@ -0,0 +1,149 @@
import { defineConfig, type DefaultTheme } from 'vitepress'
export const ko = defineConfig({
lang: 'ko',
description: '빠른 성능, 작은 번들 사이즈를 가지는 현대적인 JavaScript 유틸리티 라이브러리',
themeConfig: {
nav: nav(),
sidebar: sidebar(),
editLink: {
pattern: 'https://github.com/toss/es-toolkit/edit/main/docs/:path',
text: 'GitHub에서 수정하기'
},
footer: {
message: 'MIT 라이선스에 따라 배포됩니다.',
copyright: `Copyright © ${new Date().getFullYear()} Viva Republica, Inc.`
}
}
})
function nav(): DefaultTheme.NavItem[] {
return [
{ text: '홈', link: '/ko' },
{ text: '소개', link: '/ko/intro' },
{ text: '레퍼런스', link: '/ko/reference/array/chunk' },
]
}
function sidebar(): DefaultTheme.Sidebar {
return [
{
text: '가이드',
items: [
{ text: '소개', link: '/ko/intro' },
{ text: '설치', link: '/ko/installation' },
{ text: '번들 사이즈', link: '/ko/bundle-size' },
{ text: '성능', link: '/ko/performance' },
]
},
{
text: '레퍼런스',
items: [
{
text: '배열',
items: [
{ text: 'chunk', link: '/ko/reference/array/chunk' },
{ text: 'difference', link: '/ko/reference/array/difference' },
{ text: 'differenceBy', link: '/ko/reference/array/differenceBy' },
{ text: 'differenceWith', link: '/ko/reference/array/differenceWith' },
{ text: 'drop', link: '/ko/reference/array/drop' },
{ text: 'dropRight', link: '/ko/reference/array/dropRight' },
{ text: 'dropWhile', link: '/ko/reference/array/dropWhile' },
{ text: 'dropRightWhile', link: '/ko/reference/array/dropRightWhile' },
{ text: 'groupBy', link: '/ko/reference/array/groupBy' },
{ text: 'intersection', link: '/ko/reference/array/intersection' },
{ text: 'intersectionBy', link: '/ko/reference/array/intersectionBy' },
{ text: 'intersectionWith', link: '/ko/reference/array/intersectionWith' },
{ text: 'partition', link: '/ko/reference/array/partition' },
{ text: 'sample', link: '/ko/reference/array/sample' },
{ text: 'shuffle', link: '/ko/reference/array/shuffle' },
{ text: 'take', link: '/ko/reference/array/take' },
{ text: 'takeWhile', link: '/ko/reference/array/takeWhile' },
{ text: 'takeRight', link: '/ko/reference/array/takeRight' },
{ text: 'takeRightWhile', link: '/ko/reference/array/takeRightWhile' },
{ text: 'union', link: '/ko/reference/array/union' },
{ text: 'unionBy', link: '/ko/reference/array/unionBy' },
{ text: 'unionWith', link: '/ko/reference/array/unionWith' },
{ text: 'uniq', link: '/ko/reference/array/uniq' },
{ text: 'xor', link: '/ko/reference/array/xor' },
{ text: 'xorBy', link: '/ko/reference/array/xorBy' },
{ text: 'xorWith', link: '/ko/reference/array/xorWith' },
{ text: 'zip', link: '/ko/reference/array/zip' },
{ text: 'zipWith', link: '/ko/reference/array/zipWith' },
]
},
{
text: '함수',
items: [
{ text: 'debounce', link: '/ko/reference/function/debounce' },
{ text: 'throttle', link: '/ko/reference/function/throttle' },
{ text: 'once', link: '/ko/reference/function/once' },
]
},
{
text: '숫자',
items: [
{ text: 'clamp', link: '/ko/reference/math/clamp' },
{ text: 'round', link: '/ko/reference/math/round' },
{ text: 'sum', link: '/ko/reference/math/sum' },
]
},
{
text: '객체',
items: [
{ text: 'omit', link: '/ko/reference/object/omit' },
{ text: 'omitBy', link: '/ko/reference/object/omitBy' },
{ text: 'pick', link: '/ko/reference/object/pick' },
{ text: 'pickBy', link: '/ko/reference/object/pickBy' },
]
},
{
text: '타입 가드',
items: [
{ text: 'isNil', link: '/ko/reference/predicate/isNil' },
{ text: 'isNotNil', link: '/ko/reference/predicate/isNotNil' },
{ text: 'isNull', link: '/ko/reference/predicate/isNull' },
{ text: 'isUndefined', link: '/ko/reference/predicate/isUndefined' },
]
},
{
text: 'Promise',
items: [
{ text: 'delay', link: '/ko/reference/promise/delay' },
]
}
]
}
]
}
export const search: DefaultTheme.LocalSearchOptions["locales"] = {
ko: {
translations: {
button: {
buttonText: '검색',
buttonAriaLabel: '검색',
},
modal: {
backButtonTitle: '뒤로가기',
displayDetails: '더보기',
footer: {
closeKeyAriaLabel: '닫기',
closeText: '닫기',
navigateDownKeyAriaLabel: '아래로',
navigateText: '이동',
navigateUpKeyAriaLabel: '위로',
selectKeyAriaLabel: '선택',
selectText: '선택',
},
noResultsText: '검색 결과를 찾지 못했어요.',
resetButtonTitle: '모두 지우기',
}
}
}
}

View File

@ -0,0 +1,82 @@
import { createRequire } from 'module';
import path from 'path';
import { defineConfig } from 'vitepress';
import { search as koSearch } from './ko.mts';
const require = createRequire(import.meta.url)
export const shared = defineConfig({
title: 'es-toolkit',
lastUpdated: true,
metaChunk: true,
/* prettier-ignore */
head: [
[
"link",
{
rel: "icon",
type: "image/png",
sizes: "100x100",
href: "/favicon-100x100.png",
},
],
[
"link",
{
rel: "stylesheet",
href: "https://static.toss.im/tps/main.css"
}
],
[
"link",
{
rel: "stylesheet",
href: "https://static.toss.im/tps/others.css"
}
],
[
"meta",
{
property: "og:image",
content: "/og.png",
},
],
],
themeConfig: {
logo: {
dark: '/logo_white.png',
light: '/logo_black.png',
},
siteTitle: false,
search: {
provider: 'local',
options: {
locales: {
...koSearch,
}
}
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/toss/es-toolkit' }
],
},
vite: {
resolve: {
alias: {
'vue': path.dirname(require.resolve('vue/package.json', {
paths: [require.resolve('vitepress')],
})),
'vue/server-renderer': path.dirname(require.resolve('vue/server-renderer', {
paths: [require.resolve('vitepress')],
})),
}
}
},
})

View File

@ -0,0 +1,8 @@
:root {
--vp-nav-logo-height: 16px;
--vp-font-family-base: "Toss Product Sans", ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
:root[lang=ko] {
word-break: keep-all;
}

View File

@ -0,0 +1,4 @@
import DefaultTheme from 'vitepress/theme'
import './index.css'
export default DefaultTheme

24
docs/bundle-size.md Normal file
View File

@ -0,0 +1,24 @@
---
description: The minimal bundle footprint offered by es-toolkit
---
Bundle Footprint
============
![Graph showing the difference in bundle size between es-toolkit and lodash. There is a difference up to 97% in bundle size.](/assets/bundle-size.png)
With its modern implementation, es-toolkit significantly reduces its bundle size, cutting it down by up to 97% compared to other libraries like lodash.
This makes es-toolkit the most efficient in terms of bundle size, with some utility functions being as small as less than 100 bytes.
## Bundle Footprint Comparison
| | es-toolkit@0.0.1 | lodash-es@4.17.21 | Difference |
|-----------------------------------------------|------------------|--------------------|------------|
| [sample](./reference/array/sample) | 88 bytes | 2000 bytes | -95.6% |
| [difference](./reference/array/difference) | 91 bytes | 3190 bytes | -97.2% |
| [sum](./reference/math/sum) | 152 bytes | 413 bytes | -63.2% |
| [debounce](./reference/function/debounce) | 144 bytes | 1400 bytes | -89.7% |
| [throttle](./reference/function/throttle) | 110 bytes | 1460 bytes | -92.5% |
| [pick](./reference/object/pick) | 657 bytes | 3860 bytes | -83.0% |
| [zip](./reference/array/zip) | 797 bytes | 1790 bytes | -55.5% |

32
docs/index.md Normal file
View File

@ -0,0 +1,32 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "es-toolkit"
text: "State-of-the-art JavaScript utility library"
# tagline:
actions:
- theme: brand
text: About es-toolkit
link: /intro
- theme: alt
text: Reference
link: /reference/array/chunk
- theme: alt
text: Installation
link: /installation
features:
- title: Best performance
details: es-toolkit delivers 2-3× better performance in modern JavaScript runtimes compared to other libraries.
- title: Small bundle footprint
details: es-toolkit ships up to 97% less JavaScript code compared to other alternative libraries.
- title: Modern implementation
details: es-toolkit fully leverages modern JavaScript APIs for straightforward and error-free implementation.
- title: Robust types
details: es-toolkit offers simple yet robust types for all functions.
- title: Battle-tested
details: es-toolkit has 100% test coverage, ensuring maximum robustness.
---

44
docs/installation.md Normal file
View File

@ -0,0 +1,44 @@
---
description: How to install es-toolkit
prev:
text: Introduction to es-toolkit
link: ./intro.md
next:
text: Impact on Bundle Size
link: ./bundle-size
---
Installation
============
es-toolkit is available on [npm](https://npmjs.com/package/es-toolkit) for Node.js, Deno, and Bun.
Node.js
---
es-toolkit supports Node.js 18 or later. Install es-toolkit with the following command:
```sh
npm install es-toolkit
yarn add es-toolkit
pnpm install es-toolkit
```
Deno
----
es-toolkit is also available on npm for Deno. Use es-toolkit with the `npm:` specifier:
```typescript
import { chunk } from 'npm:es-toolkit@1.0.0';
```
Bun
---
es-toolkit is also available on Bun. You can install it via the following command:
```sh
bun add es-toolkit
```

26
docs/intro.md Normal file
View File

@ -0,0 +1,26 @@
About es-toolkit
===============
es-toolkit is a modern JavaScript utility library that offers a collection of powerful functions for everyday use.
Compared to alternatives like lodash, es-toolkit provides a [significantly smaller bundle size](./bundle-size.md) (up to 97% less) and [2-3 times faster runtime performance](./performance.md). This is achieved through a modern implementation that leverages the latest JavaScript features.
es-toolkit comes with built-in TypeScript types and has been rigorously tested, ensuring 100% test coverage for maximum reliability.
## Features
Here are some of the features es-toolkit offers:
- **Array**: Utilities for array manipulation, such as [uniq](./reference/array/uniq.md) and [difference](./reference/array/difference.md).
- **Function**: Tools for controlling function execution, including [debounce](./reference/function/debounce.md) and [throttle](./reference/function/throttle.md).
- **Math**: Numerical utilities like [sum](./reference/math/sum.md) and [round](./reference/math/round.md).
- **Object**: Tools for manipulating JavaScript objects, such as [pick](./reference/object/pick.md) and [omit](./reference/object/omit.md).
- **Predicate**: Type guard functions like [isNotNil](./reference/predicate/isNotNil.md).
- **Promise**: Asynchronous utilities like [delay](./reference/promise/delay.md).
## Links
Please refer to the following links for more information about this project.
- [GitHub](https://github.com/toss/es-toolkit)

24
docs/ko/bundle-size.md Normal file
View File

@ -0,0 +1,24 @@
---
description: es-toolkit이 가지는 작은 번들 사이즈
---
번들 사이즈
============
![es-toolkit과 lodash의 번들 사이즈를 비교하는 그래프. es-toolkit이 최대 97% 작은 번들 사이즈를 가진다.](/assets/bundle-size.png)
es-toolkit은 현대적인 구현을 가지고 있기 때문에, 다른 라이브러리에 비해서 매우 작은 번들 사이즈를 가져요. [lodash](https://lodash.com)와 비교했을 때, 함수에 따라서는 최대 97% 작은 크기를 가져요.
이런 측면에서, es-toolkit은 번들 사이즈를 줄이는 데에 가장 효율적인 선택이에요. 어떤 유틸리티 함수는 100바이트보다 작은 크기를 가져요.
## 번들 사이즈 비교
| | es-toolkit@0.0.1 | lodash-es@4.17.21 | 차이 |
|-----------------------------------------------|------------------|--------------------|------------|
| [sample](./reference/array/sample.md) | 88 bytes | 2000 bytes | -95.6% |
| [difference](./reference/array/difference.md) | 91 bytes | 3190 bytes | -97.2% |
| [sum](./reference/math/sum.md) | 152 bytes | 413 bytes | -63.2% |
| [debounce](./reference/function/debounce.md) | 144 bytes | 1400 bytes | -89.7% |
| [throttle](./reference/function/throttle.md) | 110 bytes | 1460 bytes | -92.5% |
| [pick](./reference/object/pick.md) | 657 bytes | 3860 bytes | -83.0% |
| [zip](./reference/array/zip.md) | 797 bytes | 1790 bytes | -55.5% |

32
docs/ko/index.md Normal file
View File

@ -0,0 +1,32 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "es-toolkit"
text: "현대적인 JavaScript 유틸리티 라이브러리"
# tagline:
actions:
- theme: brand
text: es-toolkit 알아보기
link: /ko/intro
- theme: alt
text: 레퍼런스
link: /ko/reference/array/chunk
- theme: alt
text: 설치
link: /ko/installation
features:
- title: 빠른 성능
details: 같은 기능을 제공하는 다른 라이브러리와 비교했을 때, 평균 2-3배 빠른 런타임 성능을 제공해요.
- title: 작은 번들 사이즈
details: 같은 기능의 함수들을 기준으로, 최대 97% 작은 JavaScript 크기를 가져요.
- title: 현대적인 구현
details: 최신 JavaScript API를 사용하여, 구현이 직관적이고 간단해요.
- title: 견고한 타입
details: 모든 함수에 대해서 간단하지만 견고한 타입을 제공해요.
- title: 테스트 커버리지 100%
details: 모든 함수와 분기에 대해서 꼼꼼하게 테스트가 작성되어 있어요.
---

44
docs/ko/installation.md Normal file
View File

@ -0,0 +1,44 @@
---
description: es-toolkit을 설치하는 방법
prev:
text: es-toolkit 소개
link: ./intro.md
next:
text: 번들 사이즈에 미치는 영향
link: ./bundle-size
---
설치
============
Node.js나 Bun을 사용하는 경우, [npm](https://npmjs.com/package/es-toolkit)에서 설치할 수 있어요. Deno에서는 [JSR](https://jsr.io/es-toolkit)에서 설치할 수 있어요.
Node.js
---
es-toolkit은 Node.js 18 또는 이후 버전을 지원해요. es-toolkit을 설치하기 위해서는 아래 명령어를 사용해주세요.
```sh
npm install es-toolkit
yarn add es-toolkit
pnpm install es-toolkit
```
Deno
----
es-toolkit은 Deno도 지원해요. `npm:`을 이용해서 사용하세요.
```typescript
import { chunk } from 'npm:es-toolkit@1.0.0';
```
Bun
---
es-toolkit은 Bun에서도 사용할 수 있어요. 아래 명령어를 사용해주세요.
```sh
bun add es-toolkit
```

27
docs/ko/intro.md Normal file
View File

@ -0,0 +1,27 @@
es-toolkit 소개
===============
es-toolkit은 일상적인 개발에서 사용하는 다양한 함수들을 모은 현대적인 JavaScript 유틸리티 라이브러리예요.
[lodash](https://lodash.com/)와 같은 다른 라이브러리와 비교했을 때, es-toolkit은 [같은 함수 기준 최대 97% 작은 번들 사이즈](./bundle-size.md)
를 제공하며, [2~3배 빠른 속도로](./performance.md) 동작합니다. 최신 JavaScript API를 활용해서 현대적으로 구현한 덕분이죠.
es-toolkit은 견고한 TypeScript 타입을 내장하여 제공하며, 신뢰성을 높일 수 있도록 100% 테스트 커버리지를 목표로 하고 있습니다.
## 제공하는 기능
es-toolkit이 제공하는 기능 목록은 다음과 같습니다.
- **배열**: [uniq](./reference/array/uniq.md)나 [difference](./reference/array/difference.md)와 같이 배열을 다루기 위한 다양한 함수를 제공해요.
- **함수**: [debounce](./reference/function/debounce.md)나 [throttle](./reference/function/throttle.md)처럼 함수 호출을 다루는 도구를 제공해요.
- **숫자**: [sum](./reference/math/sum.md)이나 [round](./reference/math/round.md) 처럼 숫자를 쉽게 다루는 함수를 제공해요.
- **객체**: [pick](./reference/object/pick.md)이나 [omit](./reference/object/omit.md)처럼 JavaScript 객체를 다루는 함수를 제공해요.
- **타입 가드**: [isNotNil](./reference/predicate/isNotNil.md)처럼 특정한 객체가 어떤 상태인지 검사하는 타입 가드 함수를 제공해요.
- **Promise**: [delay](./reference/promise/delay.md)와 같은 비동기 유틸리티 함수를 제공해요.
## 링크
이 프로젝트에 대해서 더 많은 정보를 얻기 위해서는 아래 링크를 참고하세요.
- [GitHub](https://github.com/toss/es-toolkit)

27
docs/ko/performance.md Normal file
View File

@ -0,0 +1,27 @@
---
description: es-toolkit과 다른 라이브러리의 성능 차이
---
성능
============
![es-toolkit과 lodash 사이의 성능 차이를 보여주는 그래프. es-toolkit은 lodash를 쓸 때보다 최대 11배 높은 성능을 보여요.](/assets/performance.png)
es-toolkit은 설계할 때 성능을 우선적으로 고려하고 있어요. lodash와 같은 다른 라이브러리와 비교했을 때, 평균적으로 2배의 성능 향상을 확인할 수 있었어요. 함수에 따라서는 11배 빠른 성능을 보이기도 했죠.
현대적인 JavaScript API을 이용하여 구현했기 때문이에요.
## 성능 비교
| | es-toolkit@0.0.1 | lodash-es@4.17.21 | 차이 |
|-----------------------------------------------------------|------------------|------------------|------------|
| [omit](./reference/object/omit.md) | 4,767,360회 | 403,624회 | 11.8× |
| [pick](./reference/object/pick.md) | 9,121,839회 | 2,663,072회 | 3.43× |
| [differenceWith](./reference/array/differenceWith.md) | 9,291,897회 | 4,275,222회 | 2.17× |
| [difference](./reference/array/difference.md) | 10,436,101회 | 5,155,631회 | 2.02× |
| [intersectionWith](./reference/array/intersectionWith.md) | 8,074,722회 | 3,814,479회 | 2.12× |
| [intersection](./reference/array/intersection.md) | 9,999,571회 | 4,630,316회 | 2.15× |
| [unionBy](./reference/array/unionBy.md) | 6,435,983회 | 3,794,899회 | 1.69× |
| [union](./reference/array/union.md) | 5,059,209회 | 4,771,400회 | 1.06× |
| [dropRightWhile](./reference/array/dropRightWhile.md) | 7,529,559회 | 5,606,439회 | 1.34× |
| [groupBy](./reference/array/groupBy.md) | 5,000,235회 | 5,206,286회 | 0.96× |
테스트 환경은 MacBook Pro 14인치(M1 Max, 2021)예요. [벤치마크 코드](https://github.com/toss/es-toolkit/tree/main/benchmarks)를 참고하세요.

View File

@ -0,0 +1,40 @@
# chunk
배열을 정해진 길이에 맞게 더 작은 배열로 나눠요.
입력값으로 배열을 받아서, 정해진 길이를 따르는 더 작은 배열 여러 개로 나눠요.
입력 배열이 똑같은 길이로 나눠질 수 없다면, 나눠진 마지막 배열이 남은 요소들을 포함하게 돼요.
## 인터페이스
```typescript
function chunk<T>(arr: T[], size: number): T[][];
```
### 파라미터
- `arr` (`T[]`): 작은 배열들로 나눌 배열
- `size` (`number`): 작은 배열들의 길이. 양의 정수여야 해요.
### 반환 값
(`T[][]`): 최대 길이 `size`를 가지는 작은 배열들로 구성된 2차원 배열을 반환해요.
### 에러
`size`가 양의 정수가 아니면 에러를 던져요.
## 예시
```typescript
import { chunk } from 'es-toolkit/array';
// 숫자의 배열을 최대 2의 길이를 가지는 더 작은 배열들로 쪼개요.
chunk([1, 2, 3, 4, 5], 2);
// Returns: [[1, 2], [3, 4], [5]]
// 문자열의 배열을 최대 3의 길이를 가지는 더 작은 배열들로 쪼개요.
chunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 3);
// Returns: [['a', 'b', 'c'], ['d', 'e', 'f'], ['g']]
```

View File

@ -0,0 +1,36 @@
# difference
두 배열의 차이를 계산해요.
이 함수는 파라미터로 두 배열을 받아서, 첫 번째 배열에 있지만 두 번째 배열에는 없는 요소들을 포함한 새로운 배열을 반환해요.
즉, 첫 번째 배열에서 두 번째 배열에 있는 요소들을 제외한 나머지 요소들로 구성된 배열을 만들어줘요.
## 인터페이스
```typescript
function difference<T>(firstArr: T[], secondArr: T[]): T[];
```
### 파라미터
- `firstArr` (`T[]`): 차이를 계산할 배열이에요. 이 배열이 주 배열이고, 이 배열의 요소들이 비교되고 필터링돼요.
- `secondArr` (`T[]`): 첫 번째 배열에서 제외할 요소들을 포함한 배열이에요. 이 배열의 각 요소는 첫 번째 배열과 비교되며, 일치하는 요소가 있으면 결과에서 제외돼요.
### 반환 값
(`T[]`): 첫 번째 배열에는 있지만 두 번째 배열에는 없는 요소들이 담긴 새로운 배열이에요.
## 예시
```typescript
import { difference } from 'es-toolkit/array';
// 사용 예제:
const array1 = [1, 2, 3, 4, 5];
const array2 = [2, 4];
const result = difference(array1, array2);
// 2와 4는 두 배열 모두에 있기 때문에 결과에서 제외되고, result 변수에는 [1, 3, 5]가 할당되어요.
```

View File

@ -0,0 +1,33 @@
# differenceBy
제공된 함수로 요소들을 매핑한 후 두 배열의 차이를 계산해요.
이 함수는 두 배열과 매퍼 함수를 받아, 매퍼 함수로 계산된 결과를 기준으로 첫 번째 배열에 있지만 두 번째 배열에는 없는 요소들을 포함한 새로운 배열을 반환해요. 즉, 매핑된 값이 두 번째 배열의 매핑된 값과 일치하는 첫 번째 배열의 요소들을 제외한 나머지 요소들로 구성된 배열을 만들어줘요.
## 인터페이스
```typescript
function differenceBy<T, U>(firstArr: T[], secondArr: T[], mapper: (value: T) => U): T[]
```
### 파라미터
- `firstArr` (`T[]`): 차이를 계산할 배열이에요. 이 배열이 주 배열이고, 이 배열의 요소들이 비교되고 필터링돼요.
- `secondArr` (`T[]`): 첫 번째 배열에서 제외할 요소들을 포함한 배열이에요.
- `mapper` (`(value: T) => U`): 두 배열의 요소들을 매핑할 함수에요. 이 함수는 두 배열의 각 요소에 적용되며, 매핑된 값들을 기준으로 비교를 해요.
### 반환 값
(`T[]`): 첫 번째 배열에는 있지만 매핑된 값이 두 번째 배열의 매핑된 값과 일치하지 않는 요소들이 담긴 새로운 배열이에요.
## 예시
```typescript
import { differenceBy } from 'es-toolkit/array';
const array1 = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];
const array2 = [{ id: 2 }, { id: 4 }];
const mapper = item => item.id;
const result = differenceBy(array1, array2, mapper);
// result는 [{ id: 1 }, { id: 3 }, { id: 5 }]가 돼요. id가 2인 요소들은 두 배열 모두에 있어서 결과에서 제외돼요.
```

View File

@ -0,0 +1,33 @@
# differenceWith
인자로 제공된 비교 함수에 따라 두 배열의 차이를 계산해요.
이 함수는 두 배열과 비교 함수를 받아요. 이 비교 함수로 요소들이 동일한지 비교해서 첫 번째 배열에 있지만 두 번째 배열에는 없는 요소들을 포함한 새로운 배열을 반환해요.
## 인터페이스
```typescript
function differenceWith<T>(firstArr: T[], secondArr: T[], areItemsEqual: (x: T, y: T) => boolean): T[]
```
### 파라미터
- `firstArr` (`T[]`): 차이를 계산할 배열이에요. 이 배열이 주 배열이고, 이 배열의 요소들이 비교되고 필터링돼요.
- `secondArr` (`T[]`) : 첫 번째 배열에서 제외할 요소들을 포함한 배열이에요.
- `areItemsEqual` (`(x: T, y: T) => boolean`): 두 요소가 동일한지 결정할 함수에요.
### 반환 값
(`T[]`) 비교 함수에 따라 첫 번째 배열에는 있지만 두 번째 배열에는 존재하지 않는다고 결정된 요소들이 담긴 새로운 배열이에요.
## 예시
```typescript
import { differenceWith } from 'es-toolkit/array';
const array1 = [{ id: 1 }, { id: 2 }, { id: 3 }];
const array2 = [{ id: 2 }, { id: 4 }];
const areItemsEqual = (a, b) => a.id === b.id;
const result = differenceWith(array1, array2, areItemsEqual);
// result는 [{ id: 1 }, { id: 3 }]가 돼요. id가 2인 요소들은 동일하다고 간주돼서 결과에서 제외돼요.
```

View File

@ -0,0 +1,28 @@
# drop
배열의 시작부터 몇 개의 요소를 제거한 새로운 배열을 반환해요.
이 함수는 파라미터로 배열과 숫자를 받아요. 배열의 시작부터 숫자만큼의 요소를 제외한 새로운 배열을 반환해요.
## 인터페이스
```typescript
function drop<T>(arr: T[], itemsCount: number): T[];
```
### 파라미터
- `arr` (`T[]`): 요소를 제거할 배열.
- `itemsCount` (`number`): 배열의 시작부터 제거할 요소의 숫자.
### 반환 값
(`T[]`): 배열의 시작부터 숫자만큼의 요소를 제외한 새로운 배열을 반환해요.
## 예시
```typescript
const array = [1, 2, 3, 4, 5];
const result = drop(array, 2);
// 첫 두 개 요소가 제거되므로, 결과는 [3, 4, 5]이에요.
```

View File

@ -0,0 +1,28 @@
# dropRight
배열의 마지막부터 몇 개의 요소를 제거한 새로운 배열을 반환해요.
이 함수는 파라미터로 배열과 숫자를 받아요. 배열의 마지막부터 숫자만큼의 요소를 제외한 새로운 배열을 반환해요.
## 인터페이스
```typescript
function dropRight<T>(arr: T[], itemsCount: number): T[];
```
### 파라미터
- `arr` (`T[]`): 요소를 제거할 배열.
- `itemsCount` (`number`): 배열의 지막부터 제거할 요소의 숫자.
### 반환 값
(`T[]`): 배열의 마지막부터 숫자만큼의 요소를 제외한 새로운 배열을 반환해요.
## 예시
```typescript
const array = [1, 2, 3, 4, 5];
const result = dropRight(array, 2);
// 마지막 두 개 요소가 반환되므로, 결괏값은 [1, 2, 3]이 돼요.
```

View File

@ -0,0 +1,29 @@
# dropRightWhile
배열의 끝부터 시작해서, 조건 함수가 `false`를 반환할 때까지 요소들을 제거해요.
이 함수는 각 배열의 요소를 순회하면서, 배열의 끝부터 조건 함수가 `false`를 반환할 때까지 요소를 제거해요.
남은 요소들로 구성된 새로운 배열을 반환해요.
## 인터페이스
```typescript
function dropRightWhile<T>(arr: T[], canContinueDropping: (item: T) => boolean): T[]
```
### 파라미터
- `arr` (`T[]`): 요소를 제거할 배열.
- `canContinueDropping` (`(item: T) => boolean`): 요소를 제거하는 것을 계속할지 반환하는 조건 함수예요. 각 요소에 대해서 호출되면서, `true`를 반환하는 동안 요소를 제거해요.
### 반환 값
(`T[]`): 조건 함수가 `false`를 반환할 때까지 남은 요소들로 이루어진 새로운 배열.
## Examples
```typescript
const array = [1, 2, 3, 2, 4, 5];
const result = dropRightWhile(array, x => x > 3);
// 3보다 작거나 같은 요소를 만날 때까지 요소를 제거하므로, 결괏값은 [1, 2, 3, 2]이 되어요.
```

View File

@ -0,0 +1,30 @@
# dropWhile
배열의 처음부터 시작해서, 조건 함수가 `false`를 반환할 때까지 요소들을 제거해요.
이 함수는 각 배열의 요소를 순회하면서, 배열의 시작부터 조건 함수가 `false`를 반환할 때까지 요소를 제거해요.
남은 요소들로 구성된 새로운 배열을 반환해요.
## 인터페이스
```typescript
function dropWhile<T>(arr: T[], canContinueDropping: (item: T) => boolean): T[]
```
### 파라미터
- `arr` (`T[]`): 요소를 제거할 배열.
- `canContinueDropping` (`(item: T) => boolean`): 요소를 제거하는 것을 계속할지 반환하는 조건 함수예요. 각 요소에 대해서 호출되면서, `true`를 반환하는 동안 요소를 제거해요.
### 반환 값
(`T[]`): 조건 함수가 `false`를 반환할 때까지 남은 요소들로 이루어진 새로운 배열.
## 예시
```typescript
const array = [1, 2, 3, 2, 4, 5];
const result = dropWhile(array, x => x < 3);
// 3보다 작은 요소를 발견할 때까지 요소를 제거하므로, 결괏값은 [3, 2, 4, 5]이 되어요.
```

View File

@ -0,0 +1,42 @@
# groupBy
주어진 키 생성 함수에 따라서 배열의 요소를 분류해요.
이 함수는 파라미터로 배열과 각 요소에서 키를 생성하는 함수를 받아요.
키는 함수에서 생성된 키이고, 값은 그 키를 공유하는 요소끼리 묶은 배열인 객체를 반환해요.
## 인터페이스
```typescript
function groupBy<T, K extends string>(arr: T[], getKeyFromItem: (item: T) => K): Record<K, T[]>;
```
### 파라미터
- `arr` (`T[]`): 요소를 분류할 배열.
- `getKeyFromItem` (`(item: T) => K`): 요소에서 키를 생성하는 함수.
### 반환 값
(`Record<K, T[]>`): 키에 따라 요소가 분류된 객체를 반환해요.
## 예시
```typescript
const array = [
{ category: 'fruit', name: 'apple' },
{ category: 'fruit', name: 'banana' },
{ category: 'vegetable', name: 'carrot' }
];
const result = groupBy(array, item => item.category);
// 결괏값:
// {
// fruit: [
// { category: 'fruit', name: 'apple' },
// { category: 'fruit', name: 'banana' }
// ],
// vegetable: [
// { category: 'vegetable', name: 'carrot' }
// ]
// }
```

View File

@ -0,0 +1,32 @@
# intersection
두 배열 모두에 포함되어 있는 요소를 반환해요.
이 함수는 두 개의 배열을 받고, 두 배열 모두에 포함되어 있는 요소로 구성된 새로운 배열을 반환해요.
실제로는 첫 번째 배열의 요소들 중에서 두 번째 배열에 포함되어 있지 않은 요소들을 제거해요.
## 인터페이스
```typescript
function intersection<T>(firstArr: T[], secondArr: T[]): T[];
```
### 파라미터
- `firstArr` (`T[]`): 비교할 첫 번째 배열.
- `secondArr` (`T[]`): 비교할 두 번째 배열.
### 반환 값
(`T[]`) 두 배열 모두에 포함되어 있는 요소로 구성된 새로운 배열.
## 예시
```typescript
const array1 = [1, 2, 3, 4, 5];
const array2 = [3, 4, 5, 6, 7];
const result = intersection(array1, array2);
// 두 배열에 모두 포함되어 있는 [3, 4, 5]를 반환해요.
```

View File

@ -0,0 +1,36 @@
# intersectionBy
`mapper` 함수가 반환하는 값을 기준으로, 두 배열의 교집합을 반환해요.
이 함수는 파라미터로 두 개의 배열과 `mapper` 함수를 받아요.
`mapper` 함수로 각 배열의 요소들을 변환했을 때, 두 배열에 모두 포함되는 요소들로 이루어진 새로운 배열을 반환해요.
실제 구현을 살펴보면, 첫 번째 배열과 두 번째 배열을 `mapper` 가 반환하는 값을 기준으로 비교하여, 첫 번째 배열의 요소들 중 두 번째 배열에 없는 요소들을 제거해요.
## 인터페이스
```typescript
function intersectionBy<T, U>(firstArr: T[], secondArr: T[], mapper: (item: T) => U): T[];
```
### 파라미터
- `firstArr` (`T[]`): 비교할 첫 번째 배열.
- `secondArr` (`T[]`): 비교할 두 번째 배열.
- `mapper` (`(item: T) => U`): 비교하기 위해 요소를 새로운 값으로 변환할 함수.
### 반환 값
(`T[]`): 첫 번째 배열과 두 번째 배열을 `mapper` 가 반환하는 값을 기준으로 비교하여, 두 배열 모두에 포함되는 요소들만 포함하는 새로운 배열.
## 예시
```typescript
const array1 = [{ id: 1 }, { id: 2 }, { id: 3 }];
const array2 = [{ id: 2 }, { id: 4 }];
const mapper = item => item.id;
const result = intersectionBy(array1, array2, mapper);
// `mapper`로 변환했을 때 두 배열 모두에 포함되는 요소로 이루어진 [{ id: 2 }] 값이 반환되어요.
```

View File

@ -0,0 +1,34 @@
# intersectionWith
두 요소가 일치하는지 여부를 판단하는 커스텀 함수를 기준으로, 두 배열의 교집합을 반환해요.
이 함수는 파라미터로 두 개의 배열과 커스텀 일치 함수를 받아요.
이 함수는 커스텀 일치 함수의 반환 값을 기준으로, 두 배열에 모두 포함된 요소들을 새로운 배열로 반환해요.
실제 구현을 살펴보면, 첫 번째 배열의 요소들 중에서 두 번째 배열의 어떤 요소와도 일치 함수를 기준으로 같지 않은 요소들을 제외한 새로운 배열을 반환해요.
## 인터페이스
```typescript
function intersectionWith<T>(firstArr: T[], secondArr: T[], areItemsEqual: (x: T, y: T) => boolean): T[];
```
### 파라미터
- `firstArr` (`T[]`): 비교할 첫 번째 배열.
- `secondArr` (`T[]`): 비교할 두 번째 배열.
- `areItemsEqual` (`(x: T, y: T) => boolean`): 두 요소가 일치하는지 판단하는 일치 함수예요. 두 요소가 일치한다면 `true`를, 일치하지 않는다면 `false`를 반환하게 해주세요.
### 반환 값
(`T[]`): 커스텀 일치 함수의 반환 값을 기준으로, 두 배열에 모두 포함된 요소들을 포함하는 새로운 배열.
## 예시
```typescript
const array1 = [{ id: 1 }, { id: 2 }, { id: 3 }];
const array2 = [{ id: 2 }, { id: 4 }];
const areItemsEqual = (a, b) => a.id === b.id;
const result = intersectionWith(array1, array2, areItemsEqual);
// `areItemsEqual` 기준으로 array1과 array2에 모두 포함되어 있는 요소들로 이루어진 [{ id: 2 }] 이 반환돼요.
```

View File

@ -0,0 +1,31 @@
# partition
배열을 2개로 나누는 데에 쓸 수 있는 함수예요.
이 함수는 파라미터로 배열과 `true`/`false`를 반환하는 함수를 받아요.
이 함수는 두 개의 배열로 이루어진 튜플을 반환하는데요, 첫 번째 배열은 함수가 `true`를 반환하는 요소들로 구성돼요.
두 번째 배열은 함수가 `false`를 반환하는 요소들로 구성돼요.
## 인터페이스
```typescript
function partition<T>(arr: T[], isInTruthy: (value: T) => boolean): [truthy: T[], falsy: T[]];
```
### 파라미터
- `arr` (`T[]`): 2개로 나눌 배열.
- `isInTruthy` (`(value: T) => boolean`): 배열의 요소가 첫 번째 배열에 포함될지, 두 번째 배열에 포함될지 결정하는 함수. `true`를 반환하면 첫 번째 배열에, `false`를 반환하면 두 번째 배열에 포함돼요.
### 반환 값
(`[T[], T[]]`): 2개의 배열로 구성된 튜플. 첫 번째 배열은 `isInTruthy``true`를 반환한 요소들로, 두 번째 배열은 `false`를 반환한 요소들로 구성돼요.
## 예시
```typescript
const array = [1, 2, 3, 4, 5];
const is짝수 = x => x % 2 === 0;
const [짝수, 홀수] = partition(array, is짝수);
// 짝수는 [2, 4], 홀수는 [1, 3, 5]가 되어요.
```

View File

@ -0,0 +1,25 @@
# sample
배열을 받아서 그 배열에서 무작위로 선택된 단일 요소를 반환해요.
## 인터페이스
```typescript
function sample<T>(arr: T[]): T;
```
### 파라미터
- `arr` (`T[]`): 샘플링할 배열이에요.
### 반환 값
(`T`): 배열에서 무작위로 선택된 요소에요.
## 예시
```typescript
const array = [1, 2, 3, 4, 5];
const randomElement = sample(array);
// randomElement는 배열에서 무작위로 선택된 요소 중 하나가 될 거예요.
```

View File

@ -0,0 +1,25 @@
# shuffle
배열의 요소 순서를 무작위로 섞어요. Fisher-Yates 알고리즘을 사용해요.
## 인터페이스
```typescript
function shuffle<T>(arr: T[]): T[];
```
### 파라미터
- `arr` (`T[]`): 요소를 뒤섞을 배열이에요.
### 반환 값
(`T[]`): 요소들이 무작위로 섞인 새로운 배열이에요.
## 예시
```typescript
const array = [1, 2, 3, 4, 5];
const shuffledArray = shuffle(array);
// shuffledArray는 배열의 요소들이 무작위로 섞인 새로운 배열인 [3, 1, 4, 5, 2]이 되어요.
```

View File

@ -0,0 +1,34 @@
# take
입력 배열 arr에서 처음 count개의 요소를 포함하는 새로운 배열을 반환해요.
만약 count가 arr의 길이보다 크면, 전체 배열을 반환해요.
## 인터페이스
```typescript
function take<T>(arr: T[], count: number): T[];
```
### 파라미터
- `arr` (`T[]`): 요소를 가져올 배열이에요.
- `count` (`number`): 가져올 요소의 개수예요.
### 반환 값
(T[]): arr에서 처음 `count`개의 요소를 포함하는 새로운 배열이에요.
## 예시
```typescript
// [1, 2, 3]
take([1, 2, 3, 4, 5], 3);
// ['a', 'b']
take(['a', 'b', 'c'], 2);
// [1, 2, 3]
take([1, 2, 3], 5);
```

View File

@ -0,0 +1,35 @@
# takeRight
입력 배열 `arr`에서 마지막 `count`개의 요소를 포함하는 새로운 배열을 반환해요.
만약 `count``arr`의 길이보다 크면, 전체 배열을 반환해요.
## 인터페이스
```typescript
function takeRight<T>(arr: T[], count: number): T[];
```
### 파라미터
- `arr` (`T[]`): 요소를 가져올 배열이에요.
- `count` (`number`): 가져올 요소의 개수예요.
### 반환 값
(`T[]`) `arr` 에서 마지막 `count` 개의 요소를 포함하는 새로운 배열이에요.
## 예시
```typescript
// [4, 5]
takeRight([1, 2, 3, 4, 5], 2);
// ['b', 'c']
takeRight(['a', 'b', 'c'], 2);
// [1, 2, 3]
takeRight([1, 2, 3], 5);
```

View File

@ -0,0 +1,30 @@
# takeRightWhile
조건 함수가 `true`를 반환하는 동안 배열의 끝에서부터 요소들을 가져와요.
조건을 만족하지 않는 요소가 나오면 멈춰요.
## 인터페이스
```typescript
function takeRightWhile<T>(arr: T[], shouldContinueTaking: (item: T) => boolean): T[]
```
### 파라미터
- `arr` (`T[]`): 요소를 가져올 배열이에요.
- `shouldContinueTaking` (`(item: T) => boolean`): 각 요소와 함께 호출되는 조건 함수예요. 이 함수가 `true`를 반환하는 동안 요소들이 결과에 포함돼요.
### 반환 값
(`T[]`): 조건 함수가 `true`를 반환하는 동안 배열의 끝에서부터 가져온 요소들을 포함하는 새로운 배열이에요.
## 예시
```typescript
// [3, 2, 1]
takeRightWhile([5, 4, 3, 2, 1], n => n < 4);
// []
takeRightWhile([1, 2, 3], n => n > 3);
```

Some files were not shown because too many files have changed in this diff Show More