Skip to content

Commit 12fea78

Browse files
committed
👍 isIntersectionOf allows anything other than isObjectOf
Fixes #68
1 parent 38607b0 commit 12fea78

File tree

4 files changed

+119
-17
lines changed

4 files changed

+119
-17
lines changed

__snapshots__/is_test.ts.snap

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,17 @@ snapshot[`isIntersectionOf<T> > returns properly named function 1`] = `
229229
})"
230230
`;
231231
232+
snapshot[`isIntersectionOf<T> > returns properly named function 2`] = `
233+
"isString"
234+
`;
235+
236+
snapshot[`isIntersectionOf<T> > returns properly named function 3`] = `
237+
"isIntersectionOf([
238+
isFunction,
239+
isObjectOf({b: isString})
240+
])"
241+
`;
242+
232243
snapshot[`isRequiredOf<T> > returns properly named function 1`] = `
233244
"isObjectOf({
234245
a: isNumber,

_typeutil.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ export type FlatType<T> = T extends Record<PropertyKey, unknown>
22
? { [K in keyof T]: FlatType<T[K]> }
33
: T;
44

5-
export type UnionToIntersection<U> =
6-
(U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void)
7-
? I
8-
: never;
5+
export type TupleToIntersection<T> = T extends readonly [] ? never
6+
: T extends readonly [infer U] ? U
7+
: T extends readonly [infer U, ...infer R] ? U & TupleToIntersection<R>
8+
: never;
99

1010
export type Writable<T> = { -readonly [P in keyof T]: T[P] };

is.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { FlatType, UnionToIntersection, Writable } from "./_typeutil.ts";
1+
import type { FlatType, TupleToIntersection, Writable } from "./_typeutil.ts";
22
import {
33
type GetMetadata,
44
getMetadata,
@@ -1430,17 +1430,51 @@ export function isIntersectionOf<
14301430
],
14311431
>(
14321432
preds: T,
1433-
): Predicate<IntersectionOf<T>> & WithMetadata<IsObjectOfMetadata> {
1433+
): Predicate<IntersectionOf<T>> & WithMetadata<IsObjectOfMetadata>;
1434+
export function isIntersectionOf<
1435+
T extends readonly [Predicate<unknown>],
1436+
>(
1437+
preds: T,
1438+
): T[0];
1439+
export function isIntersectionOf<
1440+
T extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
1441+
>(
1442+
preds: T,
1443+
): Predicate<IntersectionOf<T>> & WithMetadata<IsIntersectionOfMetadata>;
1444+
export function isIntersectionOf<
1445+
T extends readonly [Predicate<unknown>, ...Predicate<unknown>[]],
1446+
>(
1447+
preds: T,
1448+
):
1449+
| Predicate<unknown>
1450+
| Predicate<IntersectionOf<T>>
1451+
& WithMetadata<IsObjectOfMetadata | IsIntersectionOfMetadata> {
14341452
const predObj = {};
1435-
preds.forEach((pred) => {
1436-
Object.assign(predObj, getPredicateFactoryMetadata(pred).args[0]);
1453+
const restPreds = preds.filter((pred) => {
1454+
const meta = getMetadata(pred);
1455+
if ((meta as IsObjectOfMetadata)?.name !== "isObjectOf") {
1456+
return true;
1457+
}
1458+
Object.assign(predObj, (meta as IsObjectOfMetadata).args[0]);
14371459
});
1438-
return isObjectOf(predObj) as
1439-
& Predicate<IntersectionOf<T>>
1440-
& WithMetadata<IsObjectOfMetadata>;
1460+
if (restPreds.length < preds.length) {
1461+
restPreds.push(isObjectOf(predObj));
1462+
}
1463+
if (restPreds.length === 1) {
1464+
return restPreds[0];
1465+
}
1466+
return setPredicateFactoryMetadata(
1467+
(x: unknown): x is IntersectionOf<T> => restPreds.every((pred) => pred(x)),
1468+
{ name: "isIntersectionOf", args: [preds] },
1469+
);
14411470
}
14421471

1443-
type IntersectionOf<T> = UnionToIntersection<UnionOf<T>>;
1472+
type IntersectionOf<T> = TupleToIntersection<TupleOf<T>>;
1473+
1474+
type IsIntersectionOfMetadata = {
1475+
name: "isIntersectionOf";
1476+
args: Parameters<typeof isIntersectionOf>;
1477+
};
14441478

14451479
/**
14461480
* Return a type predicate function that returns `true` if the type of `x` is `Required<ObjectOf<T>>`.
@@ -1628,7 +1662,7 @@ export function isAllOf<
16281662
return isIntersectionOf(preds);
16291663
}
16301664

1631-
type AllOf<T> = UnionToIntersection<OneOf<T>>;
1665+
type AllOf<T> = IntersectionOf<T>;
16321666

16331667
export const is = {
16341668
AllOf: isAllOf,

is_test.ts

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1477,24 +1477,62 @@ Deno.test("isIntersectionOf<T>", async (t) => {
14771477
isObjectOf({ a: isNumber }),
14781478
isObjectOf({ b: isString }),
14791479
]).name,
1480+
"Should return `isObjectOf`, if all predicates that",
1481+
);
1482+
await assertSnapshot(
1483+
t,
1484+
isIntersectionOf([
1485+
isString,
1486+
]).name,
1487+
"Should return as is, if there is only one predicate",
1488+
);
1489+
await assertSnapshot(
1490+
t,
1491+
isIntersectionOf([
1492+
isFunction,
1493+
isObjectOf({ b: isString }),
1494+
]).name,
14801495
);
14811496
});
14821497
await t.step("returns proper type predicate", () => {
1483-
const preds = [
1498+
const objPreds = [
14841499
isObjectOf({ a: isNumber }),
14851500
isObjectOf({ b: isString }),
14861501
] as const;
1502+
const funcPreds = [
1503+
isFunction,
1504+
isObjectOf({ b: isString }),
1505+
] as const;
14871506
const a: unknown = { a: 0, b: "a" };
1488-
if (isIntersectionOf(preds)(a)) {
1507+
if (isIntersectionOf(objPreds)(a)) {
14891508
assertType<Equal<typeof a, { a: number } & { b: string }>>(true);
14901509
}
1510+
if (isIntersectionOf([isString])(a)) {
1511+
assertType<Equal<typeof a, string>>(true);
1512+
}
1513+
if (isIntersectionOf(funcPreds)(a)) {
1514+
assertType<
1515+
Equal<
1516+
typeof a,
1517+
& ((...args: unknown[]) => unknown)
1518+
& { b: string }
1519+
>
1520+
>(true);
1521+
}
14911522
});
14921523
await t.step("returns true on all of T", () => {
1493-
const preds = [
1524+
const objPreds = [
14941525
isObjectOf({ a: isNumber }),
14951526
isObjectOf({ b: isString }),
14961527
] as const;
1497-
assertEquals(isIntersectionOf(preds)({ a: 0, b: "a" }), true);
1528+
const funcPreds = [
1529+
isFunction,
1530+
isObjectOf({ b: isString }),
1531+
] as const;
1532+
const f = Object.assign(() => void 0, { b: "a" });
1533+
assertEquals(isIntersectionOf(objPreds)({ a: 0, b: "a" }), true);
1534+
assertEquals(isIntersectionOf([isString])("a"), true);
1535+
assertEquals(isIntersectionOf(funcPreds)(f), true);
14981536
});
14991537
await t.step("returns false on non of T", async (t) => {
15001538
const preds = [
@@ -1515,6 +1553,25 @@ Deno.test("isIntersectionOf<T>", async (t) => {
15151553
excludeExamples: ["record"],
15161554
});
15171555
});
1556+
await t.step("returns false on non of T with any predicates", async (t) => {
1557+
const preds = [
1558+
isFunction,
1559+
isObjectOf({ b: isString }),
1560+
] as const;
1561+
assertEquals(
1562+
isIntersectionOf(preds)({ b: "a" }),
1563+
false,
1564+
"Not a function object",
1565+
);
1566+
assertEquals(
1567+
isIntersectionOf(preds)(() => void 0),
1568+
false,
1569+
"Some properties does not exists in Function object",
1570+
);
1571+
await testWithExamples(t, isIntersectionOf(preds), {
1572+
excludeExamples: ["record"],
1573+
});
1574+
});
15181575
});
15191576

15201577
Deno.test("isRequiredOf<T>", async (t) => {

0 commit comments

Comments
 (0)