-
Notifications
You must be signed in to change notification settings - Fork 544
feat(promise): implement allKeyed #1672
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # allKeyed | ||
|
|
||
| Promiseのオブジェクトを並行して解決し、同じキーと解決された値を持つオブジェクトを返します。 | ||
|
|
||
| ```typescript | ||
| await allKeyed(tasks); | ||
| ``` | ||
|
|
||
| ## 使い方 | ||
|
|
||
| ### `allKeyed(tasks)` | ||
|
|
||
| 複数のPromiseを並行して実行し、位置インデックスではなく名前で結果にアクセスしたい場合に`allKeyed`を使用します。`Promise.all`と似ていますが、配列の代わりにオブジェクトのPromiseを受け取り、結果にキーを保持します。 | ||
|
|
||
| [TC39 `Promise.allKeyed` 提案](https://github.com/tc39/proposal-await-dictionary)に基づいています。 | ||
|
|
||
| ```typescript | ||
| import { allKeyed } from 'es-toolkit/promise'; | ||
|
|
||
| const { user, posts } = await allKeyed({ | ||
| user: fetchUser(), | ||
| posts: fetchPosts(), | ||
| }); | ||
| ``` | ||
|
|
||
| プレーンな値もPromiseと一緒にサポートされています。 | ||
|
|
||
| ```typescript | ||
| const result = await allKeyed({ | ||
| a: Promise.resolve(1), | ||
| b: 2, | ||
| }); | ||
| // { a: 1, b: 2 } | ||
| ``` | ||
|
|
||
| 配列の順序を気にせずに複数のリソースを並行して取得する場合にも便利です。 | ||
|
|
||
| ```typescript | ||
| // Promise.allでは順序を入れ替えると分割代入が静かに壊れます: | ||
| // const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]); | ||
|
|
||
| // allKeyedではキーが明示的なので順序バグがありません: | ||
| const { user, posts } = await allKeyed({ | ||
| user: fetchUser(), | ||
| posts: fetchPosts(), | ||
| }); | ||
| ``` | ||
|
|
||
| #### パラメータ | ||
|
|
||
| - `tasks` (`T`): 並行して解決するPromise(またはプレーンな値)を値として持つオブジェクトです。 | ||
|
|
||
| #### 戻り値 | ||
|
|
||
| (`Promise<{ [K in keyof T]: Awaited<T[K]> }>`): 同じキーと解決された値を持つオブジェクトに解決されるPromiseを返します。 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # allKeyed | ||
|
|
||
| Promise 객체를 동시에 실행하고, 같은 키와 해결된 값을 가진 객체를 반환해요. | ||
|
|
||
| ```typescript | ||
| await allKeyed(tasks); | ||
| ``` | ||
|
|
||
| ## 사용법 | ||
|
|
||
| ### `allKeyed(tasks)` | ||
|
|
||
| 여러 Promise를 병렬로 실행하고, 위치 인덱스가 아닌 이름으로 결과에 접근하고 싶을 때 `allKeyed`를 사용하세요. `Promise.all`과 비슷하지만, 배열 대신 객체의 Promise를 받아서 결과에서 키를 유지해요. | ||
|
|
||
| [TC39 `Promise.allKeyed` 제안](https://github.com/tc39/proposal-await-dictionary)에 기반해요. | ||
|
|
||
| ```typescript | ||
| import { allKeyed } from 'es-toolkit/promise'; | ||
|
|
||
| const { user, posts } = await allKeyed({ | ||
| user: fetchUser(), | ||
| posts: fetchPosts(), | ||
| }); | ||
| ``` | ||
|
|
||
| Promise와 함께 일반 값도 지원해요. | ||
|
|
||
| ```typescript | ||
| const result = await allKeyed({ | ||
| a: Promise.resolve(1), | ||
| b: 2, | ||
| }); | ||
| // { a: 1, b: 2 } | ||
| ``` | ||
|
|
||
| 배열 순서를 신경 쓰지 않고 여러 리소스를 병렬로 가져올 때도 유용해요. | ||
|
|
||
| ```typescript | ||
| // Promise.all은 순서가 바뀌면 구조 분해가 조용히 깨져요: | ||
| // const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]); | ||
|
|
||
| // allKeyed는 키가 명시적이라 순서 버그가 없어요: | ||
| const { user, posts } = await allKeyed({ | ||
| user: fetchUser(), | ||
| posts: fetchPosts(), | ||
| }); | ||
| ``` | ||
|
|
||
| #### 파라미터 | ||
|
|
||
| - `tasks` (`T`): 동시에 실행할 Promise(또는 일반 값)를 값으로 가진 객체예요. | ||
|
|
||
| #### 반환 값 | ||
|
|
||
| (`Promise<{ [K in keyof T]: Awaited<T[K]> }>`): 같은 키와 해결된 값을 가진 객체로 해결되는 Promise를 반환해요. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # allKeyed | ||
|
|
||
| Resolves an object of promises concurrently, returning an object with the same keys and resolved values. | ||
|
|
||
| ```typescript | ||
| await allKeyed(tasks); | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| ### `allKeyed(tasks)` | ||
|
|
||
| Use `allKeyed` when you want to run multiple promises in parallel and access results by name instead of positional indices. Similar to `Promise.all`, but accepts an object of promises and preserves keys in the result. | ||
|
|
||
| Based on the [TC39 `Promise.allKeyed` proposal](https://github.com/tc39/proposal-await-dictionary). | ||
|
|
||
| ```typescript | ||
| import { allKeyed } from 'es-toolkit/promise'; | ||
|
|
||
| const { user, posts } = await allKeyed({ | ||
| user: fetchUser(), | ||
| posts: fetchPosts(), | ||
| }); | ||
| ``` | ||
|
|
||
| Plain values are also supported alongside promises. | ||
|
|
||
| ```typescript | ||
| const result = await allKeyed({ | ||
| a: Promise.resolve(1), | ||
| b: 2, | ||
| }); | ||
| // { a: 1, b: 2 } | ||
| ``` | ||
|
|
||
| It's also useful when you want to fetch multiple resources in parallel without worrying about array ordering. | ||
|
|
||
| ```typescript | ||
| // With Promise.all, swapping the order silently breaks destructuring: | ||
| // const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]); | ||
|
|
||
| // With allKeyed, keys are explicit — no ordering bugs: | ||
| const { user, posts } = await allKeyed({ | ||
| user: fetchUser(), | ||
| posts: fetchPosts(), | ||
| }); | ||
| ``` | ||
|
|
||
| #### Parameters | ||
|
|
||
| - `tasks` (`T`): An object whose values are promises (or plain values) to resolve concurrently. | ||
|
|
||
| #### Returns | ||
|
|
||
| (`Promise<{ [K in keyof T]: Awaited<T[K]> }>`): A promise that resolves to an object with the same keys and resolved values. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # allKeyed | ||
|
|
||
| 并发解析一个 Promise 对象,返回一个具有相同键和解析值的对象。 | ||
|
|
||
| ```typescript | ||
| await allKeyed(tasks); | ||
| ``` | ||
|
|
||
| ## 用法 | ||
|
|
||
| ### `allKeyed(tasks)` | ||
|
|
||
| 当您想要并行运行多个 Promise 并通过名称而不是位置索引访问结果时,可以使用 `allKeyed`。它类似于 `Promise.all`,但接受一个 Promise 对象而不是数组,在结果中保留键。 | ||
|
|
||
| 基于 [TC39 `Promise.allKeyed` 提案](https://github.com/tc39/proposal-await-dictionary)。 | ||
|
|
||
| ```typescript | ||
| import { allKeyed } from 'es-toolkit/promise'; | ||
|
|
||
| const { user, posts } = await allKeyed({ | ||
| user: fetchUser(), | ||
| posts: fetchPosts(), | ||
| }); | ||
| ``` | ||
|
|
||
| 也支持与 Promise 一起使用普通值。 | ||
|
|
||
| ```typescript | ||
| const result = await allKeyed({ | ||
| a: Promise.resolve(1), | ||
| b: 2, | ||
| }); | ||
| // { a: 1, b: 2 } | ||
| ``` | ||
|
|
||
| 当您想要并行获取多个资源而不必担心数组顺序时也很有用。 | ||
|
|
||
| ```typescript | ||
| // 使用 Promise.all 时,交换顺序会静默破坏解构: | ||
| // const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]); | ||
|
|
||
| // 使用 allKeyed 时,键是显式的——没有顺序错误: | ||
| const { user, posts } = await allKeyed({ | ||
| user: fetchUser(), | ||
| posts: fetchPosts(), | ||
| }); | ||
| ``` | ||
|
|
||
| #### 参数 | ||
|
|
||
| - `tasks` (`T`): 一个对象,其值为要并发解析的 Promise(或普通值)。 | ||
|
|
||
| #### 返回值 | ||
|
|
||
| (`Promise<{ [K in keyof T]: Awaited<T[K]> }>`): 返回一个 Promise,解析为具有相同键和解析值的对象。 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { allKeyed } from './allKeyed'; | ||
|
|
||
| describe('allKeyed', () => { | ||
| it('should resolve an object of promises concurrently', async () => { | ||
| const result = await allKeyed({ | ||
| a: Promise.resolve(1), | ||
| b: Promise.resolve('hello'), | ||
| c: Promise.resolve(true), | ||
| }); | ||
|
|
||
| expect(result).toEqual({ a: 1, b: 'hello', c: true }); | ||
| }); | ||
|
|
||
| it('should handle plain (non-promise) values', async () => { | ||
| const result = await allKeyed({ | ||
| a: 1, | ||
| b: 'hello', | ||
| }); | ||
|
|
||
| expect(result).toEqual({ a: 1, b: 'hello' }); | ||
| }); | ||
|
|
||
| it('should handle a mix of promises and plain values', async () => { | ||
| const result = await allKeyed({ | ||
| a: Promise.resolve(1), | ||
| b: 'plain', | ||
| }); | ||
|
|
||
| expect(result).toEqual({ a: 1, b: 'plain' }); | ||
| }); | ||
|
|
||
| it('should return an empty object for empty input', async () => { | ||
| const result = await allKeyed({}); | ||
|
|
||
| expect(result).toEqual({}); | ||
| }); | ||
|
|
||
| it('should reject if any promise rejects', async () => { | ||
| await expect( | ||
| allKeyed({ | ||
| a: Promise.resolve(1), | ||
| b: Promise.reject(new Error('fail')), | ||
| }) | ||
| ).rejects.toThrow('fail'); | ||
| }); | ||
|
|
||
| it('should resolve promises concurrently, not sequentially', async () => { | ||
| const start = Date.now(); | ||
|
|
||
| await allKeyed({ | ||
| a: new Promise(resolve => setTimeout(() => resolve('a'), 50)), | ||
| b: new Promise(resolve => setTimeout(() => resolve('b'), 50)), | ||
| }); | ||
|
|
||
| const elapsed = Date.now() - start; | ||
|
|
||
| // If run sequentially, would take ~100ms. Concurrently should be ~50ms. | ||
| expect(elapsed).toBeLessThan(90); | ||
| }); | ||
|
Comment on lines
+48
to
+60
|
||
|
|
||
| it('should preserve key-value associations', async () => { | ||
| const result = await allKeyed({ | ||
| first: Promise.resolve(1), | ||
| second: Promise.resolve(2), | ||
| third: Promise.resolve(3), | ||
| }); | ||
|
|
||
| expect(result.first).toBe(1); | ||
| expect(result.second).toBe(2); | ||
| expect(result.third).toBe(3); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /** | ||
| * Resolves an object of promises concurrently, returning an object with the same keys and resolved values. | ||
| * | ||
| * Similar to `Promise.all`, but accepts an object of promises instead of an array, | ||
| * preserving the keys in the result. This makes it easy to destructure the resolved values | ||
| * by name instead of relying on positional indices. | ||
| * | ||
| * Based on the [TC39 `Promise.allKeyed` proposal](https://github.com/tc39/proposal-await-dictionary). | ||
| * | ||
| * @template T - A record type where each value is a promise or a value. | ||
| * @param {T} tasks - An object whose values are promises (or plain values) to resolve concurrently. | ||
| * @returns {Promise<{ [K in keyof T]: Awaited<T[K]> }>} A promise that resolves to an object with the same keys and resolved values. | ||
| * | ||
| * @example | ||
| * const { user, posts } = await allKeyed({ | ||
| * user: fetchUser(), | ||
| * posts: fetchPosts(), | ||
| * }); | ||
| * | ||
| * @example | ||
| * // Plain values are also supported | ||
| * const result = await allKeyed({ | ||
| * a: Promise.resolve(1), | ||
| * b: 2, | ||
| * }); | ||
| * // { a: 1, b: 2 } | ||
| */ | ||
| export async function allKeyed<T extends Record<string, unknown>>( | ||
| tasks: T | ||
| ): Promise<{ [K in keyof T]: Awaited<T[K]> }> { | ||
| const keys = Object.keys(tasks) as Array<keyof T>; | ||
| const values = await Promise.all(keys.map(key => tasks[key])); | ||
|
|
||
| const result = {} as { [K in keyof T]: Awaited<T[K]> }; | ||
|
|
||
| for (let i = 0; i < keys.length; i++) { | ||
| result[keys[i]] = values[i] as Awaited<T[(typeof keys)[number]]>; | ||
| } | ||
|
Comment on lines
+34
to
+38
|
||
|
|
||
| return result; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the library already has prototype-pollution hardening elsewhere (e.g., skipping/handling
__proto__), it would be good to add a test case where the input contains a__proto__key and assert it becomes an own enumerable property (and does not alter the returned object's prototype). This will prevent regressions once the implementation is hardened.