Repository: marpple/FxTS (ts)

Issue:
Type Inference issue `isArray`?
# Bug Report

<!--
  Please fill in each section completely. Thank you!
-->

### 💻 Code

<!-- Please post the relevant code sample here as well-->
<!-- How to reproduce it -->

```ts
type Profile = {
  thumbnail: string;
  nickname?: string;
  id: string;
};

type UserState = {
  profiles: Profile[];
};

const extractId = (profile: Profile) => profile.id;

// from https://github.com/marpple/FxTS/blob/main/src/isArray.ts (except for type)
// This is example :)
const isArrayCompatible = <T>(
  input: T
): input is T & (T extends readonly unknown[] ? T : never) =>
  Array.isArray(input);

// The `map` function throws an error at compile because the return type of `isArray` might be `unknown[]`.
const actualBehavior = (state: UserState) =>
  pipe(prop("profiles", state), when(isArray, map(extractId)));

const expectedBehavior = (state: UserState) =>
  pipe(prop("profiles", state), when(isArrayCompatible, map(extractId)));

```

### 🙁 Actual behavior

`isArray`'s return type has `unknown[]`, so `map` function cause error with `no matching overload`

### 🙂 Expected behavior

If `isArray` passes, the type should be inferred correctly.

---

### Version Information

<!-- Please let me know your environment -->

- Operating system: MacOS 15.3.2
- Typescript: ^5.8.3
- Nodejs: v20.10.0
- @fxts/core: ^1.8.0

Pull request description:
fix: use Extract type for preventing type widening in isArray function
Fixes #317 

**This changes that use `Extract` utility type instead of instersection type prevent unintented type widening.**
In the previous implemetation generic type `T` has been sometimes interpreted as `unknown[]` because of tendency to widen types.

**All tests were passed.**




Patch:
diff --git a/src/isArray.ts b/src/isArray.ts
index af728e4f..4fd42483 100644
--- a/src/isArray.ts
+++ b/src/isArray.ts
@@ -7,7 +7,8 @@
  * isArray(2); // false
  * ```
  */
-const isArray = <T>(input: T): input is T & (unknown[] | Readonly<unknown[]>) =>
-  Array.isArray(input);
+const isArray = <T>(
+  input: T,
+): input is Extract<T, unknown[] | Readonly<unknown[]>> => Array.isArray(input);
 
 export default isArray;


Test patch:
diff --git a/type-check/isArray.test.ts b/type-check/isArray.test.ts
index 2549f7dc..952bcfe3 100644
--- a/type-check/isArray.test.ts
+++ b/type-check/isArray.test.ts
@@ -1,4 +1,4 @@
-import { isArray, negate, pipe, throwIf } from "../src";
+import { isArray, map, negate, pipe, prop, throwIf, when } from "../src";
 import * as Test from "../src/types/Test";
 
 const { checks, check } = Test;
@@ -18,9 +18,27 @@ const res4 = pipe(
   throwIf(negate(isArray)),
 );
 
+type Profile = {
+  thumbnail: string;
+  nickname?: string;
+  id: string;
+};
+
+type UserState = {
+  profiles: Profile[];
+};
+
+declare const state: UserState;
+
+const res5 = pipe(prop("profiles", state), throwIf(negate(isArray)));
+
 checks([
   check<typeof res1, boolean, Test.Pass>(),
   check<typeof res2, boolean, Test.Pass>(),
   check<typeof res3, readonly [1, 2, 3], Test.Pass>(),
   check<typeof res4, number[], Test.Pass>(),
+
+  // this type test prove #317 issue is resolved
+  // isArray type guard infer generic type `T` to `Profile[]` type instead of `unknown[]` type
+  check<typeof res5, Profile[], Test.Pass>(),
 ]);