From d0a868b0adc1612d394b9b21dfcd46dabbab3c51 Mon Sep 17 00:00:00 2001 From: unional Date: Mon, 28 Sep 2026 01:28:25 -0700 Subject: [PATCH 1/2] docs(changeset): record that predicates distribute over unions in v8 A 7.x predicate checked a union as a whole; the v8 predicates check each member by default, so IsString<'a' | 1> went from false to boolean. No changeset recorded it. The results were compared against the 7.6.2 declarations predicate by predicate. Co-Authored-By: Claude Opus 5.5 --- .changeset/predicates-distribute.md | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .changeset/predicates-distribute.md diff --git a/.changeset/predicates-distribute.md b/.changeset/predicates-distribute.md new file mode 100644 index 0000000000..8b4abd7ce7 --- /dev/null +++ b/.changeset/predicates-distribute.md @@ -0,0 +1,31 @@ +--- +'type-plus': major +--- + +The type predicates distribute over a union by default. + +A 7.x predicate checked a union as a whole, so a union with any member that failed answered `false`. +A v8 predicate checks each member and combines the answers. A union whose members pass and fail now answers `boolean`: + +```ts +type R = IsString<'a' | 1> // 7.x: false, 8.0: boolean +type R = IsTrue // 7.x: false, 8.0: boolean +type R = IsNotString<'a' | 1> // 7.x: true, 8.0: boolean +``` + +The filter form keeps the members that pass. `IsString<'a' | 1, { selection: 'filter' }>` is `'a'`, where the 7.x `StringType<'a' | 1>` was `never`. + +This applies to `IsArray`, `IsBigint`, `IsBoolean`, `IsFalse`, `IsFunction`, `IsInteger`, `IsNull`, `IsNumber`, `IsNumeric`, `IsObject`, `IsString`, `IsSymbol`, `IsTrue`, `IsTuple`, `IsUndefined`, `IsVoid`, and the `IsNot*` negation of each. +`IsPositive`, `IsNegative`, `IsNotPositive` and `IsNotNegative` already distributed in 7.x and do not change. + +A result tested with `extends true` behaves as before, because `boolean` does not extend `true`. +A result tested with `extends false`, or compared with `IsEqual`, changes. + +Migration: pass `{ distributive: false }` to keep the 7.x result. + +```ts +type R = IsString<'a' | 1, { distributive: false }> // false +type R = IsString<'a' | 1, { distributive: false; selection: 'filter' }> // never +``` + +See [Predicates distribute over unions](https://cyberuni.github.io/type-plus/guides/migrating-to-v8/#predicates-distribute-over-unions) in the migration guide. From fb30cd4ff51678fa2991834962cd122f5d3cf20c Mon Sep 17 00:00:00 2001 From: unional Date: Mon, 28 Sep 2026 01:32:08 -0700 Subject: [PATCH 2/2] chore(changeset): drop the duplicate distribution changeset predicates-distribute.md records the same change in more detail. Co-Authored-By: Claude Opus 5.5 --- .../predicates-distribute-over-unions.md | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 .changeset/predicates-distribute-over-unions.md diff --git a/.changeset/predicates-distribute-over-unions.md b/.changeset/predicates-distribute-over-unions.md deleted file mode 100644 index 5fb5672782..0000000000 --- a/.changeset/predicates-distribute-over-unions.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'type-plus': major ---- - -The `IsXxx` and `IsNotXxx` predicates that take the `distributive` option now distribute over unions by default. - -A 7.x predicate checked a union as a whole. -A v8 predicate checks each member and combines the answers, -so a union with members that pass and members that fail answers `boolean`: - -```ts -type R = IsString<'a' | 1> // 7.x: false, 8.0: boolean -type R = IsTrue // 7.x: false, 8.0: boolean -``` - -Pass `{ distributive: false }` to keep the 7.x result: - -```ts -type R = IsString<'a' | 1, { distributive: false }> // false -type R = IsTrue // false -``` - -A result used as `extends true` needs no change, because `boolean extends true` is still false. -A result compared with `extends false` does change.