From ef14440d4e53805f7acbb118aeea6e1926962f01 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Wed, 3 Jul 2024 20:18:02 -0400 Subject: [PATCH] fix(types): improve the `isArray` return type for `unknown` input type (#72) --- src/async/all.ts | 16 +++++------ src/mod.ts | 2 ++ src/typed/isArray.ts | 13 +++++++-- src/types.ts | 25 +++++++++++++++++ tests/typed/isArray.test-d.ts | 52 +++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 src/types.ts create mode 100644 tests/typed/isArray.test-d.ts diff --git a/src/async/all.ts b/src/async/all.ts index c291390b..b0b07902 100644 --- a/src/async/all.ts +++ b/src/async/all.ts @@ -42,11 +42,11 @@ export async function all>>( promises: T, ): Promise<{ [K in keyof T]: Awaited }> -export async function all< - T extends Record> | Promise[], ->(promises: T) { +export async function all( + promises: Record> | Promise[], +): Promise { const entries = isArray(promises) - ? promises.map(p => [null, p] as [null, Promise]) + ? promises.map(p => [null, p] as const) : Object.entries(promises) const results = await Promise.all( @@ -63,16 +63,14 @@ export async function all< } if (isArray(promises)) { - return results.map(r => r.result) as T extends Promise[] - ? PromiseValues - : unknown + return results.map(r => r.result) } return results.reduce( (acc, item) => { - acc[item.key as keyof T] = item.result + acc[item.key!] = item.result return acc }, - {} as { [K in keyof T]: Awaited }, + {} as Record, ) } diff --git a/src/mod.ts b/src/mod.ts index f48219cf..13272078 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -107,3 +107,5 @@ export * from './typed/isPrimitive.ts' export * from './typed/isPromise.ts' export * from './typed/isString.ts' export * from './typed/isSymbol.ts' + +export * from './types' diff --git a/src/typed/isArray.ts b/src/typed/isArray.ts index 9899cd9b..c40f4338 100644 --- a/src/typed/isArray.ts +++ b/src/typed/isArray.ts @@ -1,2 +1,11 @@ -export const isArray: (value: unknown) => value is readonly any[] = - Array.isArray +import type { ExtractNotAny } from 'radashi' + +export const isArray = Array.isArray as ( + value: Input, +) => value is readonly any[] extends ExtractNotAny + ? Extract + : any[] extends ExtractNotAny + ? Extract + : unknown[] extends Input + ? unknown[] + : never diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..52e3f02f --- /dev/null +++ b/src/types.ts @@ -0,0 +1,25 @@ +declare const any: unique symbol + +/** + * The `Any` class does not exist at runtime. It's used in type + * definitions to detect an `any` type. + * + * ```ts + * type IsAny = [T] extends [Any] ? 'is any' : 'is not any' + * ``` + */ +export declare class Any { + private any: typeof any +} + +/** + * Extracts `T` if `T` is not `any`, otherwise `never`. + * + * ```ts + * type A = ExtractNotAny + * // ^? never + * type B = ExtractNotAny + * // ^? string + * ``` + */ +export type ExtractNotAny = Extract<[T] extends [Any] ? never : T, U> diff --git a/tests/typed/isArray.test-d.ts b/tests/typed/isArray.test-d.ts new file mode 100644 index 00000000..33431155 --- /dev/null +++ b/tests/typed/isArray.test-d.ts @@ -0,0 +1,52 @@ +import * as _ from 'radashi' + +describe('isArray return type', () => { + test('value is any', () => { + const value = {} as any + if (_.isArray(value)) { + expectTypeOf(value).toEqualTypeOf() + } else { + expectTypeOf(value).toEqualTypeOf() + } + }) + test('value is unknown', () => { + const value = {} as unknown + if (_.isArray(value)) { + expectTypeOf(value).toEqualTypeOf() + } else { + expectTypeOf(value).toEqualTypeOf() + } + }) + test('value is string', () => { + const value = {} as string + if (_.isArray(value)) { + expectTypeOf(value).toEqualTypeOf() + } else { + expectTypeOf(value).toEqualTypeOf() + } + }) + test('value is string or ReadonlyMap', () => { + const value = {} as string | readonly string[] + if (_.isArray(value)) { + expectTypeOf(value).toEqualTypeOf() + } else { + expectTypeOf(value).toEqualTypeOf() + } + }) + test('value is string, ReadonlyMap, or Map', () => { + const value = {} as string | readonly string[] | string[] + if (_.isArray(value)) { + expectTypeOf(value).toEqualTypeOf() + } else { + expectTypeOf(value).toEqualTypeOf() + } + }) + test('value is string or Map', () => { + const value = {} as string | string[] + if (_.isArray(value)) { + expectTypeOf(value).toEqualTypeOf() + } else { + expectTypeOf(value).toEqualTypeOf() + } + }) +})