Skip to content

Commit 9257535

Browse files
committed
fix(types): handle tuples in isArray return type
1 parent c47fd3d commit 9257535

5 files changed

Lines changed: 74 additions & 38 deletions

File tree

src/typed/isArray.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import type { ExtractNotAny } from 'radashi'
1+
import type { StrictExtract } from 'radashi'
22

33
export const isArray = Array.isArray as <Input>(
44
value: Input,
5-
) => value is readonly any[] extends ExtractNotAny<Input, readonly any[]>
6-
? Extract<Input, readonly any[]>
7-
: any[] extends ExtractNotAny<Input, any[]>
8-
? Extract<Input, any[]>
9-
: unknown[] extends Input
10-
? unknown[]
11-
: never
5+
) => value is ExtractArray<Input>
6+
7+
export type ExtractArray<Input> = Input extends any
8+
? [StrictExtract<Input, readonly any[]>] extends [readonly any[]]
9+
? Extract<Input, readonly any[]>
10+
: [StrictExtract<Input, any[]>] extends [any[]]
11+
? Extract<Input, any[]>
12+
: unknown[] extends Input
13+
? unknown[]
14+
: never
15+
: never

src/typed/isMap.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1-
import { type ExtractNotAny, isTagged } from 'radashi'
1+
import { isTagged, type StrictExtract } from 'radashi'
22

3-
export function isMap<Input>(
4-
value: Input,
5-
): value is ReadonlyMap<any, any> extends ExtractNotAny<
6-
Input,
7-
ReadonlyMap<any, any>
8-
>
9-
? Extract<Input, ReadonlyMap<unknown, unknown>>
10-
: Map<any, any> extends ExtractNotAny<Input, Map<any, any>>
11-
? Extract<Input, Map<unknown, unknown>>
12-
: Map<unknown, unknown> extends Input
13-
? Map<unknown, unknown>
14-
: never
15-
16-
export function isMap(value: unknown): boolean {
3+
export function isMap<Input>(value: Input): value is ExtractMap<Input> {
174
return isTagged(value, '[object Map]')
185
}
6+
7+
/**
8+
* An absurdly complicated but accurate type for extracting Map types.
9+
*/
10+
export type ExtractMap<Input> = Input extends any
11+
? [StrictExtract<Input, ReadonlyMap<unknown, unknown>>] extends [
12+
ReadonlyMap<unknown, unknown>,
13+
]
14+
? Extract<Input, ReadonlyMap<unknown, unknown>>
15+
: [StrictExtract<Input, Map<unknown, unknown>>] extends [
16+
Map<unknown, unknown>,
17+
]
18+
? Extract<Input, Map<unknown, unknown>>
19+
: Map<unknown, unknown> extends Input
20+
? Map<unknown, unknown>
21+
: never
22+
: never

src/typed/isSet.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { type ExtractNotAny, isTagged } from 'radashi'
1+
import { isTagged, type StrictExtract } from 'radashi'
22

3-
export function isSet<Input>(
4-
value: Input,
5-
): value is ReadonlySet<any> extends ExtractNotAny<Input, ReadonlySet<any>>
6-
? Extract<Input, ReadonlySet<unknown>>
7-
: Set<any> extends ExtractNotAny<Input, Set<any>>
8-
? Extract<Input, Set<unknown>>
9-
: Set<unknown> extends Input
10-
? Set<unknown>
11-
: never
12-
13-
export function isSet(value: unknown): boolean {
3+
export function isSet<Input>(value: Input): value is ExtractSet<Input> {
144
return isTagged(value, '[object Set]')
155
}
6+
7+
export type ExtractSet<Input> = Input extends any
8+
? [StrictExtract<Input, ReadonlySet<unknown>>] extends [ReadonlySet<unknown>]
9+
? Extract<Input, ReadonlySet<unknown>>
10+
: [StrictExtract<Input, Set<unknown>>] extends [Set<unknown>]
11+
? Extract<Input, Set<unknown>>
12+
: Set<unknown> extends Input
13+
? Set<unknown>
14+
: never
15+
: never

src/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,15 @@ export declare class Any {
2323
* ```
2424
*/
2525
export type ExtractNotAny<T, U> = Extract<[T] extends [Any] ? never : T, U>
26+
27+
export type SwitchAny<T, U> = [T] extends [Any] ? U : T
28+
export type SwitchNever<T, U> = [T] extends [never] ? U : T
29+
30+
/**
31+
* Extract types in `T` that are assignable to `U`. Coerce `any` and
32+
* `never` types to unknown.
33+
*/
34+
export type StrictExtract<T, U> = SwitchNever<
35+
Extract<SwitchAny<T, unknown>, U>,
36+
unknown
37+
>

tests/typed/isArray.test-d.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ describe('isArray return type', () => {
1717
expectTypeOf(value).toEqualTypeOf<unknown>()
1818
}
1919
})
20+
test('value is never', () => {
21+
const value = {} as never
22+
if (_.isArray(value)) {
23+
expectTypeOf(value).toEqualTypeOf<never>()
24+
} else {
25+
expectTypeOf(value).toEqualTypeOf<never>()
26+
}
27+
})
2028
test('value is string', () => {
2129
const value = {} as string
2230
if (_.isArray(value)) {
@@ -25,28 +33,36 @@ describe('isArray return type', () => {
2533
expectTypeOf(value).toEqualTypeOf<string>()
2634
}
2735
})
28-
test('value is string or ReadonlyMap', () => {
36+
test('value is string or readonly string[]', () => {
2937
const value = {} as string | readonly string[]
3038
if (_.isArray(value)) {
3139
expectTypeOf(value).toEqualTypeOf<readonly string[]>()
3240
} else {
3341
expectTypeOf(value).toEqualTypeOf<string>()
3442
}
3543
})
36-
test('value is string, ReadonlyMap, or Map', () => {
44+
test('value is string, readonly string[], or string[]', () => {
3745
const value = {} as string | readonly string[] | string[]
3846
if (_.isArray(value)) {
3947
expectTypeOf(value).toEqualTypeOf<readonly string[] | string[]>()
4048
} else {
4149
expectTypeOf(value).toEqualTypeOf<string>()
4250
}
4351
})
44-
test('value is string or Map', () => {
52+
test('value is string or string[]', () => {
4553
const value = {} as string | string[]
4654
if (_.isArray(value)) {
4755
expectTypeOf(value).toEqualTypeOf<string[]>()
4856
} else {
4957
expectTypeOf(value).toEqualTypeOf<string>()
5058
}
5159
})
60+
test('value is readonly [number, number] | number', () => {
61+
const value = {} as readonly [number, number] | number
62+
if (_.isArray(value)) {
63+
expectTypeOf(value).toEqualTypeOf<readonly [number, number]>()
64+
} else {
65+
expectTypeOf(value).toEqualTypeOf<number>()
66+
}
67+
})
5268
})

0 commit comments

Comments
 (0)