From c77a28b9b50a42b1bfb1c9a90a545b33dd10a85e Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 19:48:21 +0900 Subject: [PATCH 01/31] :muscle: Extract type utilities to `_typeutil.ts` --- _typeutil.ts | 8 ++++++++ is.ts | 9 +-------- 2 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 _typeutil.ts diff --git a/_typeutil.ts b/_typeutil.ts new file mode 100644 index 0000000..1728384 --- /dev/null +++ b/_typeutil.ts @@ -0,0 +1,8 @@ +export type FlatType = T extends Record + ? { [K in keyof T]: FlatType } + : T; + +export type UnionToIntersection = + (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) + ? I + : never; diff --git a/is.ts b/is.ts index a7ea3ee..d939f78 100644 --- a/is.ts +++ b/is.ts @@ -1,3 +1,4 @@ +import type { FlatType, UnionToIntersection } from "./_typeutil.ts"; import { inspect } from "./inspect.ts"; /** @@ -731,10 +732,6 @@ export function isMapOf( ); } -type FlatType = T extends RecordOf - ? { [K in keyof T]: FlatType } - : T; - type OptionalPredicateKeys> = { [K in keyof T]: T[K] extends OptionalPredicate ? K : never; }[keyof T]; @@ -1134,10 +1131,6 @@ export function isOneOf< ); } -type UnionToIntersection = - (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) - ? I - : never; export type AllOf = UnionToIntersection>; /** From 34d2a04b06604b4cd017390d8aa11a5a1b6ae533 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 19:52:05 +0900 Subject: [PATCH 02/31] :boom: Remove some internal types from the public --- is.ts | 84 ++++++++---------------------------------------------- is_test.ts | 53 ---------------------------------- 2 files changed, 12 insertions(+), 125 deletions(-) diff --git a/is.ts b/is.ts index d939f78..2722017 100644 --- a/is.ts +++ b/is.ts @@ -233,33 +233,11 @@ export function isSetOf( ); } -/** - * Tuple type of types that are predicated by an array of predicate functions. - * - * ```ts - * import { is, TupleOf } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * type A = TupleOf; - * // Above is equivalent to the following type - * // type A = [string, number]; - * ``` - */ -export type TupleOf = { +type TupleOf = { -readonly [P in keyof T]: T[P] extends Predicate ? U : never; }; -/** - * Readonly tuple type of types that are predicated by an array of predicate functions. - * - * ```ts - * import { is, ReadonlyTupleOf } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * type A = ReadonlyTupleOf; - * // Above is equivalent to the following type - * // type A = readonly [string, number]; - * ``` - */ -export type ReadonlyTupleOf = { +type ReadonlyTupleOf = { [P in keyof T]: T[P] extends Predicate ? U : never; }; @@ -454,36 +432,14 @@ export function isReadonlyTupleOf< } } -/** - * Uniform tuple type of types that are predicated by a predicate function and the length is `N`. - * - * ```ts - * import { is, UniformTupleOf } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * type A = UniformTupleOf; - * // Above is equivalent to the following type - * // type A = [number, number, number, number, number]; - * ``` - */ // https://stackoverflow.com/a/71700658/1273406 -export type UniformTupleOf< +type UniformTupleOf< T, N extends number, R extends readonly T[] = [], > = R["length"] extends N ? R : UniformTupleOf; -/** - * Readonly uniform tuple type of types that are predicated by a predicate function `T` and the length is `N`. - * - * ```ts - * import { is, ReadonlyUniformTupleOf } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * type A = ReadonlyUniformTupleOf; - * // Above is equivalent to the following type - * // type A = readonly [number, number, number, number, number]; - * ``` - */ -export type ReadonlyUniformTupleOf< +type ReadonlyUniformTupleOf< T, N extends number, R extends readonly T[] = [], @@ -581,11 +537,6 @@ export function isReadonlyUniformTupleOf( ); } -/** - * Synonym of `Record` - */ -export type RecordOf = Record; - /** * Return `true` if the type of `x` is `Record`. * @@ -732,22 +683,11 @@ export function isMapOf( ); } -type OptionalPredicateKeys> = { +type OptionalPredicateKeys> = { [K in keyof T]: T[K] extends OptionalPredicate ? K : never; }[keyof T]; -/** - * Object types that are predicated by predicate functions in the object `T`. - * - * ```ts - * import { is, ObjectOf } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * type A = ObjectOf<{ a: typeof is.Number, b: typeof is.String }>; - * // Above is equivalent to the following type - * // type A = { a: number; b: string }; - * ``` - */ -export type ObjectOf>> = FlatType< +type ObjectOf>> = FlatType< & { [K in Exclude>]: T[K] extends Predicate ? U : never; @@ -801,7 +741,7 @@ export type ObjectOf>> = FlatType< * ``` */ export function isObjectOf< - T extends RecordOf>, + T extends Record>, >( predObj: T, { strict }: { strict?: boolean } = {}, @@ -817,7 +757,7 @@ export function isObjectOf< } function isObjectOfLoose< - T extends RecordOf>, + T extends Record>, >( predObj: T, ): Predicate> { @@ -831,7 +771,7 @@ function isObjectOfLoose< } function isObjectOfStrict< - T extends RecordOf>, + T extends Record>, >( predObj: T, ): Predicate> { @@ -1081,7 +1021,7 @@ export function isLiteralOneOf( ); } -export type OneOf = T extends readonly [Predicate, ...infer R] +type OneOf = T extends readonly [Predicate, ...infer R] ? U | OneOf : never; @@ -1131,7 +1071,7 @@ export function isOneOf< ); } -export type AllOf = UnionToIntersection>; +type AllOf = UnionToIntersection>; /** * Return a type predicate function that returns `true` if the type of `x` is `AllOf`. @@ -1185,7 +1125,7 @@ export function isAllOf< ); } -export type OptionalPredicate = Predicate & { +type OptionalPredicate = Predicate & { optional: true; }; diff --git a/is_test.ts b/is_test.ts index d6d2244..287075b 100644 --- a/is_test.ts +++ b/is_test.ts @@ -40,13 +40,8 @@ import is, { isUndefined, isUniformTupleOf, isUnknown, - ObjectOf, Predicate, PredicateType, - ReadonlyTupleOf, - ReadonlyUniformTupleOf, - TupleOf, - UniformTupleOf, } from "./is.ts"; // It seems 'IsExact' in deno_std is false positive so use `Equal` in type-challenges @@ -252,24 +247,6 @@ Deno.test("isSetOf", async (t) => { }); }); -Deno.test("TupleOf", () => { - assertType< - Equal< - TupleOf, - [string, number] - > - >(true); -}); - -Deno.test("ReadonlyTupleOf", () => { - assertType< - Equal< - ReadonlyTupleOf, - readonly [string, number] - > - >(true); -}); - Deno.test("isTupleOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot( @@ -482,27 +459,6 @@ Deno.test("isReadonlyTupleOf", async (t) => { ); }); -Deno.test("UniformTupleOf", () => { - assertType< - Equal, [number, number, number, number, number]> - >(true); -}); - -Deno.test("ReadonlyUniformTupleOf", () => { - assertType< - Equal< - ReadonlyUniformTupleOf, - readonly [ - number, - number, - number, - number, - number, - ] - > - >(true); -}); - Deno.test("isUniformTupleOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot(t, isUniformTupleOf(3).name); @@ -715,15 +671,6 @@ Deno.test("isMapOf", async (t) => { }); }); -Deno.test("ObjectOf", () => { - assertType< - Equal< - ObjectOf<{ a: typeof is.Number; b: typeof is.String }>, - { a: number; b: string } - > - >(true); -}); - Deno.test("isObjectOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot( From 38e0705a38b4408476ea4477b929c20f4e3c04a5 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 19:54:31 +0900 Subject: [PATCH 03/31] :boom: Fix `isRecord` to check if the object is a plain object Previously, `isRecord` returned `true` for `Date` and `Promise` objects. This commit fixes it to return `true` only for plain objects. --- is.ts | 5 +---- is_test.ts | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/is.ts b/is.ts index 2722017..b2b3134 100644 --- a/is.ts +++ b/is.ts @@ -553,10 +553,7 @@ export function isReadonlyUniformTupleOf( export function isRecord( x: unknown, ): x is Record { - if (isNullish(x) || isArray(x) || isSet(x) || isMap(x)) { - return false; - } - return typeof x === "object"; + return x != null && typeof x === "object" && x.constructor === Object; } /** diff --git a/is_test.ts b/is_test.ts index 287075b..56b2515 100644 --- a/is_test.ts +++ b/is_test.ts @@ -541,7 +541,7 @@ Deno.test("isReadonlyUniformTupleOf", async (t) => { Deno.test("isRecord", async (t) => { await testWithExamples(t, isRecord, { - validExamples: ["record", "date", "promise"], + validExamples: ["record"], }); }); @@ -1112,7 +1112,7 @@ Deno.test("isOptionalOf", async (t) => { }); await t.step("with isRecord", async (t) => { await testWithExamples(t, isOptionalOf(isRecord), { - validExamples: ["record", "date", "promise", "undefined"], + validExamples: ["record", "undefined"], }); }); await t.step("with isFunction", async (t) => { From c14bc942362550cec63b0f0f68671652fa394b7e Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 19:59:44 +0900 Subject: [PATCH 04/31] :muscle: Simplify type definitions of `isObjectOf/isOptionalOf` --- is.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/is.ts b/is.ts index b2b3134..c4a10b7 100644 --- a/is.ts +++ b/is.ts @@ -680,21 +680,23 @@ export function isMapOf( ); } -type OptionalPredicateKeys> = { - [K in keyof T]: T[K] extends OptionalPredicate ? K : never; -}[keyof T]; - type ObjectOf>> = FlatType< + // Non optional & { - [K in Exclude>]: T[K] extends + [K in keyof T as T[K] extends Optional ? never : K]: T[K] extends Predicate ? U : never; } + // Optional & { - [K in OptionalPredicateKeys]?: T[K] extends Predicate ? U - : never; + [K in keyof T as T[K] extends Optional ? K : never]?: T[K] extends + Predicate ? U : never; } >; +type Optional = { + optional: true; +}; + /** * Return a type predicate function that returns `true` if the type of `x` is `ObjectOf`. * @@ -1122,10 +1124,6 @@ export function isAllOf< ); } -type OptionalPredicate = Predicate & { - optional: true; -}; - /** * Return a type predicate function that returns `true` if the type of `x` is `T` or `undefined`. * @@ -1144,7 +1142,7 @@ type OptionalPredicate = Predicate & { */ export function isOptionalOf( pred: Predicate, -): OptionalPredicate { +): Predicate & Optional { return Object.defineProperties( (x: unknown): x is Predicate => isUndefined(x) || pred(x), { @@ -1155,7 +1153,7 @@ export function isOptionalOf( get: () => `isOptionalOf(${inspect(pred)})`, }, }, - ) as OptionalPredicate; + ) as Predicate & Optional; } export default { From 3c7665f3d2504cbee2ecce82f5fb6cbe46cb5bec Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 20:01:57 +0900 Subject: [PATCH 05/31] :muscle: Define `isSet` and `isMap` as functions instead --- is.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/is.ts b/is.ts index c4a10b7..c63a8cf 100644 --- a/is.ts +++ b/is.ts @@ -192,11 +192,9 @@ export function isArrayOf( * } * ``` */ -export const isSet = Object.defineProperties(isInstanceOf(Set), { - name: { - value: "isSet", - }, -}); +export function isSet(x: unknown): x is Set { + return x instanceof Set; +} /** * Return a type predicate function that returns `true` if the type of `x` is `Set`. @@ -621,11 +619,9 @@ export function isRecordOf( * } * ``` */ -export const isMap = Object.defineProperties(isInstanceOf(Map), { - name: { - value: "isMap", - }, -}); +export function isMap(x: unknown): x is Map { + return x instanceof Map; +} /** * Return a type predicate function that returns `true` if the type of `x` is `Map`. From d673129c1e16e556e30ddc710bd60da61582d1b6 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 20:03:45 +0900 Subject: [PATCH 06/31] :muscle: Avoid using `Array.prototype.includes` --- is.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/is.ts b/is.ts index c63a8cf..1ae5add 100644 --- a/is.ts +++ b/is.ts @@ -955,10 +955,17 @@ export type Primitive = * ``` */ export function isPrimitive(x: unknown): x is Primitive { - return x == null || - ["string", "number", "bigint", "boolean", "symbol"].includes(typeof x); + return x == null || primitiveSet.has(typeof x); } +const primitiveSet = new Set([ + "string", + "number", + "bigint", + "boolean", + "symbol", +]); + /** * Return a type predicate function that returns `true` if the type of `x` is a literal type of `pred`. * @@ -1005,9 +1012,9 @@ export function isLiteralOf(literal: T): Predicate { export function isLiteralOneOf( literals: T, ): Predicate { + const s = new Set(literals); return Object.defineProperties( - (x: unknown): x is T[number] => - literals.includes(x as unknown as T[number]), + (x: unknown): x is T[number] => s.has(x as T[number]), { name: { get: () => `isLiteralOneOf(${inspect(literals)})`, From b4eea433cfaa0acd58bcb4c4a2a6d870addfd6e0 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 20:05:44 +0900 Subject: [PATCH 07/31] :muscle: Avoid calling `Object.prototype.toString` multiple times --- is.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/is.ts b/is.ts index 1ae5add..858a734 100644 --- a/is.ts +++ b/is.ts @@ -1,6 +1,8 @@ import type { FlatType, UnionToIntersection } from "./_typeutil.ts"; import { inspect } from "./inspect.ts"; +const objectToString = Object.prototype.toString; + /** * A type predicate function. */ @@ -812,7 +814,7 @@ export function isFunction(x: unknown): x is (...args: unknown[]) => unknown { export function isSyncFunction( x: unknown, ): x is (...args: unknown[]) => unknown { - return Object.prototype.toString.call(x) === "[object Function]"; + return objectToString.call(x) === "[object Function]"; } /** @@ -831,7 +833,7 @@ export function isSyncFunction( export function isAsyncFunction( x: unknown, ): x is (...args: unknown[]) => Promise { - return Object.prototype.toString.call(x) === "[object AsyncFunction]"; + return objectToString.call(x) === "[object AsyncFunction]"; } /** From 4afebc9eff82d67c3db46faa80642adbde9035d1 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 20:06:15 +0900 Subject: [PATCH 08/31] :muscle: Sort entries for `default` export --- is.ts | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/is.ts b/is.ts index 858a734..3b298da 100644 --- a/is.ts +++ b/is.ts @@ -1162,37 +1162,37 @@ export function isOptionalOf( } export default { + AllOf: isAllOf, Any: isAny, - Unknown: isUnknown, - String: isString, - Number: isNumber, - BigInt: isBigInt, - Boolean: isBoolean, Array: isArray, ArrayOf: isArrayOf, - Set: isSet, - SetOf: isSetOf, - TupleOf: isTupleOf, - ReadonlyTupleOf: isReadonlyTupleOf, - UniformTupleOf: isUniformTupleOf, - ReadonlyUniformTupleOf: isReadonlyUniformTupleOf, - Record: isRecord, - RecordOf: isRecordOf, - Map: isMap, - MapOf: isMapOf, - ObjectOf: isObjectOf, - Function: isFunction, - SyncFunction: isSyncFunction, AsyncFunction: isAsyncFunction, + BigInt: isBigInt, + Boolean: isBoolean, + Function: isFunction, InstanceOf: isInstanceOf, - Null: isNull, - Undefined: isUndefined, - Nullish: isNullish, - Symbol: isSymbol, - Primitive: isPrimitive, LiteralOf: isLiteralOf, LiteralOneOf: isLiteralOneOf, + Map: isMap, + MapOf: isMapOf, + Null: isNull, + Nullish: isNullish, + Number: isNumber, + ObjectOf: isObjectOf, OneOf: isOneOf, - AllOf: isAllOf, OptionalOf: isOptionalOf, + Primitive: isPrimitive, + ReadonlyTupleOf: isReadonlyTupleOf, + ReadonlyUniformTupleOf: isReadonlyUniformTupleOf, + Record: isRecord, + RecordOf: isRecordOf, + Set: isSet, + SetOf: isSetOf, + String: isString, + Symbol: isSymbol, + SyncFunction: isSyncFunction, + TupleOf: isTupleOf, + Undefined: isUndefined, + UniformTupleOf: isUniformTupleOf, + Unknown: isUnknown, }; From d60845e63dc721f65d896a4caecc348133ab2f1c Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 20:32:13 +0900 Subject: [PATCH 09/31] :muscle: Split is.ts into multiple files under `is` directory --- is.ts | 1204 +---------------- .../__snapshots__/factory_test.ts.snap | 167 +-- is/__snapshots__/utility_test.ts.snap | 18 + is/_testutil.ts | 13 + is/core.ts | 354 +++++ is/core_test.ts | 223 +++ is/factory.ts | 689 ++++++++++ is_test.ts => is/factory_test.ts | 492 +------ is/type.ts | 26 + is/utility.ts | 147 ++ is/utility_test.ts | 255 ++++ 11 files changed, 1836 insertions(+), 1752 deletions(-) rename __snapshots__/is_test.ts.snap => is/__snapshots__/factory_test.ts.snap (93%) create mode 100644 is/__snapshots__/utility_test.ts.snap create mode 100644 is/_testutil.ts create mode 100644 is/core.ts create mode 100644 is/core_test.ts create mode 100644 is/factory.ts rename is_test.ts => is/factory_test.ts (63%) create mode 100644 is/type.ts create mode 100644 is/utility.ts create mode 100644 is/utility_test.ts diff --git a/is.ts b/is.ts index 3b298da..5700029 100644 --- a/is.ts +++ b/is.ts @@ -1,1198 +1,14 @@ -import type { FlatType, UnionToIntersection } from "./_typeutil.ts"; -import { inspect } from "./inspect.ts"; +import core from "./is/core.ts"; +import factory from "./is/factory.ts"; +import utility from "./is/utility.ts"; -const objectToString = Object.prototype.toString; - -/** - * A type predicate function. - */ -export type Predicate = (x: unknown) => x is T; - -/** - * A type predicated by Predicate. - * - * ```ts - * import { is, type PredicateType } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isPerson = is.ObjectOf({ - * name: is.String, - * age: is.Number, - * address: is.OptionalOf(is.String), - * }); - * - * type Person = PredicateType; - * // Above is equivalent to the following type - * // type Person = { - * // name: string; - * // age: number; - * // address: string | undefined; - * // }; - */ -export type PredicateType

= P extends Predicate ? T : never; - -/** - * Assume `x is `any` and always return `true` regardless of the type of `x`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a = "a"; - * if (is.Any(a)) { - * // a is narrowed to any - * const _: any = a; - * } - * ``` - */ -// deno-lint-ignore no-explicit-any -export function isAny(_x: unknown): _x is any { - return true; -} - -/** - * Assume `x` is `unknown` and always return `true` regardless of the type of `x`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a = "a"; - * if (is.Unknown(a)) { - * // a is narrowed to unknown - * const _: unknown = a; - * } - * ``` - */ -export function isUnknown(_x: unknown): _x is unknown { - return true; -} - -/** - * Return `true` if the type of `x` is `string`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = "a"; - * if (is.String(a)) { - * // a is narrowed to string - * const _: string = a; - * } - * ``` - */ -export function isString(x: unknown): x is string { - return typeof x === "string"; -} - -/** - * Return `true` if the type of `x` is `number`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = 0; - * if (is.Number(a)) { - * // a is narrowed to number - * const _: number = a; - * } - * ``` - */ -export function isNumber(x: unknown): x is number { - return typeof x === "number"; -} - -/** - * Return `true` if the type of `x` is `bigint`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = 0n; - * if (is.BigInt(a)) { - * // a is narrowed to bigint - * const _: bigint = a; - * } - * ``` - */ -export function isBigInt(x: unknown): x is bigint { - return typeof x === "bigint"; -} - -/** - * Return `true` if the type of `x` is `boolean`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = true; - * if (is.Boolean(a)) { - * // a is narrowed to boolean - * const _: boolean = a; - * } - * ``` - */ -export function isBoolean(x: unknown): x is boolean { - return typeof x === "boolean"; -} - -/** - * Return `true` if the type of `x` is `unknown[]`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = [0, 1, 2]; - * if (is.Array(a)) { - * // a is narrowed to unknown[] - * const _: unknown[] = a; - * } - * ``` - */ -export function isArray( - x: unknown, -): x is unknown[] { - return Array.isArray(x); -} - -/** - * Return a type predicate function that returns `true` if the type of `x` is `T[]`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.ArrayOf(is.String); - * const a: unknown = ["a", "b", "c"]; - * if (isMyType(a)) { - * // a is narrowed to string[] - * const _: string[] = a; - * } - * ``` - */ -export function isArrayOf( - pred: Predicate, -): Predicate { - return Object.defineProperties( - (x: unknown): x is T[] => isArray(x) && x.every(pred), - { - name: { - get: () => `isArrayOf(${inspect(pred)})`, - }, - }, - ); -} - -/** - * Return `true` if the type of `x` is `Set`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = new Set([0, 1, 2]); - * if (is.Set(a)) { - * // a is narrowed to Set - * const _: Set = a; - * } - * ``` - */ -export function isSet(x: unknown): x is Set { - return x instanceof Set; -} - -/** - * Return a type predicate function that returns `true` if the type of `x` is `Set`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.SetOf(is.String); - * const a: unknown = new Set(["a", "b", "c"]); - * if (isMyType(a)) { - * // a is narrowed to Set - * const _: Set = a; - * } - * ``` - */ -export function isSetOf( - pred: Predicate, -): Predicate> { - return Object.defineProperties( - (x: unknown): x is Set => { - if (!isSet(x)) return false; - for (const v of x.values()) { - if (!pred(v)) return false; - } - return true; - }, - { - name: { - get: () => `isSetOf(${inspect(pred)})`, - }, - }, - ); -} - -type TupleOf = { - -readonly [P in keyof T]: T[P] extends Predicate ? U : never; -}; - -type ReadonlyTupleOf = { - [P in keyof T]: T[P] extends Predicate ? U : never; -}; - -/** - * Return a type predicate function that returns `true` if the type of `x` is `TupleOf` or `TupleOf`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.TupleOf([is.Number, is.String, is.Boolean]); - * const a: unknown = [0, "a", true]; - * if (isMyType(a)) { - * // a is narrowed to [number, string, boolean] - * const _: [number, string, boolean] = a; - * } - * ``` - * - * With `predElse`: - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.TupleOf( - * [is.Number, is.String, is.Boolean], - * is.ArrayOf(is.Number), - * ); - * const a: unknown = [0, "a", true, 0, 1, 2]; - * if (isMyType(a)) { - * // a is narrowed to [number, string, boolean, ...number[]] - * const _: [number, string, boolean, ...number[]] = a; - * } - * ``` - * - * Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array - * used as `predTup`. If a type error occurs, try adding `as const` as follows: - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const predTup = [is.Number, is.String, is.Boolean] as const; - * const isMyType = is.TupleOf(predTup); - * const a: unknown = [0, "a", true]; - * if (isMyType(a)) { - * // a is narrowed to [number, string, boolean] - * const _: [number, string, boolean] = a; - * } - * ``` - */ -export function isTupleOf< - T extends readonly [Predicate, ...Predicate[]], ->( - predTup: T, -): Predicate>; -export function isTupleOf< - T extends readonly [Predicate, ...Predicate[]], - E extends Predicate, ->( - predTup: T, - predElse: E, -): Predicate<[...TupleOf, ...PredicateType]>; -export function isTupleOf< - T extends readonly [Predicate, ...Predicate[]], - E extends Predicate, ->( - predTup: T, - predElse?: E, -): Predicate | [...TupleOf, ...PredicateType]> { - if (!predElse) { - return Object.defineProperties( - (x: unknown): x is TupleOf => { - if (!isArray(x) || x.length !== predTup.length) { - return false; - } - return predTup.every((pred, i) => pred(x[i])); - }, - { - name: { - get: () => `isTupleOf(${inspect(predTup)})`, - }, - }, - ); - } else { - return Object.defineProperties( - (x: unknown): x is [...TupleOf, ...PredicateType] => { - if (!isArray(x) || x.length < predTup.length) { - return false; - } - const head = x.slice(0, predTup.length); - const tail = x.slice(predTup.length); - return predTup.every((pred, i) => pred(head[i])) && predElse(tail); - }, - { - name: { - get: () => `isTupleOf(${inspect(predTup)}, ${inspect(predElse)})`, - }, - }, - ); - } -} - -/** - * Return a type predicate function that returns `true` if the type of `x` is `ReadonlyTupleOf`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.ReadonlyTupleOf([is.Number, is.String, is.Boolean]); - * const a: unknown = [0, "a", true]; - * if (isMyType(a)) { - * // a is narrowed to readonly [number, string, boolean] - * const _: readonly [number, string, boolean] = a; - * } - * ``` - * - * With `predElse`: - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.ReadonlyTupleOf( - * [is.Number, is.String, is.Boolean], - * is.ArrayOf(is.Number), - * ); - * const a: unknown = [0, "a", true, 0, 1, 2]; - * if (isMyType(a)) { - * // a is narrowed to readonly [number, string, boolean, ...number[]] - * const _: readonly [number, string, boolean, ...number[]] = a; - * } - * ``` - * - * Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array - * used as `predTup`. If a type error occurs, try adding `as const` as follows: - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const predTup = [is.Number, is.String, is.Boolean] as const; - * const isMyType = is.ReadonlyTupleOf(predTup); - * const a: unknown = [0, "a", true]; - * if (isMyType(a)) { - * // a is narrowed to readonly [number, string, boolean] - * const _: readonly [number, string, boolean] = a; - * } - * ``` - */ -export function isReadonlyTupleOf< - T extends readonly [Predicate, ...Predicate[]], ->( - predTup: T, -): Predicate>; -export function isReadonlyTupleOf< - T extends readonly [Predicate, ...Predicate[]], - E extends Predicate, ->( - predTup: T, - predElse: E, -): Predicate, ...PredicateType]>; -export function isReadonlyTupleOf< - T extends readonly [Predicate, ...Predicate[]], - E extends Predicate, ->( - predTup: T, - predElse?: E, -): Predicate< - ReadonlyTupleOf | readonly [...ReadonlyTupleOf, ...PredicateType] -> { - if (!predElse) { - return Object.defineProperties( - isTupleOf(predTup) as Predicate>, - { - name: { - get: () => `isReadonlyTupleOf(${inspect(predTup)})`, - }, - }, - ); - } else { - return Object.defineProperties( - isTupleOf(predTup, predElse) as unknown as Predicate< - readonly [...ReadonlyTupleOf, ...PredicateType] - >, - { - name: { - get: () => - `isReadonlyTupleOf(${inspect(predTup)}, ${inspect(predElse)})`, - }, - }, - ); - } -} - -// https://stackoverflow.com/a/71700658/1273406 -type UniformTupleOf< - T, - N extends number, - R extends readonly T[] = [], -> = R["length"] extends N ? R : UniformTupleOf; - -type ReadonlyUniformTupleOf< - T, - N extends number, - R extends readonly T[] = [], -> = R["length"] extends N ? R - : ReadonlyUniformTupleOf; - -/** - * Return a type predicate function that returns `true` if the type of `x` is `UniformTupleOf`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.UniformTupleOf(5); - * const a: unknown = [0, 1, 2, 3, 4]; - * if (isMyType(a)) { - * // a is narrowed to [unknown, unknown, unknown, unknown, unknown] - * const _: [unknown, unknown, unknown, unknown, unknown] = a; - * } - * ``` - * - * With predicate function: - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.UniformTupleOf(5, is.Number); - * const a: unknown = [0, 1, 2, 3, 4]; - * if (isMyType(a)) { - * // a is narrowed to [number, number, number, number, number] - * const _: [number, number, number, number, number] = a; - * } - * ``` - */ -export function isUniformTupleOf( - n: N, - pred: Predicate = isAny, -): Predicate> { - return Object.defineProperties( - (x: unknown): x is UniformTupleOf => { - if (!isArray(x) || x.length !== n) { - return false; - } - return x.every((v) => pred(v)); - }, - { - name: { - get: () => `isUniformTupleOf(${n}, ${inspect(pred)})`, - }, - }, - ); -} - -/** - * Return a type predicate function that returns `true` if the type of `x` is `ReadonlyUniformTupleOf`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.ReadonlyUniformTupleOf(5); - * const a: unknown = [0, 1, 2, 3, 4]; - * if (isMyType(a)) { - * // a is narrowed to readonly [unknown, unknown, unknown, unknown, unknown] - * const _: readonly [unknown, unknown, unknown, unknown, unknown] = a; - * } - * ``` - * - * With predicate function: - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.ReadonlyUniformTupleOf(5, is.Number); - * const a: unknown = [0, 1, 2, 3, 4]; - * if (isMyType(a)) { - * // a is narrowed to readonly [number, number, number, number, number] - * const _: readonly [number, number, number, number, number] = a; - * } - * ``` - */ -export function isReadonlyUniformTupleOf( - n: N, - pred: Predicate = isAny, -): Predicate> { - return Object.defineProperties( - isUniformTupleOf(n, pred) as Predicate>, - { - name: { - get: () => `isReadonlyUniformTupleOf(${n}, ${inspect(pred)})`, - }, - }, - ); -} - -/** - * Return `true` if the type of `x` is `Record`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = {"a": 0, "b": 1}; - * if (is.Record(a)) { - * // a is narrowed to Record - * const _: Record = a; - * } - * ``` - */ -export function isRecord( - x: unknown, -): x is Record { - return x != null && typeof x === "object" && x.constructor === Object; -} - -/** - * Return a type predicate function that returns `true` if the type of `x` is `Record`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.RecordOf(is.Number); - * const a: unknown = {"a": 0, "b": 1}; - * if (isMyType(a)) { - * // a is narrowed to Record - * const _: Record = a; - * } - * ``` - * - * With predicate function for keys: - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.RecordOf(is.Number, is.String); - * const a: unknown = {"a": 0, "b": 1}; - * if (isMyType(a)) { - * // a is narrowed to Record - * const _: Record = a; - * } - * ``` - */ -export function isRecordOf( - pred: Predicate, - predKey?: Predicate, -): Predicate> { - return Object.defineProperties( - (x: unknown): x is Record => { - if (!isRecord(x)) return false; - for (const k in x) { - if (!pred(x[k])) return false; - if (predKey && !predKey(k)) return false; - } - return true; - }, - { - name: { - get: predKey - ? () => `isRecordOf(${inspect(pred)}, ${inspect(predKey)})` - : () => `isRecordOf(${inspect(pred)})`, - }, - }, - ); -} - -/** - * Return `true` if the type of `x` is `Map`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = new Map([["a", 0], ["b", 1]]); - * if (is.Map(a)) { - * // a is narrowed to Map - * const _: Map = a; - * } - * ``` - */ -export function isMap(x: unknown): x is Map { - return x instanceof Map; -} - -/** - * Return a type predicate function that returns `true` if the type of `x` is `Map`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.MapOf(is.Number); - * const a: unknown = new Map([["a", 0], ["b", 1]]); - * if (isMyType(a)) { - * // a is narrowed to Map - * const _: Map = a; - * } - * ``` - * - * With predicate function for keys: - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.MapOf(is.Number, is.String); - * const a: unknown = new Map([["a", 0], ["b", 1]]); - * if (isMyType(a)) { - * // a is narrowed to Map - * const _: Map = a; - * } - * ``` - */ -export function isMapOf( - pred: Predicate, - predKey?: Predicate, -): Predicate> { - return Object.defineProperties( - (x: unknown): x is Map => { - if (!isMap(x)) return false; - for (const entry of x.entries()) { - const [k, v] = entry; - if (!pred(v)) return false; - if (predKey && !predKey(k)) return false; - } - return true; - }, - { - name: { - get: predKey - ? () => `isMapOf(${inspect(pred)}, ${inspect(predKey)})` - : () => `isMapOf(${inspect(pred)})`, - }, - }, - ); -} - -type ObjectOf>> = FlatType< - // Non optional - & { - [K in keyof T as T[K] extends Optional ? never : K]: T[K] extends - Predicate ? U : never; - } - // Optional - & { - [K in keyof T as T[K] extends Optional ? K : never]?: T[K] extends - Predicate ? U : never; - } ->; - -type Optional = { - optional: true; -}; - -/** - * Return a type predicate function that returns `true` if the type of `x` is `ObjectOf`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * If `is.OptionalOf()` is specified in the predicate function, the property becomes optional. - * - * When `options.strict` is `true`, the number of keys of `x` must be equal to the number of keys of `predObj`. - * Otherwise, the number of keys of `x` must be greater than or equal to the number of keys of `predObj`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.ObjectOf({ - * a: is.Number, - * b: is.String, - * c: is.OptionalOf(is.Boolean), - * }); - * const a: unknown = { a: 0, b: "a", other: "other" }; - * if (isMyType(a)) { - * // "other" key in `a` is ignored because of `options.strict` is `false`. - * // a is narrowed to { a: number; b: string; c?: boolean | undefined } - * const _: { a: number; b: string; c?: boolean | undefined } = a; - * } - * ``` - * - * With `options.strict`: - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.ObjectOf({ - * a: is.Number, - * b: is.String, - * c: is.OptionalOf(is.Boolean), - * }, { strict: true }); - * const a: unknown = { a: 0, b: "a", other: "other" }; - * if (isMyType(a)) { - * // This block will not be executed because of "other" key in `a`. - * } - * ``` - */ -export function isObjectOf< - T extends Record>, ->( - predObj: T, - { strict }: { strict?: boolean } = {}, -): Predicate> { - return Object.defineProperties( - strict ? isObjectOfStrict(predObj) : isObjectOfLoose(predObj), - { - name: { - get: () => `isObjectOf(${inspect(predObj)})`, - }, - }, - ); -} - -function isObjectOfLoose< - T extends Record>, ->( - predObj: T, -): Predicate> { - return (x: unknown): x is ObjectOf => { - if (!isRecord(x)) return false; - for (const k in predObj) { - if (!predObj[k](x[k])) return false; - } - return true; - }; -} - -function isObjectOfStrict< - T extends Record>, ->( - predObj: T, -): Predicate> { - const keys = new Set(Object.keys(predObj)); - const pred = isObjectOfLoose(predObj); - return (x: unknown): x is ObjectOf => { - if (!pred(x)) return false; - const ks = Object.keys(x); - return ks.length <= keys.size && ks.every((k) => keys.has(k)); - }; -} - -/** - * Return `true` if the type of `x` is `function`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = () => {}; - * if (is.Function(a)) { - * // a is narrowed to (...args: unknown[]) => unknown - * const _: ((...args: unknown[]) => unknown) = a; - * } - * ``` - */ -export function isFunction(x: unknown): x is (...args: unknown[]) => unknown { - return x instanceof Function; -} - -/** - * Return `true` if the type of `x` is `function` (non async function). - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = () => {}; - * if (is.Function(a)) { - * // a is narrowed to (...args: unknown[]) => unknown - * const _: ((...args: unknown[]) => unknown) = a; - * } - * ``` - */ -export function isSyncFunction( - x: unknown, -): x is (...args: unknown[]) => unknown { - return objectToString.call(x) === "[object Function]"; -} - -/** - * Return `true` if the type of `x` is `function` (async function). - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = async () => {}; - * if (is.Function(a)) { - * // a is narrowed to (...args: unknown[]) => Promise - * const _: ((...args: unknown[]) => unknown) = a; - * } - * ``` - */ -export function isAsyncFunction( - x: unknown, -): x is (...args: unknown[]) => Promise { - return objectToString.call(x) === "[object AsyncFunction]"; -} - -/** - * Return `true` if the type of `x` is instance of `ctor`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.InstanceOf(Date); - * const a: unknown = new Date(); - * if (isMyType(a)) { - * // a is narrowed to Date - * const _: Date = a; - * } - * ``` - */ -// deno-lint-ignore no-explicit-any -export function isInstanceOf unknown>( - ctor: T, -): Predicate> { - return Object.defineProperties( - (x: unknown): x is InstanceType => x instanceof ctor, - { - name: { - get: () => `isInstanceOf(${inspect(ctor)})`, - }, - }, - ); -} - -/** - * Return `true` if the type of `x` is `null`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = null; - * if (is.Null(a)) { - * // a is narrowed to null - * const _: null = a; - * } - * ``` - */ -export function isNull(x: unknown): x is null { - return x === null; -} - -/** - * Return `true` if the type of `x` is `undefined`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = undefined; - * if (is.Undefined(a)) { - * // a is narrowed to undefined - * const _: undefined = a; - * } - * ``` - */ -export function isUndefined(x: unknown): x is undefined { - return typeof x === "undefined"; -} - -/** - * Return `true` if the type of `x` is `null` or `undefined`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = null; - * if (is.Nullish(a)) { - * // a is narrowed to null | undefined - * const _: (null | undefined) = a; - * } - * ``` - */ -export function isNullish(x: unknown): x is null | undefined { - return x == null; -} - -/** - * Return `true` if the type of `x` is `symbol`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = Symbol("symbol"); - * if (is.Symbol(a)) { - * // a is narrowed to symbol - * const _: symbol = a; - * } - * ``` - */ -export function isSymbol(x: unknown): x is symbol { - return typeof x === "symbol"; -} - -export type Primitive = - | string - | number - | bigint - | boolean - | null - | undefined - | symbol; - -/** - * Return `true` if the type of `x` is `Primitive`. - * - * ```ts - * import { is, Primitive } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const a: unknown = 0; - * if (is.Primitive(a)) { - * // a is narrowed to Primitive - * const _: Primitive = a; - * } - * ``` - */ -export function isPrimitive(x: unknown): x is Primitive { - return x == null || primitiveSet.has(typeof x); -} - -const primitiveSet = new Set([ - "string", - "number", - "bigint", - "boolean", - "symbol", -]); - -/** - * Return a type predicate function that returns `true` if the type of `x` is a literal type of `pred`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.LiteralOf("hello"); - * const a: unknown = "hello"; - * if (isMyType(a)) { - * // a is narrowed to "hello" - * const _: "hello" = a; - * } - * ``` - */ -export function isLiteralOf(literal: T): Predicate { - return Object.defineProperties( - (x: unknown): x is T => x === literal, - { - name: { - get: () => `isLiteralOf(${inspect(literal)})`, - }, - }, - ); -} - -/** - * Return a type predicate function that returns `true` if the type of `x` is one of literal type in `preds`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.LiteralOneOf(["hello", "world"] as const); - * const a: unknown = "hello"; - * if (isMyType(a)) { - * // a is narrowed to "hello" | "world" - * const _: "hello" | "world" = a; - * } - * ``` - */ -export function isLiteralOneOf( - literals: T, -): Predicate { - const s = new Set(literals); - return Object.defineProperties( - (x: unknown): x is T[number] => s.has(x as T[number]), - { - name: { - get: () => `isLiteralOneOf(${inspect(literals)})`, - }, - }, - ); -} - -type OneOf = T extends readonly [Predicate, ...infer R] - ? U | OneOf - : never; - -/** - * Return a type predicate function that returns `true` if the type of `x` is `OneOf`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.OneOf([is.Number, is.String, is.Boolean]); - * const a: unknown = 0; - * if (isMyType(a)) { - * // a is narrowed to number | string | boolean - * const _: number | string | boolean = a; - * } - * ``` - * - * Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array - * used as `preds`. If a type error occurs, try adding `as const` as follows: - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const preds = [is.Number, is.String, is.Boolean] as const; - * const isMyType = is.OneOf(preds); - * const a: unknown = 0; - * if (isMyType(a)) { - * // a is narrowed to number | string | boolean - * const _: number | string | boolean = a; - * } - * ``` - */ -export function isOneOf< - T extends readonly [Predicate, ...Predicate[]], ->( - preds: T, -): Predicate> { - return Object.defineProperties( - (x: unknown): x is OneOf => preds.some((pred) => pred(x)), - { - name: { - get: () => `isOneOf(${inspect(preds)})`, - }, - }, - ); -} - -type AllOf = UnionToIntersection>; - -/** - * Return a type predicate function that returns `true` if the type of `x` is `AllOf`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.AllOf([ - * is.ObjectOf({ a: is.Number }), - * is.ObjectOf({ b: is.String }), - * ]); - * const a: unknown = { a: 0, b: "a" }; - * if (isMyType(a)) { - * // a is narrowed to { a: number } & { b: string } - * const _: { a: number } & { b: string } = a; - * } - * ``` - * - * Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array - * used as `preds`. If a type error occurs, try adding `as const` as follows: - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const preds = [ - * is.ObjectOf({ a: is.Number }), - * is.ObjectOf({ b: is.String }), - * ] as const - * const isMyType = is.AllOf(preds); - * const a: unknown = { a: 0, b: "a" }; - * if (isMyType(a)) { - * // a is narrowed to { a: number } & { b: string } - * const _: { a: number } & { b: string } = a; - * } - * ``` - */ -export function isAllOf< - T extends readonly [Predicate, ...Predicate[]], ->( - preds: T, -): Predicate> { - return Object.defineProperties( - (x: unknown): x is AllOf => preds.every((pred) => pred(x)), - { - name: { - get: () => `isAllOf(${inspect(preds)})`, - }, - }, - ); -} - -/** - * Return a type predicate function that returns `true` if the type of `x` is `T` or `undefined`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.OptionalOf(is.String); - * const a: unknown = "a"; - * if (isMyType(a)) { - * // a is narrowed to string | undefined - * const _: string | undefined = a; - * } - * ``` - */ -export function isOptionalOf( - pred: Predicate, -): Predicate & Optional { - return Object.defineProperties( - (x: unknown): x is Predicate => isUndefined(x) || pred(x), - { - optional: { - value: true as const, - }, - name: { - get: () => `isOptionalOf(${inspect(pred)})`, - }, - }, - ) as Predicate & Optional; -} +export * from "./is/type.ts"; +export * from "./is/core.ts"; +export * from "./is/factory.ts"; +export * from "./is/utility.ts"; export default { - AllOf: isAllOf, - Any: isAny, - Array: isArray, - ArrayOf: isArrayOf, - AsyncFunction: isAsyncFunction, - BigInt: isBigInt, - Boolean: isBoolean, - Function: isFunction, - InstanceOf: isInstanceOf, - LiteralOf: isLiteralOf, - LiteralOneOf: isLiteralOneOf, - Map: isMap, - MapOf: isMapOf, - Null: isNull, - Nullish: isNullish, - Number: isNumber, - ObjectOf: isObjectOf, - OneOf: isOneOf, - OptionalOf: isOptionalOf, - Primitive: isPrimitive, - ReadonlyTupleOf: isReadonlyTupleOf, - ReadonlyUniformTupleOf: isReadonlyUniformTupleOf, - Record: isRecord, - RecordOf: isRecordOf, - Set: isSet, - SetOf: isSetOf, - String: isString, - Symbol: isSymbol, - SyncFunction: isSyncFunction, - TupleOf: isTupleOf, - Undefined: isUndefined, - UniformTupleOf: isUniformTupleOf, - Unknown: isUnknown, + ...core, + ...factory, + ...utility, }; diff --git a/__snapshots__/is_test.ts.snap b/is/__snapshots__/factory_test.ts.snap similarity index 93% rename from __snapshots__/is_test.ts.snap rename to is/__snapshots__/factory_test.ts.snap index 0fcb167..b6bb894 100644 --- a/__snapshots__/is_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,85 +1,79 @@ export const snapshot = {}; -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber)"`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous))"`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; -snapshot[`isTupleOf > returns properly named function 1`] = ` +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; + +snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ isNumber, isString, isBoolean -])" +], isArray)" `; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` +snapshot[`isTupleOf > returns properly named function 3`] = ` "isTupleOf([ isTupleOf([ isTupleOf([ isNumber, isString, isBoolean - ]) - ]) + ], isArray) + ], isArray) ])" `; -snapshot[`isOneOf > returns properly named function 1`] = ` -"isOneOf([ - isNumber, - isString, - isBoolean -])" +snapshot[`isObjectOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +})" `; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ - isNumber, - isString, - isBoolean -], isArray)" +snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; + +snapshot[`isObjectOf > returns properly named function 3`] = ` +"isObjectOf({ + a: isObjectOf({ + b: isObjectOf({c: isBoolean}) + }) +})" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ - isNumber, - isString, - isBoolean - ], isArray) - ], isArray) -], isArray)" -`; +snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; -snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ isNumber, isString, isBoolean ])" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ isNumber, isString, isBoolean @@ -88,69 +82,49 @@ snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` ])" `; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; - -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; - -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber)"`; -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous))"`; snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber)"`; snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous))"`; -snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; - -snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; +snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; +snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; +snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyTupleOf([ isNumber, isString, isBoolean -], isArray)" +])" `; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyTupleOf([ + isReadonlyTupleOf([ + isReadonlyTupleOf([ isNumber, isString, isBoolean - ], isArray) - ], isArray) + ]) + ]) ])" `; -snapshot[`isObjectOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - b: isString, - c: isBoolean -})" -`; - -snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; -snapshot[`isObjectOf > returns properly named function 3`] = ` -"isObjectOf({ - a: isObjectOf({ - b: isObjectOf({c: isBoolean}) - }) -})" -`; +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; @@ -166,15 +140,24 @@ snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(u snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; -snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; - -snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyTupleOf([ + isNumber, + isString, + isBoolean +], isArray)" +`; -snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isAllOf > returns properly named function 1`] = ` -"isAllOf([ - isObjectOf({a: isNumber}), - isObjectOf({b: isString}) -])" +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyTupleOf([ + isReadonlyTupleOf([ + isReadonlyTupleOf([ + isNumber, + isString, + isBoolean + ], isArray) + ], isArray) +], isArray)" `; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap new file mode 100644 index 0000000..565cf28 --- /dev/null +++ b/is/__snapshots__/utility_test.ts.snap @@ -0,0 +1,18 @@ +export const snapshot = {}; + +snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; + +snapshot[`isOneOf > returns properly named function 1`] = ` +"isOneOf([ + isNumber, + isString, + isBoolean +])" +`; + +snapshot[`isAllOf > returns properly named function 1`] = ` +"isAllOf([ + isObjectOf({a: isNumber}), + isObjectOf({b: isString}) +])" +`; diff --git a/is/_testutil.ts b/is/_testutil.ts new file mode 100644 index 0000000..11e9e6f --- /dev/null +++ b/is/_testutil.ts @@ -0,0 +1,13 @@ +// It seems 'IsExact' in deno_std is false positive so use `Equal` in type-challenges +// https://github.com/type-challenges/type-challenges/blob/e77262dba62e9254451f661cb4fe5517ffd1d933/utils/index.d.ts#L7-L9 +export type Equal = (() => T extends X ? 1 : 2) extends + (() => T extends Y ? 1 : 2) ? true : false; + +export function stringify(x: unknown): string { + if (x instanceof Date) return `Date(${x.valueOf()})`; + if (x instanceof Promise) return "Promise"; + if (typeof x === "function") return x.toString(); + if (typeof x === "bigint") return `${x}n`; + if (typeof x === "symbol") return x.toString(); + return JSON.stringify(x); +} diff --git a/is/core.ts b/is/core.ts new file mode 100644 index 0000000..67cd6a9 --- /dev/null +++ b/is/core.ts @@ -0,0 +1,354 @@ +const objectToString = Object.prototype.toString; + +/** + * Assume `x is `any` and always return `true` regardless of the type of `x`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a = "a"; + * if (is.Any(a)) { + * // a is narrowed to any + * const _: any = a; + * } + * ``` + */ +// deno-lint-ignore no-explicit-any +export function isAny(_x: unknown): _x is any { + return true; +} + +/** + * Assume `x` is `unknown` and always return `true` regardless of the type of `x`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a = "a"; + * if (is.Unknown(a)) { + * // a is narrowed to unknown + * const _: unknown = a; + * } + * ``` + */ +export function isUnknown(_x: unknown): _x is unknown { + return true; +} + +/** + * Return `true` if the type of `x` is `string`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = "a"; + * if (is.String(a)) { + * // a is narrowed to string + * const _: string = a; + * } + * ``` + */ +export function isString(x: unknown): x is string { + return typeof x === "string"; +} + +/** + * Return `true` if the type of `x` is `number`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = 0; + * if (is.Number(a)) { + * // a is narrowed to number + * const _: number = a; + * } + * ``` + */ +export function isNumber(x: unknown): x is number { + return typeof x === "number"; +} + +/** + * Return `true` if the type of `x` is `bigint`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = 0n; + * if (is.BigInt(a)) { + * // a is narrowed to bigint + * const _: bigint = a; + * } + * ``` + */ +export function isBigInt(x: unknown): x is bigint { + return typeof x === "bigint"; +} + +/** + * Return `true` if the type of `x` is `boolean`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = true; + * if (is.Boolean(a)) { + * // a is narrowed to boolean + * const _: boolean = a; + * } + * ``` + */ +export function isBoolean(x: unknown): x is boolean { + return typeof x === "boolean"; +} + +/** + * Return `true` if the type of `x` is `unknown[]`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = [0, 1, 2]; + * if (is.Array(a)) { + * // a is narrowed to unknown[] + * const _: unknown[] = a; + * } + * ``` + */ +export function isArray( + x: unknown, +): x is unknown[] { + return Array.isArray(x); +} + +/** + * Return `true` if the type of `x` is `Set`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = new Set([0, 1, 2]); + * if (is.Set(a)) { + * // a is narrowed to Set + * const _: Set = a; + * } + * ``` + */ +export function isSet(x: unknown): x is Set { + return x instanceof Set; +} + +/** + * Return `true` if the type of `x` is `Record`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = {"a": 0, "b": 1}; + * if (is.Record(a)) { + * // a is narrowed to Record + * const _: Record = a; + * } + * ``` + */ +export function isRecord( + x: unknown, +): x is Record { + return x != null && typeof x === "object" && x.constructor === Object; +} + +/** + * Return `true` if the type of `x` is `Map`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = new Map([["a", 0], ["b", 1]]); + * if (is.Map(a)) { + * // a is narrowed to Map + * const _: Map = a; + * } + * ``` + */ +export function isMap(x: unknown): x is Map { + return x instanceof Map; +} + +/** + * Return `true` if the type of `x` is `function`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = () => {}; + * if (is.Function(a)) { + * // a is narrowed to (...args: unknown[]) => unknown + * const _: ((...args: unknown[]) => unknown) = a; + * } + * ``` + */ +export function isFunction(x: unknown): x is (...args: unknown[]) => unknown { + return x instanceof Function; +} + +/** + * Return `true` if the type of `x` is `function` (non async function). + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = () => {}; + * if (is.Function(a)) { + * // a is narrowed to (...args: unknown[]) => unknown + * const _: ((...args: unknown[]) => unknown) = a; + * } + * ``` + */ +export function isSyncFunction( + x: unknown, +): x is (...args: unknown[]) => unknown { + return objectToString.call(x) === "[object Function]"; +} + +/** + * Return `true` if the type of `x` is `function` (async function). + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = async () => {}; + * if (is.Function(a)) { + * // a is narrowed to (...args: unknown[]) => Promise + * const _: ((...args: unknown[]) => unknown) = a; + * } + * ``` + */ +export function isAsyncFunction( + x: unknown, +): x is (...args: unknown[]) => Promise { + return objectToString.call(x) === "[object AsyncFunction]"; +} + +/** + * Return `true` if the type of `x` is `null`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = null; + * if (is.Null(a)) { + * // a is narrowed to null + * const _: null = a; + * } + * ``` + */ +export function isNull(x: unknown): x is null { + return x === null; +} + +/** + * Return `true` if the type of `x` is `undefined`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = undefined; + * if (is.Undefined(a)) { + * // a is narrowed to undefined + * const _: undefined = a; + * } + * ``` + */ +export function isUndefined(x: unknown): x is undefined { + return typeof x === "undefined"; +} + +/** + * Return `true` if the type of `x` is `null` or `undefined`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = null; + * if (is.Nullish(a)) { + * // a is narrowed to null | undefined + * const _: (null | undefined) = a; + * } + * ``` + */ +export function isNullish(x: unknown): x is null | undefined { + return x == null; +} + +/** + * Return `true` if the type of `x` is `symbol`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = Symbol("symbol"); + * if (is.Symbol(a)) { + * // a is narrowed to symbol + * const _: symbol = a; + * } + * ``` + */ +export function isSymbol(x: unknown): x is symbol { + return typeof x === "symbol"; +} + +export type Primitive = + | string + | number + | bigint + | boolean + | null + | undefined + | symbol; + +/** + * Return `true` if the type of `x` is `Primitive`. + * + * ```ts + * import { is, Primitive } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const a: unknown = 0; + * if (is.Primitive(a)) { + * // a is narrowed to Primitive + * const _: Primitive = a; + * } + * ``` + */ +export function isPrimitive(x: unknown): x is Primitive { + return x == null || primitiveSet.has(typeof x); +} + +const primitiveSet = new Set([ + "string", + "number", + "bigint", + "boolean", + "symbol", +]); + +export default { + Any: isAny, + Array: isArray, + AsyncFunction: isAsyncFunction, + BigInt: isBigInt, + Boolean: isBoolean, + Function: isFunction, + Map: isMap, + Null: isNull, + Nullish: isNullish, + Number: isNumber, + Primitive: isPrimitive, + Record: isRecord, + Set: isSet, + String: isString, + Symbol: isSymbol, + SyncFunction: isSyncFunction, + Undefined: isUndefined, + Unknown: isUnknown, +}; diff --git a/is/core_test.ts b/is/core_test.ts new file mode 100644 index 0000000..2beccf8 --- /dev/null +++ b/is/core_test.ts @@ -0,0 +1,223 @@ +import { + assertEquals, + assertStrictEquals, +} from "https://deno.land/std@0.211.0/assert/mod.ts"; +import { stringify } from "./_testutil.ts"; +import { type Predicate } from "./type.ts"; +import is, { + isAny, + isArray, + isAsyncFunction, + isBigInt, + isBoolean, + isFunction, + isMap, + isNull, + isNullish, + isNumber, + isPrimitive, + isRecord, + isSet, + isString, + isSymbol, + isSyncFunction, + isUndefined, + isUnknown, +} from "./core.ts"; + +const examples = { + string: ["", "Hello world"], + number: [0, 1234567890], + bigint: [0n, 1234567890n], + boolean: [true, false], + array: [[], [0, 1, 2], ["a", "b", "c"], [0, "a", true]], + set: [new Set(), new Set([0, 1, 2]), new Set(["a", "b", "c"])], + record: [{}, { a: 0, b: 1, c: 2 }, { a: "a", b: "b", c: "c" }], + map: [ + new Map(), + new Map([["a", 0], ["b", 1], ["c", 2]]), + new Map([["a", "a"], ["b", "b"], ["c", "c"]]), + ], + syncFunction: [function a() {}, () => {}], + asyncFunction: [async function b() {}, async () => {}], + null: [null], + undefined: [undefined], + symbol: [Symbol("a"), Symbol("b"), Symbol("c")], + date: [new Date(1690248225000), new Date(0)], + promise: [new Promise(() => {})], +} as const; + +async function testWithExamples( + t: Deno.TestContext, + pred: Predicate, + opts?: { + validExamples?: (keyof typeof examples)[]; + excludeExamples?: (keyof typeof examples)[]; + }, +): Promise { + const { validExamples = [], excludeExamples = [] } = opts ?? {}; + const exampleEntries = (Object.entries(examples) as unknown as [ + name: keyof typeof examples, + example: unknown[], + ][]).filter(([k]) => !excludeExamples.includes(k)); + for (const [name, example] of exampleEntries) { + const expect = validExamples.includes(name); + for (const v of example) { + await t.step( + `returns ${expect} on ${stringify(v)}`, + () => { + assertEquals(pred(v), expect); + }, + ); + } + } +} + +Deno.test("isAny", async (t) => { + await testWithExamples(t, isAny, { + validExamples: [ + "string", + "number", + "bigint", + "boolean", + "array", + "set", + "record", + "map", + "syncFunction", + "asyncFunction", + "null", + "undefined", + "symbol", + "date", + "promise", + ], + }); +}); + +Deno.test("isUnknown", async (t) => { + await testWithExamples(t, isUnknown, { + validExamples: [ + "string", + "number", + "bigint", + "boolean", + "array", + "set", + "record", + "map", + "syncFunction", + "asyncFunction", + "null", + "undefined", + "symbol", + "date", + "promise", + ], + }); +}); + +Deno.test("isString", async (t) => { + await testWithExamples(t, isString, { validExamples: ["string"] }); +}); + +Deno.test("isNumber", async (t) => { + await testWithExamples(t, isNumber, { validExamples: ["number"] }); +}); + +Deno.test("isBigInt", async (t) => { + await testWithExamples(t, isBigInt, { validExamples: ["bigint"] }); +}); + +Deno.test("isBoolean", async (t) => { + await testWithExamples(t, isBoolean, { validExamples: ["boolean"] }); +}); + +Deno.test("isArray", async (t) => { + await testWithExamples(t, isArray, { validExamples: ["array"] }); +}); + +Deno.test("isSet", async (t) => { + await testWithExamples(t, isSet, { validExamples: ["set"] }); +}); + +Deno.test("isRecord", async (t) => { + await testWithExamples(t, isRecord, { + validExamples: ["record"], + }); +}); + +Deno.test("isMap", async (t) => { + await testWithExamples(t, isMap, { + validExamples: ["map"], + }); +}); + +Deno.test("isFunction", async (t) => { + await testWithExamples(t, isFunction, { + validExamples: ["syncFunction", "asyncFunction"], + }); +}); + +Deno.test("isSyncFunction", async (t) => { + await testWithExamples(t, isSyncFunction, { + validExamples: ["syncFunction"], + }); +}); + +Deno.test("isAsyncFunction", async (t) => { + await testWithExamples(t, isAsyncFunction, { + validExamples: ["asyncFunction"], + }); +}); + +Deno.test("isNull", async (t) => { + await testWithExamples(t, isNull, { validExamples: ["null"] }); +}); + +Deno.test("isUndefined", async (t) => { + await testWithExamples(t, isUndefined, { validExamples: ["undefined"] }); +}); + +Deno.test("isNullish", async (t) => { + await testWithExamples(t, isNullish, { + validExamples: ["null", "undefined"], + }); +}); + +Deno.test("isSymbol", async (t) => { + await testWithExamples(t, isSymbol, { validExamples: ["symbol"] }); +}); + +Deno.test("isPrimitive", async (t) => { + await testWithExamples(t, isPrimitive, { + validExamples: [ + "string", + "number", + "bigint", + "boolean", + "null", + "undefined", + "symbol", + ], + }); +}); + +Deno.test("is", async (t) => { + const mod = await import("./core.ts"); + const casesOfAliasAndIsFunction = Object.entries(mod) + .filter(([k, _]) => k.startsWith("is")) + .map(([k, v]) => [k.slice(2), v] as const); + for (const [alias, fn] of casesOfAliasAndIsFunction) { + await t.step(`defines \`${alias}\` function`, () => { + assertStrictEquals(is[alias as keyof typeof is], fn); + }); + } + await t.step( + "only has entries that are the same as the `is*` function aliases", + () => { + const aliases = casesOfAliasAndIsFunction.map(([a]) => a).sort(); + assertEquals(Object.keys(is).sort(), aliases); + }, + ); +}); diff --git a/is/factory.ts b/is/factory.ts new file mode 100644 index 0000000..06520c8 --- /dev/null +++ b/is/factory.ts @@ -0,0 +1,689 @@ +import type { FlatType } from "../_typeutil.ts"; +import type { Predicate, PredicateType } from "./type.ts"; +import { + isAny, + isArray, + isMap, + isRecord, + isSet, + type Primitive, +} from "./core.ts"; +import { inspect } from "../inspect.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `T[]`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.ArrayOf(is.String); + * const a: unknown = ["a", "b", "c"]; + * if (isMyType(a)) { + * // a is narrowed to string[] + * const _: string[] = a; + * } + * ``` + */ +export function isArrayOf( + pred: Predicate, +): Predicate { + return Object.defineProperties( + (x: unknown): x is T[] => isArray(x) && x.every(pred), + { + name: { + get: () => `isArrayOf(${inspect(pred)})`, + }, + }, + ); +} + +/** + * Return a type predicate function that returns `true` if the type of `x` is `Set`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.SetOf(is.String); + * const a: unknown = new Set(["a", "b", "c"]); + * if (isMyType(a)) { + * // a is narrowed to Set + * const _: Set = a; + * } + * ``` + */ +export function isSetOf( + pred: Predicate, +): Predicate> { + return Object.defineProperties( + (x: unknown): x is Set => { + if (!isSet(x)) return false; + for (const v of x.values()) { + if (!pred(v)) return false; + } + return true; + }, + { + name: { + get: () => `isSetOf(${inspect(pred)})`, + }, + }, + ); +} + +type TupleOf = { + -readonly [P in keyof T]: T[P] extends Predicate ? U : never; +}; + +type ReadonlyTupleOf = { + [P in keyof T]: T[P] extends Predicate ? U : never; +}; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `TupleOf` or `TupleOf`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.TupleOf([is.Number, is.String, is.Boolean]); + * const a: unknown = [0, "a", true]; + * if (isMyType(a)) { + * // a is narrowed to [number, string, boolean] + * const _: [number, string, boolean] = a; + * } + * ``` + * + * With `predElse`: + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.TupleOf( + * [is.Number, is.String, is.Boolean], + * is.ArrayOf(is.Number), + * ); + * const a: unknown = [0, "a", true, 0, 1, 2]; + * if (isMyType(a)) { + * // a is narrowed to [number, string, boolean, ...number[]] + * const _: [number, string, boolean, ...number[]] = a; + * } + * ``` + * + * Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array + * used as `predTup`. If a type error occurs, try adding `as const` as follows: + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const predTup = [is.Number, is.String, is.Boolean] as const; + * const isMyType = is.TupleOf(predTup); + * const a: unknown = [0, "a", true]; + * if (isMyType(a)) { + * // a is narrowed to [number, string, boolean] + * const _: [number, string, boolean] = a; + * } + * ``` + */ +export function isTupleOf< + T extends readonly [Predicate, ...Predicate[]], +>( + predTup: T, +): Predicate>; +export function isTupleOf< + T extends readonly [Predicate, ...Predicate[]], + E extends Predicate, +>( + predTup: T, + predElse: E, +): Predicate<[...TupleOf, ...PredicateType]>; +export function isTupleOf< + T extends readonly [Predicate, ...Predicate[]], + E extends Predicate, +>( + predTup: T, + predElse?: E, +): Predicate | [...TupleOf, ...PredicateType]> { + if (!predElse) { + return Object.defineProperties( + (x: unknown): x is TupleOf => { + if (!isArray(x) || x.length !== predTup.length) { + return false; + } + return predTup.every((pred, i) => pred(x[i])); + }, + { + name: { + get: () => `isTupleOf(${inspect(predTup)})`, + }, + }, + ); + } else { + return Object.defineProperties( + (x: unknown): x is [...TupleOf, ...PredicateType] => { + if (!isArray(x) || x.length < predTup.length) { + return false; + } + const head = x.slice(0, predTup.length); + const tail = x.slice(predTup.length); + return predTup.every((pred, i) => pred(head[i])) && predElse(tail); + }, + { + name: { + get: () => `isTupleOf(${inspect(predTup)}, ${inspect(predElse)})`, + }, + }, + ); + } +} + +/** + * Return a type predicate function that returns `true` if the type of `x` is `ReadonlyTupleOf`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.ReadonlyTupleOf([is.Number, is.String, is.Boolean]); + * const a: unknown = [0, "a", true]; + * if (isMyType(a)) { + * // a is narrowed to readonly [number, string, boolean] + * const _: readonly [number, string, boolean] = a; + * } + * ``` + * + * With `predElse`: + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.ReadonlyTupleOf( + * [is.Number, is.String, is.Boolean], + * is.ArrayOf(is.Number), + * ); + * const a: unknown = [0, "a", true, 0, 1, 2]; + * if (isMyType(a)) { + * // a is narrowed to readonly [number, string, boolean, ...number[]] + * const _: readonly [number, string, boolean, ...number[]] = a; + * } + * ``` + * + * Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array + * used as `predTup`. If a type error occurs, try adding `as const` as follows: + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const predTup = [is.Number, is.String, is.Boolean] as const; + * const isMyType = is.ReadonlyTupleOf(predTup); + * const a: unknown = [0, "a", true]; + * if (isMyType(a)) { + * // a is narrowed to readonly [number, string, boolean] + * const _: readonly [number, string, boolean] = a; + * } + * ``` + */ +export function isReadonlyTupleOf< + T extends readonly [Predicate, ...Predicate[]], +>( + predTup: T, +): Predicate>; +export function isReadonlyTupleOf< + T extends readonly [Predicate, ...Predicate[]], + E extends Predicate, +>( + predTup: T, + predElse: E, +): Predicate, ...PredicateType]>; +export function isReadonlyTupleOf< + T extends readonly [Predicate, ...Predicate[]], + E extends Predicate, +>( + predTup: T, + predElse?: E, +): Predicate< + ReadonlyTupleOf | readonly [...ReadonlyTupleOf, ...PredicateType] +> { + if (!predElse) { + return Object.defineProperties( + isTupleOf(predTup) as Predicate>, + { + name: { + get: () => `isReadonlyTupleOf(${inspect(predTup)})`, + }, + }, + ); + } else { + return Object.defineProperties( + isTupleOf(predTup, predElse) as unknown as Predicate< + readonly [...ReadonlyTupleOf, ...PredicateType] + >, + { + name: { + get: () => + `isReadonlyTupleOf(${inspect(predTup)}, ${inspect(predElse)})`, + }, + }, + ); + } +} + +// https://stackoverflow.com/a/71700658/1273406 +type UniformTupleOf< + T, + N extends number, + R extends readonly T[] = [], +> = R["length"] extends N ? R : UniformTupleOf; + +type ReadonlyUniformTupleOf< + T, + N extends number, + R extends readonly T[] = [], +> = R["length"] extends N ? R + : ReadonlyUniformTupleOf; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `UniformTupleOf`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.UniformTupleOf(5); + * const a: unknown = [0, 1, 2, 3, 4]; + * if (isMyType(a)) { + * // a is narrowed to [unknown, unknown, unknown, unknown, unknown] + * const _: [unknown, unknown, unknown, unknown, unknown] = a; + * } + * ``` + * + * With predicate function: + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.UniformTupleOf(5, is.Number); + * const a: unknown = [0, 1, 2, 3, 4]; + * if (isMyType(a)) { + * // a is narrowed to [number, number, number, number, number] + * const _: [number, number, number, number, number] = a; + * } + * ``` + */ +export function isUniformTupleOf( + n: N, + pred: Predicate = isAny, +): Predicate> { + return Object.defineProperties( + (x: unknown): x is UniformTupleOf => { + if (!isArray(x) || x.length !== n) { + return false; + } + return x.every((v) => pred(v)); + }, + { + name: { + get: () => `isUniformTupleOf(${n}, ${inspect(pred)})`, + }, + }, + ); +} + +/** + * Return a type predicate function that returns `true` if the type of `x` is `ReadonlyUniformTupleOf`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.ReadonlyUniformTupleOf(5); + * const a: unknown = [0, 1, 2, 3, 4]; + * if (isMyType(a)) { + * // a is narrowed to readonly [unknown, unknown, unknown, unknown, unknown] + * const _: readonly [unknown, unknown, unknown, unknown, unknown] = a; + * } + * ``` + * + * With predicate function: + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.ReadonlyUniformTupleOf(5, is.Number); + * const a: unknown = [0, 1, 2, 3, 4]; + * if (isMyType(a)) { + * // a is narrowed to readonly [number, number, number, number, number] + * const _: readonly [number, number, number, number, number] = a; + * } + * ``` + */ +export function isReadonlyUniformTupleOf( + n: N, + pred: Predicate = isAny, +): Predicate> { + return Object.defineProperties( + isUniformTupleOf(n, pred) as Predicate>, + { + name: { + get: () => `isReadonlyUniformTupleOf(${n}, ${inspect(pred)})`, + }, + }, + ); +} + +/** + * Return a type predicate function that returns `true` if the type of `x` is `Record`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.RecordOf(is.Number); + * const a: unknown = {"a": 0, "b": 1}; + * if (isMyType(a)) { + * // a is narrowed to Record + * const _: Record = a; + * } + * ``` + * + * With predicate function for keys: + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.RecordOf(is.Number, is.String); + * const a: unknown = {"a": 0, "b": 1}; + * if (isMyType(a)) { + * // a is narrowed to Record + * const _: Record = a; + * } + * ``` + */ +export function isRecordOf( + pred: Predicate, + predKey?: Predicate, +): Predicate> { + return Object.defineProperties( + (x: unknown): x is Record => { + if (!isRecord(x)) return false; + for (const k in x) { + if (!pred(x[k])) return false; + if (predKey && !predKey(k)) return false; + } + return true; + }, + { + name: { + get: predKey + ? () => `isRecordOf(${inspect(pred)}, ${inspect(predKey)})` + : () => `isRecordOf(${inspect(pred)})`, + }, + }, + ); +} + +/** + * Return a type predicate function that returns `true` if the type of `x` is `Map`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.MapOf(is.Number); + * const a: unknown = new Map([["a", 0], ["b", 1]]); + * if (isMyType(a)) { + * // a is narrowed to Map + * const _: Map = a; + * } + * ``` + * + * With predicate function for keys: + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.MapOf(is.Number, is.String); + * const a: unknown = new Map([["a", 0], ["b", 1]]); + * if (isMyType(a)) { + * // a is narrowed to Map + * const _: Map = a; + * } + * ``` + */ +export function isMapOf( + pred: Predicate, + predKey?: Predicate, +): Predicate> { + return Object.defineProperties( + (x: unknown): x is Map => { + if (!isMap(x)) return false; + for (const entry of x.entries()) { + const [k, v] = entry; + if (!pred(v)) return false; + if (predKey && !predKey(k)) return false; + } + return true; + }, + { + name: { + get: predKey + ? () => `isMapOf(${inspect(pred)}, ${inspect(predKey)})` + : () => `isMapOf(${inspect(pred)})`, + }, + }, + ); +} + +type ObjectOf>> = FlatType< + // Non optional + & { + [K in keyof T as T[K] extends Optional ? never : K]: T[K] extends + Predicate ? U : never; + } + // Optional + & { + [K in keyof T as T[K] extends Optional ? K : never]?: T[K] extends + Predicate ? U : never; + } +>; + +export type Optional = { + optional: true; +}; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `ObjectOf`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * If `is.OptionalOf()` is specified in the predicate function, the property becomes optional. + * + * When `options.strict` is `true`, the number of keys of `x` must be equal to the number of keys of `predObj`. + * Otherwise, the number of keys of `x` must be greater than or equal to the number of keys of `predObj`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.ObjectOf({ + * a: is.Number, + * b: is.String, + * c: is.OptionalOf(is.Boolean), + * }); + * const a: unknown = { a: 0, b: "a", other: "other" }; + * if (isMyType(a)) { + * // "other" key in `a` is ignored because of `options.strict` is `false`. + * // a is narrowed to { a: number; b: string; c?: boolean | undefined } + * const _: { a: number; b: string; c?: boolean | undefined } = a; + * } + * ``` + * + * With `options.strict`: + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.ObjectOf({ + * a: is.Number, + * b: is.String, + * c: is.OptionalOf(is.Boolean), + * }, { strict: true }); + * const a: unknown = { a: 0, b: "a", other: "other" }; + * if (isMyType(a)) { + * // This block will not be executed because of "other" key in `a`. + * } + * ``` + */ +export function isObjectOf< + T extends Record>, +>( + predObj: T, + { strict }: { strict?: boolean } = {}, +): Predicate> { + return Object.defineProperties( + strict ? isObjectOfStrict(predObj) : isObjectOfLoose(predObj), + { + name: { + get: () => `isObjectOf(${inspect(predObj)})`, + }, + }, + ); +} + +function isObjectOfLoose< + T extends Record>, +>( + predObj: T, +): Predicate> { + return (x: unknown): x is ObjectOf => { + if (!isRecord(x)) return false; + for (const k in predObj) { + if (!predObj[k](x[k])) return false; + } + return true; + }; +} + +function isObjectOfStrict< + T extends Record>, +>( + predObj: T, +): Predicate> { + const keys = new Set(Object.keys(predObj)); + const pred = isObjectOfLoose(predObj); + return (x: unknown): x is ObjectOf => { + if (!pred(x)) return false; + const ks = Object.keys(x); + return ks.length <= keys.size && ks.every((k) => keys.has(k)); + }; +} + +/** + * Return `true` if the type of `x` is instance of `ctor`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.InstanceOf(Date); + * const a: unknown = new Date(); + * if (isMyType(a)) { + * // a is narrowed to Date + * const _: Date = a; + * } + * ``` + */ +// deno-lint-ignore no-explicit-any +export function isInstanceOf unknown>( + ctor: T, +): Predicate> { + return Object.defineProperties( + (x: unknown): x is InstanceType => x instanceof ctor, + { + name: { + get: () => `isInstanceOf(${inspect(ctor)})`, + }, + }, + ); +} + +/** + * Return a type predicate function that returns `true` if the type of `x` is a literal type of `pred`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.LiteralOf("hello"); + * const a: unknown = "hello"; + * if (isMyType(a)) { + * // a is narrowed to "hello" + * const _: "hello" = a; + * } + * ``` + */ +export function isLiteralOf(literal: T): Predicate { + return Object.defineProperties( + (x: unknown): x is T => x === literal, + { + name: { + get: () => `isLiteralOf(${inspect(literal)})`, + }, + }, + ); +} + +/** + * Return a type predicate function that returns `true` if the type of `x` is one of literal type in `preds`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.LiteralOneOf(["hello", "world"] as const); + * const a: unknown = "hello"; + * if (isMyType(a)) { + * // a is narrowed to "hello" | "world" + * const _: "hello" | "world" = a; + * } + * ``` + */ +export function isLiteralOneOf( + literals: T, +): Predicate { + const s = new Set(literals); + return Object.defineProperties( + (x: unknown): x is T[number] => s.has(x as T[number]), + { + name: { + get: () => `isLiteralOneOf(${inspect(literals)})`, + }, + }, + ); +} + +export default { + ArrayOf: isArrayOf, + InstanceOf: isInstanceOf, + LiteralOf: isLiteralOf, + LiteralOneOf: isLiteralOneOf, + MapOf: isMapOf, + ObjectOf: isObjectOf, + ReadonlyTupleOf: isReadonlyTupleOf, + ReadonlyUniformTupleOf: isReadonlyUniformTupleOf, + RecordOf: isRecordOf, + SetOf: isSetOf, + TupleOf: isTupleOf, + UniformTupleOf: isUniformTupleOf, +}; diff --git a/is_test.ts b/is/factory_test.ts similarity index 63% rename from is_test.ts rename to is/factory_test.ts index 56b2515..669b4d8 100644 --- a/is_test.ts +++ b/is/factory_test.ts @@ -6,48 +6,23 @@ import { assertSnapshot, } from "https://deno.land/std@0.211.0/testing/snapshot.ts"; import { assertType } from "https://deno.land/std@0.211.0/testing/types.ts"; +import { type Equal, stringify } from "./_testutil.ts"; +import { type Predicate } from "./type.ts"; +import { isArray, isBoolean, isNumber, isString } from "./core.ts"; import is, { - isAllOf, - isAny, - isArray, isArrayOf, - isAsyncFunction, - isBigInt, - isBoolean, - isFunction, isInstanceOf, isLiteralOf, isLiteralOneOf, - isMap, isMapOf, - isNull, - isNullish, - isNumber, isObjectOf, - isOneOf, - isOptionalOf, - isPrimitive, isReadonlyTupleOf, isReadonlyUniformTupleOf, - isRecord, isRecordOf, - isSet, isSetOf, - isString, - isSymbol, - isSyncFunction, isTupleOf, - isUndefined, isUniformTupleOf, - isUnknown, - Predicate, - PredicateType, -} from "./is.ts"; - -// It seems 'IsExact' in deno_std is false positive so use `Equal` in type-challenges -// https://github.com/type-challenges/type-challenges/blob/e77262dba62e9254451f661cb4fe5517ffd1d933/utils/index.d.ts#L7-L9 -type Equal = (() => T extends X ? 1 : 2) extends - (() => T extends Y ? 1 : 2) ? true : false; +} from "./factory.ts"; const examples = { string: ["", "Hello world"], @@ -71,15 +46,6 @@ const examples = { promise: [new Promise(() => {})], } as const; -function stringify(x: unknown): string { - if (x instanceof Date) return `Date(${x.valueOf()})`; - if (x instanceof Promise) return "Promise"; - if (typeof x === "function") return x.toString(); - if (typeof x === "bigint") return `${x}n`; - if (typeof x === "symbol") return x.toString(); - return JSON.stringify(x); -} - async function testWithExamples( t: Deno.TestContext, pred: Predicate, @@ -106,91 +72,6 @@ async function testWithExamples( } } -Deno.test("PredicateType", () => { - const isArticle = is.ObjectOf({ - title: is.String, - body: is.String, - refs: is.ArrayOf(is.OneOf([ - is.String, - is.ObjectOf({ - name: is.String, - url: is.String, - }), - ])), - }); - assertType< - Equal, { - title: string; - body: string; - refs: (string | { name: string; url: string })[]; - }> - >(true); -}); - -Deno.test("isAny", async (t) => { - await testWithExamples(t, isAny, { - validExamples: [ - "string", - "number", - "bigint", - "boolean", - "array", - "set", - "record", - "map", - "syncFunction", - "asyncFunction", - "null", - "undefined", - "symbol", - "date", - "promise", - ], - }); -}); - -Deno.test("isUnknown", async (t) => { - await testWithExamples(t, isUnknown, { - validExamples: [ - "string", - "number", - "bigint", - "boolean", - "array", - "set", - "record", - "map", - "syncFunction", - "asyncFunction", - "null", - "undefined", - "symbol", - "date", - "promise", - ], - }); -}); - -Deno.test("isString", async (t) => { - await testWithExamples(t, isString, { validExamples: ["string"] }); -}); - -Deno.test("isNumber", async (t) => { - await testWithExamples(t, isNumber, { validExamples: ["number"] }); -}); - -Deno.test("isBigInt", async (t) => { - await testWithExamples(t, isBigInt, { validExamples: ["bigint"] }); -}); - -Deno.test("isBoolean", async (t) => { - await testWithExamples(t, isBoolean, { validExamples: ["boolean"] }); -}); - -Deno.test("isArray", async (t) => { - await testWithExamples(t, isArray, { validExamples: ["array"] }); -}); - Deno.test("isArrayOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot(t, isArrayOf(isNumber).name); @@ -217,10 +98,6 @@ Deno.test("isArrayOf", async (t) => { }); }); -Deno.test("isSet", async (t) => { - await testWithExamples(t, isSet, { validExamples: ["set"] }); -}); - Deno.test("isSetOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot(t, isSetOf(isNumber).name); @@ -289,11 +166,11 @@ Deno.test("isTupleOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot( t, - isTupleOf([isNumber, isString, isBoolean], is.Array).name, + isTupleOf([isNumber, isString, isBoolean], isArray).name, ); await assertSnapshot( t, - isTupleOf([(_x): _x is string => false], is.ArrayOf(is.String)) + isTupleOf([(_x): _x is string => false], isArrayOf(isString)) .name, ); // Nested @@ -301,15 +178,15 @@ Deno.test("isTupleOf", async (t) => { t, isTupleOf([ isTupleOf( - [isTupleOf([isNumber, isString, isBoolean], is.Array)], - is.Array, + [isTupleOf([isNumber, isString, isBoolean], isArray)], + isArray, ), ]).name, ); }); await t.step("returns proper type predicate", () => { const predTup = [isNumber, isString, isBoolean] as const; - const predElse = is.ArrayOf(is.Number); + const predElse = isArrayOf(isNumber); const a: unknown = [0, "a", true, 0, 1, 2]; if (isTupleOf(predTup, predElse)(a)) { assertType>( @@ -319,12 +196,12 @@ Deno.test("isTupleOf", async (t) => { }); await t.step("returns true on T tuple", () => { const predTup = [isNumber, isString, isBoolean] as const; - const predElse = is.ArrayOf(is.Number); + const predElse = isArrayOf(isNumber); assertEquals(isTupleOf(predTup, predElse)([0, "a", true, 0, 1, 2]), true); }); await t.step("returns false on non T tuple", () => { const predTup = [isNumber, isString, isBoolean] as const; - const predElse = is.ArrayOf(is.String); + const predElse = isArrayOf(isString); assertEquals(isTupleOf(predTup, predElse)([0, 1, 2, 0, 1, 2]), false); assertEquals(isTupleOf(predTup, predElse)([0, "a", 0, 1, 2]), false); assertEquals( @@ -333,7 +210,7 @@ Deno.test("isTupleOf", async (t) => { ); assertEquals(isTupleOf(predTup, predElse)([0, "a", true, 0, 1, 2]), false); }); - const predElse = is.Array; + const predElse = isArray; await testWithExamples( t, isTupleOf([(_: unknown): _ is unknown => true], predElse), @@ -391,14 +268,14 @@ Deno.test("isReadonlyTupleOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot( t, - isReadonlyTupleOf([isNumber, isString, isBoolean], is.Array) + isReadonlyTupleOf([isNumber, isString, isBoolean], isArray) .name, ); await assertSnapshot( t, isReadonlyTupleOf( [(_x): _x is string => false], - is.ArrayOf(is.String), + isArrayOf(isString), ).name, ); // Nested @@ -406,14 +283,14 @@ Deno.test("isReadonlyTupleOf", async (t) => { t, isReadonlyTupleOf([ isReadonlyTupleOf([ - isReadonlyTupleOf([isNumber, isString, isBoolean], is.Array), - ], is.Array), - ], is.Array).name, + isReadonlyTupleOf([isNumber, isString, isBoolean], isArray), + ], isArray), + ], isArray).name, ); }); await t.step("returns proper type predicate", () => { const predTup = [isNumber, isString, isBoolean] as const; - const predElse = is.ArrayOf(is.Number); + const predElse = isArrayOf(isNumber); const a: unknown = [0, "a", true, 0, 1, 2]; if (isReadonlyTupleOf(predTup, predElse)(a)) { assertType< @@ -423,7 +300,7 @@ Deno.test("isReadonlyTupleOf", async (t) => { }); await t.step("returns true on T tuple", () => { const predTup = [isNumber, isString, isBoolean] as const; - const predElse = is.ArrayOf(is.Number); + const predElse = isArrayOf(isNumber); assertEquals( isReadonlyTupleOf(predTup, predElse)([0, "a", true, 0, 1, 2]), true, @@ -431,7 +308,7 @@ Deno.test("isReadonlyTupleOf", async (t) => { }); await t.step("returns false on non T tuple", () => { const predTup = [isNumber, isString, isBoolean] as const; - const predElse = is.ArrayOf(is.String); + const predElse = isArrayOf(isString); assertEquals( isReadonlyTupleOf(predTup, predElse)([0, 1, 2, 0, 1, 2]), false, @@ -449,7 +326,7 @@ Deno.test("isReadonlyTupleOf", async (t) => { false, ); }); - const predElse = is.Array; + const predElse = isArray; await testWithExamples( t, isReadonlyTupleOf([(_: unknown): _ is unknown => true], predElse), @@ -484,12 +361,12 @@ Deno.test("isUniformTupleOf", async (t) => { }); await t.step("returns true on mono-typed T tuple", () => { assertEquals(isUniformTupleOf(3)([0, 1, 2]), true); - assertEquals(isUniformTupleOf(3, is.Number)([0, 1, 2]), true); + assertEquals(isUniformTupleOf(3, isNumber)([0, 1, 2]), true); }); await t.step("returns false on non mono-typed T tuple", () => { assertEquals(isUniformTupleOf(4)([0, 1, 2]), false); assertEquals(isUniformTupleOf(4)([0, 1, 2, 3, 4]), false); - assertEquals(isUniformTupleOf(3, is.Number)(["a", "b", "c"]), false); + assertEquals(isUniformTupleOf(3, isNumber)(["a", "b", "c"]), false); }); await testWithExamples(t, isUniformTupleOf(4), { excludeExamples: ["array"], @@ -524,13 +401,13 @@ Deno.test("isReadonlyUniformTupleOf", async (t) => { }); await t.step("returns true on mono-typed T tuple", () => { assertEquals(isReadonlyUniformTupleOf(3)([0, 1, 2]), true); - assertEquals(isReadonlyUniformTupleOf(3, is.Number)([0, 1, 2]), true); + assertEquals(isReadonlyUniformTupleOf(3, isNumber)([0, 1, 2]), true); }); await t.step("returns false on non mono-typed T tuple", () => { assertEquals(isReadonlyUniformTupleOf(4)([0, 1, 2]), false); assertEquals(isReadonlyUniformTupleOf(4)([0, 1, 2, 3, 4]), false); assertEquals( - isReadonlyUniformTupleOf(3, is.Number)(["a", "b", "c"]), + isReadonlyUniformTupleOf(3, isNumber)(["a", "b", "c"]), false, ); }); @@ -539,12 +416,6 @@ Deno.test("isReadonlyUniformTupleOf", async (t) => { }); }); -Deno.test("isRecord", async (t) => { - await testWithExamples(t, isRecord, { - validExamples: ["record"], - }); -}); - Deno.test("isRecordOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot(t, isRecordOf(isNumber).name); @@ -605,12 +476,6 @@ Deno.test("isRecordOf", async (t) => { }); }); -Deno.test("isMap", async (t) => { - await testWithExamples(t, isMap, { - validExamples: ["map"], - }); -}); - Deno.test("isMapOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot(t, isMapOf(isNumber).name); @@ -749,118 +614,6 @@ Deno.test("isObjectOf", async (t) => { isObjectOf({ a: (_: unknown): _ is unknown => false }), { excludeExamples: ["record"] }, ); - await t.step("with optional properties", async (t) => { - await t.step("returns proper type predicate", () => { - const predObj = { - a: isNumber, - b: isOneOf([isString, isUndefined]), - c: isOptionalOf(isBoolean), - }; - const a: unknown = { a: 0, b: "a" }; - if (isObjectOf(predObj)(a)) { - assertType< - Equal - >(true); - } - }); - await t.step("returns true on T object", () => { - const predObj = { - a: isNumber, - b: isOneOf([isString, isUndefined]), - c: isOptionalOf(isBoolean), - }; - assertEquals(isObjectOf(predObj)({ a: 0, b: "a", c: true }), true); - assertEquals( - isObjectOf(predObj)({ a: 0, b: "a" }), - true, - "Object does not have an optional property", - ); - assertEquals( - isObjectOf(predObj)({ a: 0, b: "a", c: undefined }), - true, - "Object has `undefined` as value of optional property", - ); - assertEquals( - isObjectOf(predObj, { strict: true })({ a: 0, b: "a", c: true }), - true, - "Specify `{ strict: true }`", - ); - assertEquals( - isObjectOf(predObj, { strict: true })({ a: 0, b: "a" }), - true, - "Specify `{ strict: true }` and object does not have one optional property", - ); - }); - await t.step("returns false on non T object", () => { - const predObj = { - a: isNumber, - b: isOneOf([isString, isUndefined]), - c: isOptionalOf(isBoolean), - }; - assertEquals( - isObjectOf(predObj)({ a: 0, b: "a", c: "" }), - false, - "Object have a different type property", - ); - assertEquals( - isObjectOf(predObj)({ a: 0, b: "a", c: null }), - false, - "Object has `null` as value of optional property", - ); - assertEquals( - isObjectOf(predObj, { strict: true })({ - a: 0, - b: "a", - c: true, - d: "invalid", - }), - false, - "Specify `{ strict: true }` and object have an unknown property", - ); - assertEquals( - isObjectOf(predObj, { strict: true })({ - a: 0, - b: "a", - d: "invalid", - }), - false, - "Specify `{ strict: true }` and object have the same number of properties but an unknown property exists", - ); - }); - }); -}); - -Deno.test("isFunction", async (t) => { - await testWithExamples(t, isFunction, { - validExamples: ["syncFunction", "asyncFunction"], - }); - assertType< - Equal, (...args: unknown[]) => unknown> - >(true); -}); - -Deno.test("isSyncFunction", async (t) => { - await testWithExamples(t, isSyncFunction, { - validExamples: ["syncFunction"], - }); - assertType< - Equal< - PredicateType, - (...args: unknown[]) => unknown - > - >(true); -}); - -Deno.test("isAsyncFunction", async (t) => { - await testWithExamples(t, isAsyncFunction, { - validExamples: ["asyncFunction"], - }); - assertType< - Equal< - PredicateType, - (...args: unknown[]) => Promise - > - >(true); }); Deno.test("isInstanceOf", async (t) => { @@ -905,38 +658,6 @@ Deno.test("isInstanceOf", async (t) => { }); }); -Deno.test("isNull", async (t) => { - await testWithExamples(t, isNull, { validExamples: ["null"] }); -}); - -Deno.test("isUndefined", async (t) => { - await testWithExamples(t, isUndefined, { validExamples: ["undefined"] }); -}); - -Deno.test("isNullish", async (t) => { - await testWithExamples(t, isNullish, { - validExamples: ["null", "undefined"], - }); -}); - -Deno.test("isSymbol", async (t) => { - await testWithExamples(t, isSymbol, { validExamples: ["symbol"] }); -}); - -Deno.test("isPrimitive", async (t) => { - await testWithExamples(t, isPrimitive, { - validExamples: [ - "string", - "number", - "bigint", - "boolean", - "null", - "undefined", - "symbol", - ], - }); -}); - Deno.test("isLiteralOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot(t, isLiteralOf("hello").name); @@ -986,169 +707,8 @@ Deno.test("isLiteralOneOf", async (t) => { }); }); -Deno.test("isOneOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isOneOf([isNumber, isString, isBoolean]).name); - }); - await t.step("returns proper type predicate", () => { - const preds = [isNumber, isString, isBoolean] as const; - const a: unknown = [0, "a", true]; - if (isOneOf(preds)(a)) { - assertType>(true); - } - }); - await t.step("returns proper type predicate (#49)", () => { - const isFoo = isObjectOf({ foo: isString }); - const isBar = isObjectOf({ foo: isString, bar: isNumber }); - type Foo = PredicateType; - type Bar = PredicateType; - const preds = [isFoo, isBar] as const; - const a: unknown = [0, "a", true]; - if (isOneOf(preds)(a)) { - assertType>(true); - } - }); - await t.step("returns true on one of T", () => { - const preds = [isNumber, isString, isBoolean] as const; - assertEquals(isOneOf(preds)(0), true); - assertEquals(isOneOf(preds)("a"), true); - assertEquals(isOneOf(preds)(true), true); - }); - await t.step("returns false on non of T", async (t) => { - const preds = [isNumber, isString, isBoolean] as const; - await testWithExamples(t, isOneOf(preds), { - excludeExamples: ["number", "string", "boolean"], - }); - }); -}); - -Deno.test("isAllOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot( - t, - isAllOf([ - is.ObjectOf({ a: is.Number }), - is.ObjectOf({ b: is.String }), - ]).name, - ); - }); - await t.step("returns proper type predicate", () => { - const preds = [ - is.ObjectOf({ a: is.Number }), - is.ObjectOf({ b: is.String }), - ] as const; - const a: unknown = { a: 0, b: "a" }; - if (isAllOf(preds)(a)) { - assertType>(true); - } - }); - await t.step("returns true on all of T", () => { - const preds = [ - is.ObjectOf({ a: is.Number }), - is.ObjectOf({ b: is.String }), - ] as const; - assertEquals(isAllOf(preds)({ a: 0, b: "a" }), true); - }); - await t.step("returns false on non of T", async (t) => { - const preds = [ - is.ObjectOf({ a: is.Number }), - is.ObjectOf({ b: is.String }), - ] as const; - assertEquals( - isAllOf(preds)({ a: 0, b: 0 }), - false, - "Some properties has wrong type", - ); - assertEquals( - isAllOf(preds)({ a: 0 }), - false, - "Some properties does not exists", - ); - await testWithExamples(t, isAllOf(preds), { - excludeExamples: ["record"], - }); - }); -}); - -Deno.test("isOptionalOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isOptionalOf(isNumber).name); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = undefined; - if (isOptionalOf(isNumber)(a)) { - assertType>(true); - } - }); - await t.step("with isString", async (t) => { - await testWithExamples(t, isOptionalOf(isString), { - validExamples: ["string", "undefined"], - }); - }); - await t.step("with isNumber", async (t) => { - await testWithExamples(t, isOptionalOf(isNumber), { - validExamples: ["number", "undefined"], - }); - }); - await t.step("with isBigInt", async (t) => { - await testWithExamples(t, isOptionalOf(isBigInt), { - validExamples: ["bigint", "undefined"], - }); - }); - await t.step("with isBoolean", async (t) => { - await testWithExamples(t, isOptionalOf(isBoolean), { - validExamples: ["boolean", "undefined"], - }); - }); - await t.step("with isArray", async (t) => { - await testWithExamples(t, isOptionalOf(isArray), { - validExamples: ["array", "undefined"], - }); - }); - await t.step("with isSet", async (t) => { - await testWithExamples(t, isOptionalOf(isSet), { - validExamples: ["set", "undefined"], - }); - }); - await t.step("with isRecord", async (t) => { - await testWithExamples(t, isOptionalOf(isRecord), { - validExamples: ["record", "undefined"], - }); - }); - await t.step("with isFunction", async (t) => { - await testWithExamples(t, isOptionalOf(isFunction), { - validExamples: ["syncFunction", "asyncFunction", "undefined"], - }); - }); - await t.step("with isSyncFunction", async (t) => { - await testWithExamples(t, isOptionalOf(isSyncFunction), { - validExamples: ["syncFunction", "undefined"], - }); - }); - await t.step("with isAsyncFunction", async (t) => { - await testWithExamples(t, isOptionalOf(isAsyncFunction), { - validExamples: ["asyncFunction", "undefined"], - }); - }); - await t.step("with isNull", async (t) => { - await testWithExamples(t, isOptionalOf(isNull), { - validExamples: ["null", "undefined"], - }); - }); - await t.step("with isUndefined", async (t) => { - await testWithExamples(t, isOptionalOf(isUndefined), { - validExamples: ["undefined"], - }); - }); - await t.step("with isSymbol", async (t) => { - await testWithExamples(t, isOptionalOf(isSymbol), { - validExamples: ["symbol", "undefined"], - }); - }); -}); - Deno.test("is", async (t) => { - const mod = await import("./is.ts"); + const mod = await import("./factory.ts"); const casesOfAliasAndIsFunction = Object.entries(mod) .filter(([k, _]) => k.startsWith("is")) .map(([k, v]) => [k.slice(2), v] as const); diff --git a/is/type.ts b/is/type.ts new file mode 100644 index 0000000..73cb587 --- /dev/null +++ b/is/type.ts @@ -0,0 +1,26 @@ +/** + * A type predicate function. + */ +export type Predicate = (x: unknown) => x is T; + +/** + * A type predicated by Predicate. + * + * ```ts + * import { is, type PredicateType } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isPerson = is.ObjectOf({ + * name: is.String, + * age: is.Number, + * address: is.OptionalOf(is.String), + * }); + * + * type Person = PredicateType; + * // Above is equivalent to the following type + * // type Person = { + * // name: string; + * // age: number; + * // address: string | undefined; + * // }; + */ +export type PredicateType

= P extends Predicate ? T : never; diff --git a/is/utility.ts b/is/utility.ts new file mode 100644 index 0000000..c6a1f27 --- /dev/null +++ b/is/utility.ts @@ -0,0 +1,147 @@ +import type { UnionToIntersection } from "../_typeutil.ts"; +import type { Predicate } from "./type.ts"; +import { isUndefined } from "./core.ts"; +import { type Optional } from "./factory.ts"; +import { inspect } from "../inspect.ts"; + +type OneOf = T extends readonly [Predicate, ...infer R] + ? U | OneOf + : never; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `OneOf`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.OneOf([is.Number, is.String, is.Boolean]); + * const a: unknown = 0; + * if (isMyType(a)) { + * // a is narrowed to number | string | boolean + * const _: number | string | boolean = a; + * } + * ``` + * + * Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array + * used as `preds`. If a type error occurs, try adding `as const` as follows: + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const preds = [is.Number, is.String, is.Boolean] as const; + * const isMyType = is.OneOf(preds); + * const a: unknown = 0; + * if (isMyType(a)) { + * // a is narrowed to number | string | boolean + * const _: number | string | boolean = a; + * } + * ``` + */ +export function isOneOf< + T extends readonly [Predicate, ...Predicate[]], +>( + preds: T, +): Predicate> { + return Object.defineProperties( + (x: unknown): x is OneOf => preds.some((pred) => pred(x)), + { + name: { + get: () => `isOneOf(${inspect(preds)})`, + }, + }, + ); +} + +type AllOf = UnionToIntersection>; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `AllOf`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.AllOf([ + * is.ObjectOf({ a: is.Number }), + * is.ObjectOf({ b: is.String }), + * ]); + * const a: unknown = { a: 0, b: "a" }; + * if (isMyType(a)) { + * // a is narrowed to { a: number } & { b: string } + * const _: { a: number } & { b: string } = a; + * } + * ``` + * + * Depending on the version of TypeScript and how values are provided, it may be necessary to add `as const` to the array + * used as `preds`. If a type error occurs, try adding `as const` as follows: + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const preds = [ + * is.ObjectOf({ a: is.Number }), + * is.ObjectOf({ b: is.String }), + * ] as const + * const isMyType = is.AllOf(preds); + * const a: unknown = { a: 0, b: "a" }; + * if (isMyType(a)) { + * // a is narrowed to { a: number } & { b: string } + * const _: { a: number } & { b: string } = a; + * } + * ``` + */ +export function isAllOf< + T extends readonly [Predicate, ...Predicate[]], +>( + preds: T, +): Predicate> { + return Object.defineProperties( + (x: unknown): x is AllOf => preds.every((pred) => pred(x)), + { + name: { + get: () => `isAllOf(${inspect(preds)})`, + }, + }, + ); +} + +/** + * Return a type predicate function that returns `true` if the type of `x` is `T` or `undefined`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.OptionalOf(is.String); + * const a: unknown = "a"; + * if (isMyType(a)) { + * // a is narrowed to string | undefined + * const _: string | undefined = a; + * } + * ``` + */ +export function isOptionalOf( + pred: Predicate, +): Predicate & Optional { + return Object.defineProperties( + (x: unknown): x is Predicate => isUndefined(x) || pred(x), + { + optional: { + value: true as const, + }, + name: { + get: () => `isOptionalOf(${inspect(pred)})`, + }, + }, + ) as Predicate & Optional; +} + +export default { + AllOf: isAllOf, + OneOf: isOneOf, + OptionalOf: isOptionalOf, +}; diff --git a/is/utility_test.ts b/is/utility_test.ts new file mode 100644 index 0000000..dbb04c4 --- /dev/null +++ b/is/utility_test.ts @@ -0,0 +1,255 @@ +import { + assertEquals, + assertStrictEquals, +} from "https://deno.land/std@0.211.0/assert/mod.ts"; +import { + assertSnapshot, +} from "https://deno.land/std@0.211.0/testing/snapshot.ts"; +import { assertType } from "https://deno.land/std@0.211.0/testing/types.ts"; +import { type Equal, stringify } from "./_testutil.ts"; +import { type Predicate, type PredicateType } from "./type.ts"; +import { + isArray, + isAsyncFunction, + isBigInt, + isBoolean, + isFunction, + isNull, + isNumber, + isRecord, + isSet, + isString, + isSymbol, + isSyncFunction, + isUndefined, +} from "./core.ts"; +import { isObjectOf } from "./factory.ts"; +import is, { isAllOf, isOneOf, isOptionalOf } from "./utility.ts"; + +const examples = { + string: ["", "Hello world"], + number: [0, 1234567890], + bigint: [0n, 1234567890n], + boolean: [true, false], + array: [[], [0, 1, 2], ["a", "b", "c"], [0, "a", true]], + set: [new Set(), new Set([0, 1, 2]), new Set(["a", "b", "c"])], + record: [{}, { a: 0, b: 1, c: 2 }, { a: "a", b: "b", c: "c" }], + map: [ + new Map(), + new Map([["a", 0], ["b", 1], ["c", 2]]), + new Map([["a", "a"], ["b", "b"], ["c", "c"]]), + ], + syncFunction: [function a() {}, () => {}], + asyncFunction: [async function b() {}, async () => {}], + null: [null], + undefined: [undefined], + symbol: [Symbol("a"), Symbol("b"), Symbol("c")], + date: [new Date(1690248225000), new Date(0)], + promise: [new Promise(() => {})], +} as const; + +async function testWithExamples( + t: Deno.TestContext, + pred: Predicate, + opts?: { + validExamples?: (keyof typeof examples)[]; + excludeExamples?: (keyof typeof examples)[]; + }, +): Promise { + const { validExamples = [], excludeExamples = [] } = opts ?? {}; + const exampleEntries = (Object.entries(examples) as unknown as [ + name: keyof typeof examples, + example: unknown[], + ][]).filter(([k]) => !excludeExamples.includes(k)); + for (const [name, example] of exampleEntries) { + const expect = validExamples.includes(name); + for (const v of example) { + await t.step( + `returns ${expect} on ${stringify(v)}`, + () => { + assertEquals(pred(v), expect); + }, + ); + } + } +} + +Deno.test("isOneOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isOneOf([isNumber, isString, isBoolean]).name); + }); + await t.step("returns proper type predicate", () => { + const preds = [isNumber, isString, isBoolean] as const; + const a: unknown = [0, "a", true]; + if (isOneOf(preds)(a)) { + assertType>(true); + } + }); + await t.step("returns proper type predicate (#49)", () => { + const isFoo = isObjectOf({ foo: isString }); + const isBar = isObjectOf({ foo: isString, bar: isNumber }); + type Foo = PredicateType; + type Bar = PredicateType; + const preds = [isFoo, isBar] as const; + const a: unknown = [0, "a", true]; + if (isOneOf(preds)(a)) { + assertType>(true); + } + }); + await t.step("returns true on one of T", () => { + const preds = [isNumber, isString, isBoolean] as const; + assertEquals(isOneOf(preds)(0), true); + assertEquals(isOneOf(preds)("a"), true); + assertEquals(isOneOf(preds)(true), true); + }); + await t.step("returns false on non of T", async (t) => { + const preds = [isNumber, isString, isBoolean] as const; + await testWithExamples(t, isOneOf(preds), { + excludeExamples: ["number", "string", "boolean"], + }); + }); +}); + +Deno.test("isAllOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot( + t, + isAllOf([ + isObjectOf({ a: isNumber }), + isObjectOf({ b: isString }), + ]).name, + ); + }); + await t.step("returns proper type predicate", () => { + const preds = [ + isObjectOf({ a: isNumber }), + isObjectOf({ b: isString }), + ] as const; + const a: unknown = { a: 0, b: "a" }; + if (isAllOf(preds)(a)) { + assertType>(true); + } + }); + await t.step("returns true on all of T", () => { + const preds = [ + isObjectOf({ a: isNumber }), + isObjectOf({ b: isString }), + ] as const; + assertEquals(isAllOf(preds)({ a: 0, b: "a" }), true); + }); + await t.step("returns false on non of T", async (t) => { + const preds = [ + isObjectOf({ a: isNumber }), + isObjectOf({ b: isString }), + ] as const; + assertEquals( + isAllOf(preds)({ a: 0, b: 0 }), + false, + "Some properties has wrong type", + ); + assertEquals( + isAllOf(preds)({ a: 0 }), + false, + "Some properties does not exists", + ); + await testWithExamples(t, isAllOf(preds), { + excludeExamples: ["record"], + }); + }); +}); + +Deno.test("isOptionalOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isOptionalOf(isNumber).name); + }); + await t.step("returns proper type predicate", () => { + const a: unknown = undefined; + if (isOptionalOf(isNumber)(a)) { + assertType>(true); + } + }); + await t.step("with isString", async (t) => { + await testWithExamples(t, isOptionalOf(isString), { + validExamples: ["string", "undefined"], + }); + }); + await t.step("with isNumber", async (t) => { + await testWithExamples(t, isOptionalOf(isNumber), { + validExamples: ["number", "undefined"], + }); + }); + await t.step("with isBigInt", async (t) => { + await testWithExamples(t, isOptionalOf(isBigInt), { + validExamples: ["bigint", "undefined"], + }); + }); + await t.step("with isBoolean", async (t) => { + await testWithExamples(t, isOptionalOf(isBoolean), { + validExamples: ["boolean", "undefined"], + }); + }); + await t.step("with isArray", async (t) => { + await testWithExamples(t, isOptionalOf(isArray), { + validExamples: ["array", "undefined"], + }); + }); + await t.step("with isSet", async (t) => { + await testWithExamples(t, isOptionalOf(isSet), { + validExamples: ["set", "undefined"], + }); + }); + await t.step("with isRecord", async (t) => { + await testWithExamples(t, isOptionalOf(isRecord), { + validExamples: ["record", "undefined"], + }); + }); + await t.step("with isFunction", async (t) => { + await testWithExamples(t, isOptionalOf(isFunction), { + validExamples: ["syncFunction", "asyncFunction", "undefined"], + }); + }); + await t.step("with isSyncFunction", async (t) => { + await testWithExamples(t, isOptionalOf(isSyncFunction), { + validExamples: ["syncFunction", "undefined"], + }); + }); + await t.step("with isAsyncFunction", async (t) => { + await testWithExamples(t, isOptionalOf(isAsyncFunction), { + validExamples: ["asyncFunction", "undefined"], + }); + }); + await t.step("with isNull", async (t) => { + await testWithExamples(t, isOptionalOf(isNull), { + validExamples: ["null", "undefined"], + }); + }); + await t.step("with isUndefined", async (t) => { + await testWithExamples(t, isOptionalOf(isUndefined), { + validExamples: ["undefined"], + }); + }); + await t.step("with isSymbol", async (t) => { + await testWithExamples(t, isOptionalOf(isSymbol), { + validExamples: ["symbol", "undefined"], + }); + }); +}); + +Deno.test("is", async (t) => { + const mod = await import("./utility.ts"); + const casesOfAliasAndIsFunction = Object.entries(mod) + .filter(([k, _]) => k.startsWith("is")) + .map(([k, v]) => [k.slice(2), v] as const); + for (const [alias, fn] of casesOfAliasAndIsFunction) { + await t.step(`defines \`${alias}\` function`, () => { + assertStrictEquals(is[alias as keyof typeof is], fn); + }); + } + await t.step( + "only has entries that are the same as the `is*` function aliases", + () => { + const aliases = casesOfAliasAndIsFunction.map(([a]) => a).sort(); + assertEquals(Object.keys(is).sort(), aliases); + }, + ); +}); From 1f4e16e722d4a8d0f97eef77a084e977853002bb Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 21:01:49 +0900 Subject: [PATCH 10/31] :+1: Add `metadata` to store metadata of predicates --- is/__snapshots__/factory_test.ts.snap | 164 +++++++------- is/__snapshots__/utility_test.ts.snap | 8 +- is/factory.ts | 300 ++++++++++++++------------ is/utility.ts | 65 +++--- metadata.ts | 37 ++++ 5 files changed, 314 insertions(+), 260 deletions(-) create mode 100644 metadata.ts diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index b6bb894..37480e2 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,119 +1,127 @@ export const snapshot = {}; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; - snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; +snapshot[`isObjectOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +}, undefined)" +`; -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; +snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a}, undefined)"`; -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ +snapshot[`isObjectOf > returns properly named function 3`] = ` +"isObjectOf({ + a: isObjectOf({ + b: isObjectOf({c: isBoolean}, undefined) + }, undefined) +}, undefined)" +`; + +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; + +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; + +snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; + +snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; + +snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; + +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyTupleOf([ isNumber, isString, isBoolean -], isArray)" +])" `; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyTupleOf([ + isReadonlyTupleOf([ + isReadonlyTupleOf([ isNumber, isString, isBoolean - ], isArray) - ], isArray) + ]) + ]) ])" `; -snapshot[`isObjectOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - b: isString, - c: isBoolean -})" +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyTupleOf([ + isNumber, + isString, + isBoolean +], isArray)" `; -snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isObjectOf > returns properly named function 3`] = ` -"isObjectOf({ - a: isObjectOf({ - b: isObjectOf({c: isBoolean}) - }) -})" +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyTupleOf([ + isReadonlyTupleOf([ + isReadonlyTupleOf([ + isNumber, + isString, + isBoolean + ], isArray) + ], isArray) +], isArray)" `; -snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; - -snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; - snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; -snapshot[`isTupleOf > returns properly named function 1`] = ` +snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ isNumber, isString, isBoolean -])" +], isArray)" `; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` +snapshot[`isTupleOf > returns properly named function 3`] = ` "isTupleOf([ isTupleOf([ isTupleOf([ isNumber, isString, isBoolean - ]) - ]) + ], isArray) + ], isArray) ])" `; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous))"`; - -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber)"`; - -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous))"`; - -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; - -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; - -snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; - -snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; +snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; -snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; +snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ isNumber, isString, isBoolean ])" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ isNumber, isString, isBoolean @@ -126,6 +134,20 @@ snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber) snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; + +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; + +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; + +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; + +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; + snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; @@ -139,25 +161,3 @@ snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(n snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; - -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ - isNumber, - isString, - isBoolean -], isArray)" -`; - -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; - -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ - isNumber, - isString, - isBoolean - ], isArray) - ], isArray) -], isArray)" -`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index 565cf28..466da9d 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,7 +1,5 @@ export const snapshot = {}; -snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; - snapshot[`isOneOf > returns properly named function 1`] = ` "isOneOf([ isNumber, @@ -10,9 +8,11 @@ snapshot[`isOneOf > returns properly named function 1`] = ` ])" `; +snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; + snapshot[`isAllOf > returns properly named function 1`] = ` "isAllOf([ - isObjectOf({a: isNumber}), - isObjectOf({b: isString}) + isObjectOf({a: isNumber}, undefined), + isObjectOf({b: isString}, undefined) ])" `; diff --git a/is/factory.ts b/is/factory.ts index 06520c8..876c0de 100644 --- a/is/factory.ts +++ b/is/factory.ts @@ -8,7 +8,7 @@ import { isSet, type Primitive, } from "./core.ts"; -import { inspect } from "../inspect.ts"; +import { setPredicateMetadata, type WithMetadata } from "../metadata.ts"; /** * Return a type predicate function that returns `true` if the type of `x` is `T[]`. @@ -28,17 +28,18 @@ import { inspect } from "../inspect.ts"; */ export function isArrayOf( pred: Predicate, -): Predicate { - return Object.defineProperties( +): Predicate & WithMetadata { + return setPredicateMetadata( (x: unknown): x is T[] => isArray(x) && x.every(pred), - { - name: { - get: () => `isArrayOf(${inspect(pred)})`, - }, - }, + { name: "isArrayOf", args: [pred] }, ); } +type IsArrayOfMetadata = { + name: "isArrayOf"; + args: Parameters; +}; + /** * Return a type predicate function that returns `true` if the type of `x` is `Set`. * @@ -57,8 +58,8 @@ export function isArrayOf( */ export function isSetOf( pred: Predicate, -): Predicate> { - return Object.defineProperties( +): Predicate> & WithMetadata { + return setPredicateMetadata( (x: unknown): x is Set => { if (!isSet(x)) return false; for (const v of x.values()) { @@ -66,20 +67,13 @@ export function isSetOf( } return true; }, - { - name: { - get: () => `isSetOf(${inspect(pred)})`, - }, - }, + { name: "isSetOf", args: [pred] }, ); } -type TupleOf = { - -readonly [P in keyof T]: T[P] extends Predicate ? U : never; -}; - -type ReadonlyTupleOf = { - [P in keyof T]: T[P] extends Predicate ? U : never; +type IsSetOfMetadata = { + name: "isSetOf"; + args: Parameters; }; /** @@ -133,37 +127,37 @@ export function isTupleOf< T extends readonly [Predicate, ...Predicate[]], >( predTup: T, -): Predicate>; +): Predicate> & WithMetadata; export function isTupleOf< T extends readonly [Predicate, ...Predicate[]], E extends Predicate, >( predTup: T, predElse: E, -): Predicate<[...TupleOf, ...PredicateType]>; +): + & Predicate<[...TupleOf, ...PredicateType]> + & WithMetadata; export function isTupleOf< T extends readonly [Predicate, ...Predicate[]], E extends Predicate, >( predTup: T, predElse?: E, -): Predicate | [...TupleOf, ...PredicateType]> { +): + & Predicate | [...TupleOf, ...PredicateType]> + & WithMetadata { if (!predElse) { - return Object.defineProperties( + return setPredicateMetadata( (x: unknown): x is TupleOf => { if (!isArray(x) || x.length !== predTup.length) { return false; } return predTup.every((pred, i) => pred(x[i])); }, - { - name: { - get: () => `isTupleOf(${inspect(predTup)})`, - }, - }, + { name: "isTupleOf", args: [predTup] }, ); } else { - return Object.defineProperties( + return setPredicateMetadata( (x: unknown): x is [...TupleOf, ...PredicateType] => { if (!isArray(x) || x.length < predTup.length) { return false; @@ -172,15 +166,20 @@ export function isTupleOf< const tail = x.slice(predTup.length); return predTup.every((pred, i) => pred(head[i])) && predElse(tail); }, - { - name: { - get: () => `isTupleOf(${inspect(predTup)}, ${inspect(predElse)})`, - }, - }, + { name: "isTupleOf", args: [predTup, predElse] }, ); } } +type TupleOf = { + -readonly [P in keyof T]: T[P] extends Predicate ? U : never; +}; + +type IsTupleOfMetadata = { + name: "isTupleOf"; + args: [Parameters[0], Parameters[1]?]; +}; + /** * Return a type predicate function that returns `true` if the type of `x` is `ReadonlyTupleOf`. * @@ -232,60 +231,53 @@ export function isReadonlyTupleOf< T extends readonly [Predicate, ...Predicate[]], >( predTup: T, -): Predicate>; +): Predicate> & WithMetadata; export function isReadonlyTupleOf< T extends readonly [Predicate, ...Predicate[]], E extends Predicate, >( predTup: T, predElse: E, -): Predicate, ...PredicateType]>; +): + & Predicate, ...PredicateType]> + & WithMetadata; export function isReadonlyTupleOf< T extends readonly [Predicate, ...Predicate[]], E extends Predicate, >( predTup: T, predElse?: E, -): Predicate< - ReadonlyTupleOf | readonly [...ReadonlyTupleOf, ...PredicateType] -> { +): + & Predicate< + ReadonlyTupleOf | readonly [...ReadonlyTupleOf, ...PredicateType] + > + & WithMetadata { if (!predElse) { - return Object.defineProperties( + return setPredicateMetadata( isTupleOf(predTup) as Predicate>, - { - name: { - get: () => `isReadonlyTupleOf(${inspect(predTup)})`, - }, - }, + { name: "isReadonlyTupleOf", args: [predTup] }, ); } else { - return Object.defineProperties( + return setPredicateMetadata( isTupleOf(predTup, predElse) as unknown as Predicate< readonly [...ReadonlyTupleOf, ...PredicateType] >, - { - name: { - get: () => - `isReadonlyTupleOf(${inspect(predTup)}, ${inspect(predElse)})`, - }, - }, + { name: "isReadonlyTupleOf", args: [predTup, predElse] }, ); } } -// https://stackoverflow.com/a/71700658/1273406 -type UniformTupleOf< - T, - N extends number, - R extends readonly T[] = [], -> = R["length"] extends N ? R : UniformTupleOf; +type ReadonlyTupleOf = { + [P in keyof T]: T[P] extends Predicate ? U : never; +}; -type ReadonlyUniformTupleOf< - T, - N extends number, - R extends readonly T[] = [], -> = R["length"] extends N ? R - : ReadonlyUniformTupleOf; +type IsReadonlyTupleOfMetadata = { + name: "isReadonlyTupleOf"; + args: [ + Parameters[0], + Parameters[1]?, + ]; +}; /** * Return a type predicate function that returns `true` if the type of `x` is `UniformTupleOf`. @@ -319,22 +311,30 @@ type ReadonlyUniformTupleOf< export function isUniformTupleOf( n: N, pred: Predicate = isAny, -): Predicate> { - return Object.defineProperties( +): Predicate> & WithMetadata { + return setPredicateMetadata( (x: unknown): x is UniformTupleOf => { if (!isArray(x) || x.length !== n) { return false; } return x.every((v) => pred(v)); }, - { - name: { - get: () => `isUniformTupleOf(${n}, ${inspect(pred)})`, - }, - }, + { name: "isUniformTupleOf", args: [n, pred] }, ); } +// https://stackoverflow.com/a/71700658/1273406 +type UniformTupleOf< + T, + N extends number, + R extends readonly T[] = [], +> = R["length"] extends N ? R : UniformTupleOf; + +type IsUniformTupleOfMetadata = { + name: "isUniformTupleOf"; + args: Parameters; +}; + /** * Return a type predicate function that returns `true` if the type of `x` is `ReadonlyUniformTupleOf`. * @@ -367,17 +367,27 @@ export function isUniformTupleOf( export function isReadonlyUniformTupleOf( n: N, pred: Predicate = isAny, -): Predicate> { - return Object.defineProperties( +): + & Predicate> + & WithMetadata { + return setPredicateMetadata( isUniformTupleOf(n, pred) as Predicate>, - { - name: { - get: () => `isReadonlyUniformTupleOf(${n}, ${inspect(pred)})`, - }, - }, + { name: "isReadonlyUniformTupleOf", args: [n, pred] }, ); } +type ReadonlyUniformTupleOf< + T, + N extends number, + R extends readonly T[] = [], +> = R["length"] extends N ? R + : ReadonlyUniformTupleOf; + +type IsReadonlyUniformTupleOfMetadata = { + name: "isReadonlyUniformTupleOf"; + args: Parameters; +}; + /** * Return a type predicate function that returns `true` if the type of `x` is `Record`. * @@ -410,8 +420,8 @@ export function isReadonlyUniformTupleOf( export function isRecordOf( pred: Predicate, predKey?: Predicate, -): Predicate> { - return Object.defineProperties( +): Predicate> & WithMetadata { + return setPredicateMetadata( (x: unknown): x is Record => { if (!isRecord(x)) return false; for (const k in x) { @@ -420,16 +430,15 @@ export function isRecordOf( } return true; }, - { - name: { - get: predKey - ? () => `isRecordOf(${inspect(pred)}, ${inspect(predKey)})` - : () => `isRecordOf(${inspect(pred)})`, - }, - }, + { name: "isRecordOf", args: [pred, predKey] }, ); } +type IsRecordOfMetadata = { + name: "isRecordOf"; + args: Parameters; +}; + /** * Return a type predicate function that returns `true` if the type of `x` is `Map`. * @@ -462,8 +471,8 @@ export function isRecordOf( export function isMapOf( pred: Predicate, predKey?: Predicate, -): Predicate> { - return Object.defineProperties( +): Predicate> & WithMetadata { + return setPredicateMetadata( (x: unknown): x is Map => { if (!isMap(x)) return false; for (const entry of x.entries()) { @@ -473,31 +482,13 @@ export function isMapOf( } return true; }, - { - name: { - get: predKey - ? () => `isMapOf(${inspect(pred)}, ${inspect(predKey)})` - : () => `isMapOf(${inspect(pred)})`, - }, - }, + { name: "isMapOf", args: [pred, predKey] }, ); } -type ObjectOf>> = FlatType< - // Non optional - & { - [K in keyof T as T[K] extends Optional ? never : K]: T[K] extends - Predicate ? U : never; - } - // Optional - & { - [K in keyof T as T[K] extends Optional ? K : never]?: T[K] extends - Predicate ? U : never; - } ->; - -export type Optional = { - optional: true; +type IsMapOfMetadata = { + name: "isMapOf"; + args: Parameters; }; /** @@ -546,15 +537,11 @@ export function isObjectOf< T extends Record>, >( predObj: T, - { strict }: { strict?: boolean } = {}, -): Predicate> { - return Object.defineProperties( - strict ? isObjectOfStrict(predObj) : isObjectOfLoose(predObj), - { - name: { - get: () => `isObjectOf(${inspect(predObj)})`, - }, - }, + options?: { strict?: boolean }, +): Predicate> & WithMetadata { + return setPredicateMetadata( + options?.strict ? isObjectOfStrict(predObj) : isObjectOfLoose(predObj), + { name: "isObjectOf", args: [predObj, options] }, ); } @@ -586,6 +573,28 @@ function isObjectOfStrict< }; } +export type Optional = { + optional: true; +}; + +type ObjectOf>> = FlatType< + // Non optional + & { + [K in keyof T as T[K] extends Optional ? never : K]: T[K] extends + Predicate ? U : never; + } + // Optional + & { + [K in keyof T as T[K] extends Optional ? K : never]?: T[K] extends + Predicate ? U : never; + } +>; + +type IsObjectOfMetadata = { + name: "isObjectOf"; + args: Parameters; +}; + /** * Return `true` if the type of `x` is instance of `ctor`. * @@ -605,17 +614,18 @@ function isObjectOfStrict< // deno-lint-ignore no-explicit-any export function isInstanceOf unknown>( ctor: T, -): Predicate> { - return Object.defineProperties( +): Predicate> & WithMetadata { + return setPredicateMetadata( (x: unknown): x is InstanceType => x instanceof ctor, - { - name: { - get: () => `isInstanceOf(${inspect(ctor)})`, - }, - }, + { name: "isInstanceOf", args: [ctor] }, ); } +type IsInstanceOfMetadata = { + name: "isInstanceOf"; + args: Parameters; +}; + /** * Return a type predicate function that returns `true` if the type of `x` is a literal type of `pred`. * @@ -632,17 +642,20 @@ export function isInstanceOf unknown>( * } * ``` */ -export function isLiteralOf(literal: T): Predicate { - return Object.defineProperties( +export function isLiteralOf( + literal: T, +): Predicate & WithMetadata { + return setPredicateMetadata( (x: unknown): x is T => x === literal, - { - name: { - get: () => `isLiteralOf(${inspect(literal)})`, - }, - }, + { name: "isLiteralOf", args: [literal] }, ); } +type IsLiteralOfMetadata = { + name: "isLiteralOf"; + args: Parameters; +}; + /** * Return a type predicate function that returns `true` if the type of `x` is one of literal type in `preds`. * @@ -661,18 +674,19 @@ export function isLiteralOf(literal: T): Predicate { */ export function isLiteralOneOf( literals: T, -): Predicate { +): Predicate & WithMetadata { const s = new Set(literals); - return Object.defineProperties( + return setPredicateMetadata( (x: unknown): x is T[number] => s.has(x as T[number]), - { - name: { - get: () => `isLiteralOneOf(${inspect(literals)})`, - }, - }, + { name: "isLiteralOneOf", args: [literals] }, ); } +type IsLiteralOneOfMetadata = { + name: "isLiteralOneOf"; + args: Parameters; +}; + export default { ArrayOf: isArrayOf, InstanceOf: isInstanceOf, diff --git a/is/utility.ts b/is/utility.ts index c6a1f27..beec339 100644 --- a/is/utility.ts +++ b/is/utility.ts @@ -2,11 +2,7 @@ import type { UnionToIntersection } from "../_typeutil.ts"; import type { Predicate } from "./type.ts"; import { isUndefined } from "./core.ts"; import { type Optional } from "./factory.ts"; -import { inspect } from "../inspect.ts"; - -type OneOf = T extends readonly [Predicate, ...infer R] - ? U | OneOf - : never; +import { setPredicateMetadata, type WithMetadata } from "../metadata.ts"; /** * Return a type predicate function that returns `true` if the type of `x` is `OneOf`. @@ -43,18 +39,21 @@ export function isOneOf< T extends readonly [Predicate, ...Predicate[]], >( preds: T, -): Predicate> { - return Object.defineProperties( +): Predicate> & WithMetadata { + return setPredicateMetadata( (x: unknown): x is OneOf => preds.some((pred) => pred(x)), - { - name: { - get: () => `isOneOf(${inspect(preds)})`, - }, - }, + { name: "isOneOf", args: [preds] }, ); } -type AllOf = UnionToIntersection>; +type OneOf = T extends readonly [Predicate, ...infer R] + ? U | OneOf + : never; + +type IsOneOfMetadata = { + name: "isOneOf"; + args: Parameters; +}; /** * Return a type predicate function that returns `true` if the type of `x` is `AllOf`. @@ -97,17 +96,20 @@ export function isAllOf< T extends readonly [Predicate, ...Predicate[]], >( preds: T, -): Predicate> { - return Object.defineProperties( +): Predicate> & WithMetadata { + return setPredicateMetadata( (x: unknown): x is AllOf => preds.every((pred) => pred(x)), - { - name: { - get: () => `isAllOf(${inspect(preds)})`, - }, - }, + { name: "isAllOf", args: [preds] }, ); } +type AllOf = UnionToIntersection>; + +type IsAllOfMetadata = { + name: "isAllOf"; + args: Parameters; +}; + /** * Return a type predicate function that returns `true` if the type of `x` is `T` or `undefined`. * @@ -126,20 +128,21 @@ export function isAllOf< */ export function isOptionalOf( pred: Predicate, -): Predicate & Optional { +): Predicate & Optional & WithMetadata { return Object.defineProperties( - (x: unknown): x is Predicate => isUndefined(x) || pred(x), - { - optional: { - value: true as const, - }, - name: { - get: () => `isOptionalOf(${inspect(pred)})`, - }, - }, - ) as Predicate & Optional; + setPredicateMetadata( + (x: unknown): x is Predicate => isUndefined(x) || pred(x), + { name: "isOptionalOf", args: [pred] }, + ), + { optional: { value: true as const } }, + ) as Predicate & Optional & WithMetadata; } +type IsOptionalOfMetadata = { + name: "isOptionalOf"; + args: Parameters; +}; + export default { AllOf: isAllOf, OneOf: isOneOf, diff --git a/metadata.ts b/metadata.ts new file mode 100644 index 0000000..5b704ae --- /dev/null +++ b/metadata.ts @@ -0,0 +1,37 @@ +import type { Predicate } from "./is/type.ts"; +import { inspect } from "./inspect.ts"; + +const metadataKey = "__unknownutil_metadata"; + +export type WithMetadata = { + [metadataKey]: T; +}; + +export type PredicateMetadata = { + name: string; + args: unknown[]; +}; + +export function setPredicateMetadata< + P extends Predicate, + M extends PredicateMetadata, +>( + pred: P, + metadata: M, +): P & WithMetadata { + let cachedName: string | undefined; + return Object.defineProperties(pred, { + [metadataKey]: { + value: metadata, + configurable: true, + }, + name: { + get: () => { + if (cachedName) return cachedName; + const { name, args } = metadata; + cachedName = `${name}(${args.map((v) => inspect(v)).join(", ")})`; + return cachedName; + }, + }, + }) as P & WithMetadata; +} From afd031d1649672936e43b65772c1cade6c42cb0b Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 21:05:09 +0900 Subject: [PATCH 11/31] :muscle: Simplify `isObjectOf` implementation --- is/factory.ts | 48 +++++++++++++++++++----------------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/is/factory.ts b/is/factory.ts index 876c0de..ad62cdb 100644 --- a/is/factory.ts +++ b/is/factory.ts @@ -539,40 +539,30 @@ export function isObjectOf< predObj: T, options?: { strict?: boolean }, ): Predicate> & WithMetadata { + if (options?.strict) { + const keys = new Set(Object.keys(predObj)); + const pred = isObjectOf(predObj); + return setPredicateMetadata( + (x: unknown): x is ObjectOf => { + if (!pred(x)) return false; + const ks = Object.keys(x); + return ks.length <= keys.size && ks.every((k) => keys.has(k)); + }, + { name: "isObjectOf", args: [predObj, options] }, + ); + } return setPredicateMetadata( - options?.strict ? isObjectOfStrict(predObj) : isObjectOfLoose(predObj), + (x: unknown): x is ObjectOf => { + if (!isRecord(x)) return false; + for (const k in predObj) { + if (!predObj[k](x[k])) return false; + } + return true; + }, { name: "isObjectOf", args: [predObj, options] }, ); } -function isObjectOfLoose< - T extends Record>, ->( - predObj: T, -): Predicate> { - return (x: unknown): x is ObjectOf => { - if (!isRecord(x)) return false; - for (const k in predObj) { - if (!predObj[k](x[k])) return false; - } - return true; - }; -} - -function isObjectOfStrict< - T extends Record>, ->( - predObj: T, -): Predicate> { - const keys = new Set(Object.keys(predObj)); - const pred = isObjectOfLoose(predObj); - return (x: unknown): x is ObjectOf => { - if (!pred(x)) return false; - const ks = Object.keys(x); - return ks.length <= keys.size && ks.every((k) => keys.has(k)); - }; -} - export type Optional = { optional: true; }; From 3c5885264a07a518c160f3fcf53482cacb1236b9 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 21:16:40 +0900 Subject: [PATCH 12/31] :+1: Add `isStrictOf()` --- is/__snapshots__/factory_test.ts.snap | 144 ++++++++++++------------ is/__snapshots__/utility_test.ts.snap | 36 ++++-- is/factory.ts | 27 +---- is/utility.ts | 58 +++++++++- is/utility_test.ts | 152 +++++++++++++++++++++++++- metadata.ts | 6 + 6 files changed, 318 insertions(+), 105 deletions(-) diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index 37480e2..f2f6df4 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,51 +1,75 @@ export const snapshot = {}; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyTupleOf([ + isNumber, + isString, + isBoolean +], isArray)" +`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; + +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyTupleOf([ + isReadonlyTupleOf([ + isReadonlyTupleOf([ + isNumber, + isString, + isBoolean + ], isArray) + ], isArray) +], isArray)" +`; + +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; + +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; snapshot[`isObjectOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, b: isString, c: isBoolean -}, undefined)" +})" `; -snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a}, undefined)"`; +snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; snapshot[`isObjectOf > returns properly named function 3`] = ` "isObjectOf({ a: isObjectOf({ - b: isObjectOf({c: isBoolean}, undefined) - }, undefined) -}, undefined)" + b: isObjectOf({c: isBoolean}) + }) +})" `; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; - -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; - -snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; -snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; -snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ isNumber, isString, isBoolean ])" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ isNumber, isString, isBoolean @@ -54,33 +78,13 @@ snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` ])" `; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ - isNumber, - isString, - isBoolean -], isArray)" -`; - -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; - -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ - isNumber, - isString, - isBoolean - ], isArray) - ], isArray) -], isArray)" -`; +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ @@ -104,24 +108,26 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` ])" `; -snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; +snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; -snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; +snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ +snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; + +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyTupleOf([ isNumber, isString, isBoolean ])" `; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyTupleOf([ + isReadonlyTupleOf([ + isReadonlyTupleOf([ isNumber, isString, isBoolean @@ -130,24 +136,6 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` ])" `; -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; - -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; - -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; - -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; - -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; - -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; - -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; - snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; @@ -161,3 +149,15 @@ snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(n snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; + +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; + +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; + +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; + +snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; + +snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index 466da9d..e1e7e0c 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,18 +1,36 @@ export const snapshot = {}; -snapshot[`isOneOf > returns properly named function 1`] = ` -"isOneOf([ - isNumber, - isString, - isBoolean +snapshot[`isAllOf > returns properly named function 1`] = ` +"isAllOf([ + isObjectOf({a: isNumber}), + isObjectOf({b: isString}) ])" `; +snapshot[`isStrictOf > returns properly named function 1`] = ` +"isStrictOf(isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +}))" +`; + +snapshot[`isStrictOf > returns properly named function 2`] = `"isStrictOf(isObjectOf({a: a}))"`; + +snapshot[`isStrictOf > returns properly named function 3`] = ` +"isStrictOf(isObjectOf({ + a: isStrictOf(isObjectOf({ + b: isStrictOf(isObjectOf({c: isBoolean})) + })) +}))" +`; + snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; -snapshot[`isAllOf > returns properly named function 1`] = ` -"isAllOf([ - isObjectOf({a: isNumber}, undefined), - isObjectOf({b: isString}, undefined) +snapshot[`isOneOf > returns properly named function 1`] = ` +"isOneOf([ + isNumber, + isString, + isBoolean ])" `; diff --git a/is/factory.ts b/is/factory.ts index ad62cdb..5af51b9 100644 --- a/is/factory.ts +++ b/is/factory.ts @@ -498,8 +498,7 @@ type IsMapOfMetadata = { * * If `is.OptionalOf()` is specified in the predicate function, the property becomes optional. * - * When `options.strict` is `true`, the number of keys of `x` must be equal to the number of keys of `predObj`. - * Otherwise, the number of keys of `x` must be greater than or equal to the number of keys of `predObj`. + * The number of keys of `x` must be greater than or equal to the number of keys of `predObj`. * * ```ts * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; @@ -517,21 +516,7 @@ type IsMapOfMetadata = { * } * ``` * - * With `options.strict`: - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.ObjectOf({ - * a: is.Number, - * b: is.String, - * c: is.OptionalOf(is.Boolean), - * }, { strict: true }); - * const a: unknown = { a: 0, b: "a", other: "other" }; - * if (isMyType(a)) { - * // This block will not be executed because of "other" key in `a`. - * } - * ``` + * The `option.strict` is deprecated. Use `isStrictOf()` instead. */ export function isObjectOf< T extends Record>, @@ -548,7 +533,7 @@ export function isObjectOf< const ks = Object.keys(x); return ks.length <= keys.size && ks.every((k) => keys.has(k)); }, - { name: "isObjectOf", args: [predObj, options] }, + { name: "isObjectOf", args: [predObj] }, ); } return setPredicateMetadata( @@ -559,7 +544,7 @@ export function isObjectOf< } return true; }, - { name: "isObjectOf", args: [predObj, options] }, + { name: "isObjectOf", args: [predObj] }, ); } @@ -580,9 +565,9 @@ type ObjectOf>> = FlatType< } >; -type IsObjectOfMetadata = { +export type IsObjectOfMetadata = { name: "isObjectOf"; - args: Parameters; + args: [Parameters[0]]; }; /** diff --git a/is/utility.ts b/is/utility.ts index beec339..da98641 100644 --- a/is/utility.ts +++ b/is/utility.ts @@ -1,8 +1,12 @@ import type { UnionToIntersection } from "../_typeutil.ts"; import type { Predicate } from "./type.ts"; import { isUndefined } from "./core.ts"; -import { type Optional } from "./factory.ts"; -import { setPredicateMetadata, type WithMetadata } from "../metadata.ts"; +import { type IsObjectOfMetadata, type Optional } from "./factory.ts"; +import { + getPredicateMetadata, + setPredicateMetadata, + type WithMetadata, +} from "../metadata.ts"; /** * Return a type predicate function that returns `true` if the type of `x` is `OneOf`. @@ -143,8 +147,58 @@ type IsOptionalOfMetadata = { args: Parameters; }; +/** + * Return a type predicate function that returns `true` if the type of `x` is strictly follow the `ObjectOf`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * If `is.OptionalOf()` is specified in the predicate function, the property becomes optional. + * + * The number of keys of `x` must be equal to the number of non optional keys of `predObj`. This is equivalent to + * the deprecated `options.strict` in `isObjectOf()`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.StrictOf(is.ObjectOf({ + * a: is.Number, + * b: is.String, + * c: is.OptionalOf(is.Boolean), + * })); + * const a: unknown = { a: 0, b: "a", other: "other" }; + * if (isMyType(a)) { + * // This block will not be executed because of "other" key in `a`. + * } + * ``` + */ +export function isStrictOf>( + pred: + & Predicate + & WithMetadata, +): + & Predicate + & WithMetadata { + const { args } = getPredicateMetadata(pred); + const s = new Set(Object.keys(args[0])); + return setPredicateMetadata( + (x: unknown): x is T => { + if (!pred(x)) return false; + // deno-lint-ignore no-explicit-any + const ks = Object.keys(x as any); + return ks.length <= s.size && ks.every((k) => s.has(k)); + }, + { name: "isStrictOf", args: [pred] }, + ); +} + +type IsStrictOfMetadata = { + name: "isStrictOf"; + args: Parameters; +}; + export default { AllOf: isAllOf, OneOf: isOneOf, OptionalOf: isOptionalOf, + StrictOf: isStrictOf, }; diff --git a/is/utility_test.ts b/is/utility_test.ts index dbb04c4..422e451 100644 --- a/is/utility_test.ts +++ b/is/utility_test.ts @@ -24,7 +24,7 @@ import { isUndefined, } from "./core.ts"; import { isObjectOf } from "./factory.ts"; -import is, { isAllOf, isOneOf, isOptionalOf } from "./utility.ts"; +import is, { isAllOf, isOneOf, isOptionalOf, isStrictOf } from "./utility.ts"; const examples = { string: ["", "Hello world"], @@ -235,6 +235,156 @@ Deno.test("isOptionalOf", async (t) => { }); }); +Deno.test("isStrictOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot( + t, + isStrictOf(isObjectOf({ a: isNumber, b: isString, c: isBoolean })).name, + ); + await assertSnapshot( + t, + isStrictOf(isObjectOf({ a: (_x): _x is string => false })).name, + ); + // Nested + await assertSnapshot( + t, + isStrictOf( + isObjectOf({ + a: isStrictOf( + isObjectOf({ b: isStrictOf(isObjectOf({ c: isBoolean })) }), + ), + }), + ).name, + ); + }); + await t.step("returns proper type predicate", () => { + const predObj = { + a: isNumber, + b: isString, + c: isBoolean, + }; + const a: unknown = { a: 0, b: "a", c: true }; + if (isStrictOf(isObjectOf(predObj))(a)) { + assertType>(true); + } + }); + await t.step("returns true on T object", () => { + const predObj = { + a: isNumber, + b: isString, + c: isBoolean, + }; + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: true }), + true, + ); + }); + await t.step("returns false on non T object", () => { + const predObj = { + a: isNumber, + b: isString, + c: isBoolean, + }; + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: "" }), + false, + "Object have a different type property", + ); + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a" }), + false, + "Object does not have one property", + ); + assertEquals( + isStrictOf(isObjectOf(predObj))({ + a: 0, + b: "a", + c: true, + d: "invalid", + }), + false, + "Object have an unknown property", + ); + }); + await testWithExamples( + t, + isStrictOf(isObjectOf({ a: (_: unknown): _ is unknown => false })), + { excludeExamples: ["record"] }, + ); + await t.step("with optional properties", async (t) => { + await t.step("returns proper type predicate", () => { + const predObj = { + a: isNumber, + b: isOneOf([isString, isUndefined]), + c: isOptionalOf(isBoolean), + }; + const a: unknown = { a: 0, b: "a" }; + if (isStrictOf(isObjectOf(predObj))(a)) { + assertType< + Equal + >(true); + } + }); + await t.step("returns true on T object", () => { + const predObj = { + a: isNumber, + b: isOneOf([isString, isUndefined]), + c: isOptionalOf(isBoolean), + }; + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: true }), + true, + ); + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a" }), + true, + "Object does not have an optional property", + ); + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: undefined }), + true, + "Object has `undefined` as value of optional property", + ); + }); + await t.step("returns false on non T object", () => { + const predObj = { + a: isNumber, + b: isOneOf([isString, isUndefined]), + c: isOptionalOf(isBoolean), + }; + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: "" }), + false, + "Object have a different type property", + ); + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: null }), + false, + "Object has `null` as value of optional property", + ); + assertEquals( + isStrictOf(isObjectOf(predObj))({ + a: 0, + b: "a", + c: true, + d: "invalid", + }), + false, + "Object have an unknown property", + ); + assertEquals( + isStrictOf(isObjectOf(predObj))({ + a: 0, + b: "a", + d: "invalid", + }), + false, + "Object have the same number of properties but an unknown property exists", + ); + }); + }); +}); + Deno.test("is", async (t) => { const mod = await import("./utility.ts"); const casesOfAliasAndIsFunction = Object.entries(mod) diff --git a/metadata.ts b/metadata.ts index 5b704ae..9ed40a1 100644 --- a/metadata.ts +++ b/metadata.ts @@ -35,3 +35,9 @@ export function setPredicateMetadata< }, }) as P & WithMetadata; } + +export function getPredicateMetadata( + object: WithMetadata, +): M { + return object[metadataKey]; +} From 8ba80fdddf5c98e28ea79b1daafb8827454b52a5 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 21:20:27 +0900 Subject: [PATCH 13/31] :+1: Avoid nesting of `isOptionalOf` --- is/__snapshots__/factory_test.ts.snap | 130 +++++++++++++------------- is/__snapshots__/utility_test.ts.snap | 18 ++-- is/utility.ts | 6 ++ is/utility_test.ts | 2 + 4 files changed, 83 insertions(+), 73 deletions(-) diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index f2f6df4..e37ff7c 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,5 +1,55 @@ export const snapshot = {}; +snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; + +snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; + +snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; + +snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; + +snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; + +snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; + +snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; + +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ + isNumber, + isString, + isBoolean +], isArray)" +`; + +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; + +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ + isNumber, + isString, + isBoolean + ], isArray) + ], isArray) +])" +`; + +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; + +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; + +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; + +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; + +snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; + +snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; + +snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; + snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` "isReadonlyTupleOf([ isNumber, @@ -22,15 +72,9 @@ snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` ], isArray)" `; -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; - -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; snapshot[`isObjectOf > returns properly named function 1`] = ` "isObjectOf({ @@ -50,11 +94,13 @@ snapshot[`isObjectOf > returns properly named function 3`] = ` })" `; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ @@ -78,41 +124,21 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` ])" `; -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; - -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; - -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; +snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ - isNumber, - isString, - isBoolean -], isArray)" -`; +snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ - isNumber, - isString, - isBoolean - ], isArray) - ], isArray) -])" -`; +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; -snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; -snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; -snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` "isReadonlyTupleOf([ @@ -135,29 +161,3 @@ snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` ]) ])" `; - -snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; - -snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; - -snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; - -snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; - -snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; - -snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; - -snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; - -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; - -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; - -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; - -snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; - -snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index e1e7e0c..ddb329d 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,11 +1,8 @@ export const snapshot = {}; -snapshot[`isAllOf > returns properly named function 1`] = ` -"isAllOf([ - isObjectOf({a: isNumber}), - isObjectOf({b: isString}) -])" -`; +snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; + +snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; snapshot[`isStrictOf > returns properly named function 1`] = ` "isStrictOf(isObjectOf({ @@ -25,8 +22,6 @@ snapshot[`isStrictOf > returns properly named function 3`] = ` }))" `; -snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; - snapshot[`isOneOf > returns properly named function 1`] = ` "isOneOf([ isNumber, @@ -34,3 +29,10 @@ snapshot[`isOneOf > returns properly named function 1`] = ` isBoolean ])" `; + +snapshot[`isAllOf > returns properly named function 1`] = ` +"isAllOf([ + isObjectOf({a: isNumber}), + isObjectOf({b: isString}) +])" +`; diff --git a/is/utility.ts b/is/utility.ts index da98641..8d292b4 100644 --- a/is/utility.ts +++ b/is/utility.ts @@ -133,6 +133,12 @@ type IsAllOfMetadata = { export function isOptionalOf( pred: Predicate, ): Predicate & Optional & WithMetadata { + if ((pred as Partial).optional) { + return pred as + & Predicate + & Optional + & WithMetadata; + } return Object.defineProperties( setPredicateMetadata( (x: unknown): x is Predicate => isUndefined(x) || pred(x), diff --git a/is/utility_test.ts b/is/utility_test.ts index 422e451..1cea89a 100644 --- a/is/utility_test.ts +++ b/is/utility_test.ts @@ -161,6 +161,8 @@ Deno.test("isAllOf", async (t) => { Deno.test("isOptionalOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot(t, isOptionalOf(isNumber).name); + // Nesting does nothing + await assertSnapshot(t, isOptionalOf(isOptionalOf(isNumber)).name); }); await t.step("returns proper type predicate", () => { const a: unknown = undefined; From 5d26b34e9bba0f252df5b444411446db9dc7ef92 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 21:23:40 +0900 Subject: [PATCH 14/31] :+1: Add `isPartialOf` --- is/__snapshots__/factory_test.ts.snap | 146 +++++++++++++------------- is/__snapshots__/utility_test.ts.snap | 34 ++++-- is/utility.ts | 46 +++++++- is/utility_test.ts | 44 +++++++- 4 files changed, 185 insertions(+), 85 deletions(-) diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index e37ff7c..0b2cf9e 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,54 +1,80 @@ export const snapshot = {}; -snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; - -snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; - -snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; - -snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; -snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; -snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; -snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyTupleOf([ isNumber, isString, isBoolean -], isArray)" +])" `; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyTupleOf([ + isReadonlyTupleOf([ + isReadonlyTupleOf([ isNumber, isString, isBoolean - ], isArray) - ], isArray) + ]) + ]) ])" `; +snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; + +snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; + +snapshot[`isObjectOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +})" +`; + +snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; + +snapshot[`isObjectOf > returns properly named function 3`] = ` +"isObjectOf({ + a: isObjectOf({ + b: isObjectOf({c: isBoolean}) + }) +})" +`; + +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; + +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; + snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; + +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; + +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; + snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; -snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; - -snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; -snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` "isReadonlyTupleOf([ @@ -72,88 +98,62 @@ snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` ], isArray)" `; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; - -snapshot[`isObjectOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - b: isString, - c: isBoolean -})" -`; +snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; -snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; +snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; -snapshot[`isObjectOf > returns properly named function 3`] = ` -"isObjectOf({ - a: isObjectOf({ - b: isObjectOf({c: isBoolean}) - }) -})" -`; +snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; +snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; +snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; +snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; +snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; -snapshot[`isTupleOf > returns properly named function 1`] = ` +snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ isNumber, isString, isBoolean -])" +], isArray)" `; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` +snapshot[`isTupleOf > returns properly named function 3`] = ` "isTupleOf([ isTupleOf([ isTupleOf([ isNumber, isString, isBoolean - ]) - ]) + ], isArray) + ], isArray) ])" `; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; - -snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; - -snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; - -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; - -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; +snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; +snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; +snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ isNumber, isString, isBoolean ])" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ isNumber, isString, isBoolean diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index ddb329d..9c5231b 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,8 +1,20 @@ export const snapshot = {}; -snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; +snapshot[`isPartialOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isOptionalOf(isNumber), + b: isOptionalOf(isString), + c: isOptionalOf(isBoolean) +})" +`; -snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; +snapshot[`isPartialOf > returns properly named function 2`] = ` +"isObjectOf({ + a: isOptionalOf(isNumber), + b: isOptionalOf(isString), + c: isOptionalOf(isBoolean) +})" +`; snapshot[`isStrictOf > returns properly named function 1`] = ` "isStrictOf(isObjectOf({ @@ -22,13 +34,9 @@ snapshot[`isStrictOf > returns properly named function 3`] = ` }))" `; -snapshot[`isOneOf > returns properly named function 1`] = ` -"isOneOf([ - isNumber, - isString, - isBoolean -])" -`; +snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; + +snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; snapshot[`isAllOf > returns properly named function 1`] = ` "isAllOf([ @@ -36,3 +44,11 @@ snapshot[`isAllOf > returns properly named function 1`] = ` isObjectOf({b: isString}) ])" `; + +snapshot[`isOneOf > returns properly named function 1`] = ` +"isOneOf([ + isNumber, + isString, + isBoolean +])" +`; diff --git a/is/utility.ts b/is/utility.ts index 8d292b4..b2045ac 100644 --- a/is/utility.ts +++ b/is/utility.ts @@ -1,7 +1,11 @@ -import type { UnionToIntersection } from "../_typeutil.ts"; +import type { FlatType, UnionToIntersection } from "../_typeutil.ts"; import type { Predicate } from "./type.ts"; import { isUndefined } from "./core.ts"; -import { type IsObjectOfMetadata, type Optional } from "./factory.ts"; +import { + isObjectOf, + type IsObjectOfMetadata, + type Optional, +} from "./factory.ts"; import { getPredicateMetadata, setPredicateMetadata, @@ -202,9 +206,47 @@ type IsStrictOfMetadata = { args: Parameters; }; +/** + * Return a type predicate function that returns `true` if the type of `x` is `Partial>`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```typescript + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.PartialOf(is.ObjectOf({ + * a: is.Number, + * b: is.String, + * c: is.OptionalOf(is.Boolean), + * })); + * const a: unknown = { a: undefined, other: "other" }; + * if (isMyType(a)) { + * // The "other" key in `a` is ignored. + * // 'a' is narrowed to { a?: number | undefined; b?: string | undefined; c?: boolean | undefined } + * const _: { a?: number | undefined; b?: string | undefined; c?: boolean | undefined } = a; + * } + * ``` + */ +export function isPartialOf< + T extends Record, +>( + pred: Predicate & WithMetadata, +): + & Predicate>> + & WithMetadata { + const { args } = getPredicateMetadata(pred); + const predObj = Object.fromEntries( + Object.entries(args[0]).map(([k, v]) => [k, isOptionalOf(v)]), + ); + return isObjectOf(predObj) as + & Predicate>> + & WithMetadata; +} + export default { AllOf: isAllOf, OneOf: isOneOf, OptionalOf: isOptionalOf, + PartialOf: isPartialOf, StrictOf: isStrictOf, }; diff --git a/is/utility_test.ts b/is/utility_test.ts index 1cea89a..3177423 100644 --- a/is/utility_test.ts +++ b/is/utility_test.ts @@ -24,7 +24,13 @@ import { isUndefined, } from "./core.ts"; import { isObjectOf } from "./factory.ts"; -import is, { isAllOf, isOneOf, isOptionalOf, isStrictOf } from "./utility.ts"; +import is, { + isAllOf, + isOneOf, + isOptionalOf, + isPartialOf, + isStrictOf, +} from "./utility.ts"; const examples = { string: ["", "Hello world"], @@ -387,6 +393,42 @@ Deno.test("isStrictOf", async (t) => { }); }); +Deno.test("isPartialOf", async (t) => { + const pred = isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean, + }); + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isPartialOf(pred).name); + // Nestable (no effect) + await assertSnapshot(t, isPartialOf(isPartialOf(pred)).name); + }); + await t.step("returns proper type predicate", () => { + const a: unknown = { a: 0, b: "a", c: true }; + if (isPartialOf(pred)(a)) { + assertType< + Equal> + >(true); + } + }); + await t.step("returns true on Partial object", () => { + assertEquals( + isPartialOf(pred)({ a: undefined, b: undefined, c: undefined }), + true, + ); + assertEquals(isPartialOf(pred)({}), true); + }); + await t.step("returns false on non Partial object", () => { + assertEquals(isPartialOf(pred)("a"), false, "Value is not an object"); + assertEquals( + isPartialOf(pred)({ a: 0, b: "a", c: "" }), + false, + "Object have a different type property", + ); + }); +}); + Deno.test("is", async (t) => { const mod = await import("./utility.ts"); const casesOfAliasAndIsFunction = Object.entries(mod) From 5c74c1bfe4a178e515fa0c4747a21464e182d13d Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 21:25:02 +0900 Subject: [PATCH 15/31] :+1: Add `isPickOf` --- is/__snapshots__/factory_test.ts.snap | 164 +++++++++++++------------- is/__snapshots__/utility_test.ts.snap | 23 ++-- is/utility.ts | 41 +++++++ is/utility_test.ts | 46 ++++++++ 4 files changed, 185 insertions(+), 89 deletions(-) diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index 0b2cf9e..8c4fdd0 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,56 +1,34 @@ export const snapshot = {}; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ isNumber, isString, isBoolean -])" +], isArray)" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ isNumber, isString, isBoolean - ]) - ]) + ], isArray) + ], isArray) ])" `; -snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; - -snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; - -snapshot[`isObjectOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - b: isString, - c: isBoolean -})" -`; - -snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; -snapshot[`isObjectOf > returns properly named function 3`] = ` -"isObjectOf({ - a: isObjectOf({ - b: isObjectOf({c: isBoolean}) - }) -})" -`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; @@ -60,44 +38,60 @@ snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf( snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; - -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyTupleOf([ + isNumber, + isString, + isBoolean +])" +`; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyTupleOf([ + isReadonlyTupleOf([ + isReadonlyTupleOf([ + isNumber, + isString, + isBoolean + ]) + ]) +])" +`; snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ isNumber, isString, isBoolean -], isArray)" +])" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ isNumber, isString, isBoolean - ], isArray) - ], isArray) -], isArray)" + ]) + ]) +])" `; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; + snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; @@ -112,26 +106,44 @@ snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(u snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyTupleOf([ isNumber, isString, isBoolean ], isArray)" `; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyTupleOf([ + isReadonlyTupleOf([ + isReadonlyTupleOf([ isNumber, isString, isBoolean ], isArray) ], isArray) -])" +], isArray)" +`; + +snapshot[`isObjectOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +})" +`; + +snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; + +snapshot[`isObjectOf > returns properly named function 3`] = ` +"isObjectOf({ + a: isObjectOf({ + b: isObjectOf({c: isBoolean}) + }) +})" `; snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; @@ -140,24 +152,12 @@ snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUnifor snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ - isNumber, - isString, - isBoolean -])" -`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ - isNumber, - isString, - isBoolean - ]) - ]) -])" -`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; + +snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; + +snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index 9c5231b..7424e96 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,5 +1,21 @@ export const snapshot = {}; +snapshot[`isAllOf > returns properly named function 1`] = ` +"isAllOf([ + isObjectOf({a: isNumber}), + isObjectOf({b: isString}) +])" +`; + +snapshot[`isPickOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + c: isBoolean +})" +`; + +snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; + snapshot[`isPartialOf > returns properly named function 1`] = ` "isObjectOf({ a: isOptionalOf(isNumber), @@ -38,13 +54,6 @@ snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; -snapshot[`isAllOf > returns properly named function 1`] = ` -"isAllOf([ - isObjectOf({a: isNumber}), - isObjectOf({b: isString}) -])" -`; - snapshot[`isOneOf > returns properly named function 1`] = ` "isOneOf([ isNumber, diff --git a/is/utility.ts b/is/utility.ts index b2045ac..a8a2aa0 100644 --- a/is/utility.ts +++ b/is/utility.ts @@ -243,10 +243,51 @@ export function isPartialOf< & WithMetadata; } +/** + * Return a type predicate function that returns `true` if the type of `x` is `Pick, K>`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```typescript + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.PickOf(is.ObjectOf({ + * a: is.Number, + * b: is.String, + * c: is.OptionalOf(is.Boolean), + * }), ["a", "c"]); + * const a: unknown = { a: 0, b: "a", other: "other" }; + * if (isMyType(a)) { + * // The "b" and "other" key in `a` is ignored. + * // 'a' is narrowed to { a: number; c?: boolean | undefined } + * const _: { a: number; c?: boolean | undefined } = a; + * } + * ``` + */ +export function isPickOf< + T extends Record, + K extends keyof T, +>( + pred: Predicate & WithMetadata, + keys: K[], +): + & Predicate>> + & WithMetadata { + const s = new Set(keys); + const { args } = getPredicateMetadata(pred); + const predObj = Object.fromEntries( + Object.entries(args[0]).filter(([k]) => s.has(k as K)), + ); + return isObjectOf(predObj) as + & Predicate>> + & WithMetadata; +} + export default { AllOf: isAllOf, OneOf: isOneOf, OptionalOf: isOptionalOf, PartialOf: isPartialOf, + PickOf: isPickOf, StrictOf: isStrictOf, }; diff --git a/is/utility_test.ts b/is/utility_test.ts index 3177423..a1d2717 100644 --- a/is/utility_test.ts +++ b/is/utility_test.ts @@ -29,6 +29,7 @@ import is, { isOneOf, isOptionalOf, isPartialOf, + isPickOf, isStrictOf, } from "./utility.ts"; @@ -429,6 +430,51 @@ Deno.test("isPartialOf", async (t) => { }); }); +Deno.test("isPickOf", async (t) => { + const pred = isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean, + }); + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isPickOf(pred, ["a", "c"]).name); + // Nestable + await assertSnapshot(t, isPickOf(isPickOf(pred, ["a", "c"]), ["a"]).name); + }); + await t.step("returns proper type predicate", () => { + const a: unknown = { a: 0, b: "a", c: true }; + if (isPickOf(pred, ["a", "c"])(a)) { + assertType< + Equal + >(true); + } + if (isPickOf(isPickOf(pred, ["a", "c"]), ["a"])(a)) { + assertType< + Equal + >(true); + } + }); + await t.step("returns true on Pick object", () => { + assertEquals( + isPickOf(pred, ["a", "c"])({ a: 0, b: undefined, c: true }), + true, + ); + assertEquals(isPickOf(pred, ["a"])({ a: 0 }), true); + }); + await t.step("returns false on non Pick object", () => { + assertEquals( + isPickOf(pred, ["a", "c"])("a"), + false, + "Value is not an object", + ); + assertEquals( + isPickOf(pred, ["a", "c"])({ a: 0, b: "a", c: "" }), + false, + "Object have a different type property", + ); + }); +}); + Deno.test("is", async (t) => { const mod = await import("./utility.ts"); const casesOfAliasAndIsFunction = Object.entries(mod) From f4df8e9cb17d3abe15e5e8df9c7923e0f738285b Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 21:26:17 +0900 Subject: [PATCH 16/31] :+1: Add `isOmitOf` --- is/__snapshots__/factory_test.ts.snap | 134 +++++++++++++------------- is/__snapshots__/utility_test.ts.snap | 63 ++++++------ is/utility.ts | 41 ++++++++ is/utility_test.ts | 46 +++++++++ 4 files changed, 190 insertions(+), 94 deletions(-) diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index 8c4fdd0..12410ef 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,131 +1,143 @@ export const snapshot = {}; -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; -snapshot[`isTupleOf > returns properly named function 1`] = ` +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; + +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; + +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; + +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; + +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; + +snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ isNumber, isString, isBoolean -], isArray)" +])" `; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` +snapshot[`isTupleOf > returns properly named function 3`] = ` "isTupleOf([ isTupleOf([ isTupleOf([ isNumber, isString, isBoolean - ], isArray) - ], isArray) + ]) + ]) ])" `; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; +snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; +snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; +snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; +snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; +snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; +snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; +snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; + +snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; + +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; + +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; + +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` "isReadonlyTupleOf([ isNumber, isString, isBoolean -])" +], isArray)" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` "isReadonlyTupleOf([ isReadonlyTupleOf([ isReadonlyTupleOf([ isNumber, isString, isBoolean - ]) - ]) -])" + ], isArray) + ], isArray) +], isArray)" `; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; +snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; +snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; -snapshot[`isTupleOf > returns properly named function 1`] = ` +snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; + +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; + +snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ isNumber, isString, isBoolean -])" +], isArray)" `; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` +snapshot[`isTupleOf > returns properly named function 3`] = ` "isTupleOf([ isTupleOf([ isTupleOf([ isNumber, isString, isBoolean - ]) - ]) + ], isArray) + ], isArray) ])" `; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; - -snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; - -snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; - -snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; - -snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; - -snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; - -snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; -snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` "isReadonlyTupleOf([ isNumber, isString, isBoolean -], isArray)" +])" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` "isReadonlyTupleOf([ isReadonlyTupleOf([ isReadonlyTupleOf([ isNumber, isString, isBoolean - ], isArray) - ], isArray) -], isArray)" + ]) + ]) +])" `; snapshot[`isObjectOf > returns properly named function 1`] = ` @@ -146,18 +158,6 @@ snapshot[`isObjectOf > returns properly named function 3`] = ` })" `; -snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; - -snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; - -snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; - -snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; -snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index 7424e96..77c3176 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,35 +1,31 @@ export const snapshot = {}; -snapshot[`isAllOf > returns properly named function 1`] = ` -"isAllOf([ - isObjectOf({a: isNumber}), - isObjectOf({b: isString}) +snapshot[`isOneOf > returns properly named function 1`] = ` +"isOneOf([ + isNumber, + isString, + isBoolean ])" `; -snapshot[`isPickOf > returns properly named function 1`] = ` +snapshot[`isOmitOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, c: isBoolean })" `; -snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; +snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; -snapshot[`isPartialOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isOptionalOf(isNumber), - b: isOptionalOf(isString), - c: isOptionalOf(isBoolean) -})" -`; +snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; -snapshot[`isPartialOf > returns properly named function 2`] = ` -"isObjectOf({ - a: isOptionalOf(isNumber), - b: isOptionalOf(isString), - c: isOptionalOf(isBoolean) -})" +snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; + +snapshot[`isAllOf > returns properly named function 1`] = ` +"isAllOf([ + isObjectOf({a: isNumber}), + isObjectOf({b: isString}) +])" `; snapshot[`isStrictOf > returns properly named function 1`] = ` @@ -50,14 +46,27 @@ snapshot[`isStrictOf > returns properly named function 3`] = ` }))" `; -snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; +snapshot[`isPartialOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isOptionalOf(isNumber), + b: isOptionalOf(isString), + c: isOptionalOf(isBoolean) +})" +`; -snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; +snapshot[`isPartialOf > returns properly named function 2`] = ` +"isObjectOf({ + a: isOptionalOf(isNumber), + b: isOptionalOf(isString), + c: isOptionalOf(isBoolean) +})" +`; -snapshot[`isOneOf > returns properly named function 1`] = ` -"isOneOf([ - isNumber, - isString, - isBoolean -])" +snapshot[`isPickOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + c: isBoolean +})" `; + +snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; diff --git a/is/utility.ts b/is/utility.ts index a8a2aa0..b66031f 100644 --- a/is/utility.ts +++ b/is/utility.ts @@ -283,8 +283,49 @@ export function isPickOf< & WithMetadata; } +/** + * Return a type predicate function that returns `true` if the type of `x` is `Omit, K>`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```typescript + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.OmitOf(is.ObjectOf({ + * a: is.Number, + * b: is.String, + * c: is.OptionalOf(is.Boolean), + * }), ["a", "c"]); + * const a: unknown = { a: 0, b: "a", other: "other" }; + * if (isMyType(a)) { + * // The "a", "c", and "other" key in `a` is ignored. + * // 'a' is narrowed to { b: string } + * const _: { b: string } = a; + * } + * ``` + */ +export function isOmitOf< + T extends Record, + K extends keyof T, +>( + pred: Predicate & WithMetadata, + keys: K[], +): + & Predicate>> + & WithMetadata { + const s = new Set(keys); + const { args } = getPredicateMetadata(pred); + const predObj = Object.fromEntries( + Object.entries(args[0]).filter(([k]) => !s.has(k as K)), + ); + return isObjectOf(predObj) as + & Predicate>> + & WithMetadata; +} + export default { AllOf: isAllOf, + OmitOf: isOmitOf, OneOf: isOneOf, OptionalOf: isOptionalOf, PartialOf: isPartialOf, diff --git a/is/utility_test.ts b/is/utility_test.ts index a1d2717..08e2bcd 100644 --- a/is/utility_test.ts +++ b/is/utility_test.ts @@ -26,6 +26,7 @@ import { import { isObjectOf } from "./factory.ts"; import is, { isAllOf, + isOmitOf, isOneOf, isOptionalOf, isPartialOf, @@ -475,6 +476,51 @@ Deno.test("isPickOf", async (t) => { }); }); +Deno.test("isOmitOf", async (t) => { + const pred = isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean, + }); + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isOmitOf(pred, ["b"]).name); + // Nestable + await assertSnapshot(t, isOmitOf(isOmitOf(pred, ["b"]), ["c"]).name); + }); + await t.step("returns proper type predicate", () => { + const a: unknown = { a: 0, b: "a", c: true }; + if (isOmitOf(pred, ["b"])(a)) { + assertType< + Equal + >(true); + } + if (isOmitOf(isOmitOf(pred, ["b"]), ["c"])(a)) { + assertType< + Equal + >(true); + } + }); + await t.step("returns true on Omit object", () => { + assertEquals( + isOmitOf(pred, ["b"])({ a: 0, b: undefined, c: true }), + true, + ); + assertEquals(isOmitOf(pred, ["b", "c"])({ a: 0 }), true); + }); + await t.step("returns false on non Omit object", () => { + assertEquals( + isOmitOf(pred, ["b"])("a"), + false, + "Value is not an object", + ); + assertEquals( + isOmitOf(pred, ["b"])({ a: 0, b: "a", c: "" }), + false, + "Object have a different type property", + ); + }); +}); + Deno.test("is", async (t) => { const mod = await import("./utility.ts"); const casesOfAliasAndIsFunction = Object.entries(mod) From 6d65309f4d9433854be568ff4f2fb68a65eb49f2 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 22:21:46 +0900 Subject: [PATCH 17/31] :muscle: Define `isOptionalOf/isStrictOf` in `factory.ts` --- is/__snapshots__/factory_test.ts.snap | 156 +++++++++------- is/__snapshots__/utility_test.ts.snap | 56 ++---- is/factory.ts | 99 +++++++++- is/factory_test.ts | 248 +++++++++++++++++++++++++- is/utility.ts | 93 +--------- is/utility_test.ts | 247 +------------------------ 6 files changed, 452 insertions(+), 447 deletions(-) diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index 12410ef..adefe67 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,67 +1,19 @@ export const snapshot = {}; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; - snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; - -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; - -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ - isNumber, - isString, - isBoolean -])" -`; - -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; - -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ - isNumber, - isString, - isBoolean - ]) - ]) -])" -`; +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; -snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; - -snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; - -snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; - -snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; - -snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; - -snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; - -snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; - snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; - snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` "isReadonlyTupleOf([ isNumber, @@ -90,7 +42,47 @@ snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUnifor snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; + +snapshot[`isStrictOf > returns properly named function 1`] = ` +"isStrictOf(isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +}))" +`; + +snapshot[`isStrictOf > returns properly named function 2`] = `"isStrictOf(isObjectOf({a: a}))"`; + +snapshot[`isStrictOf > returns properly named function 3`] = ` +"isStrictOf(isObjectOf({ + a: isStrictOf(isObjectOf({ + b: isStrictOf(isObjectOf({c: isBoolean})) + })) +}))" +`; + +snapshot[`isObjectOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +})" +`; + +snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; + +snapshot[`isObjectOf > returns properly named function 3`] = ` +"isObjectOf({ + a: isObjectOf({ + b: isObjectOf({c: isBoolean}) + }) +})" +`; snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ @@ -114,10 +106,6 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` ])" `; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; - -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; - snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` "isReadonlyTupleOf([ isNumber, @@ -140,24 +128,58 @@ snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` ])" `; -snapshot[`isObjectOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - b: isString, - c: isBoolean -})" +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ + isNumber, + isString, + isBoolean +])" `; -snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; -snapshot[`isObjectOf > returns properly named function 3`] = ` -"isObjectOf({ - a: isObjectOf({ - b: isObjectOf({c: isBoolean}) - }) -})" +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ + isNumber, + isString, + isBoolean + ]) + ]) +])" `; +snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; + +snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; + +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; + +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; + snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; + +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; + +snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; + +snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; + +snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; + +snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; + +snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; + +snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; + +snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; + +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; + +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index 77c3176..ed1e6e3 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,26 +1,5 @@ export const snapshot = {}; -snapshot[`isOneOf > returns properly named function 1`] = ` -"isOneOf([ - isNumber, - isString, - isBoolean -])" -`; - -snapshot[`isOmitOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - c: isBoolean -})" -`; - -snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; - -snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; - -snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; - snapshot[`isAllOf > returns properly named function 1`] = ` "isAllOf([ isObjectOf({a: isNumber}), @@ -28,24 +7,6 @@ snapshot[`isAllOf > returns properly named function 1`] = ` ])" `; -snapshot[`isStrictOf > returns properly named function 1`] = ` -"isStrictOf(isObjectOf({ - a: isNumber, - b: isString, - c: isBoolean -}))" -`; - -snapshot[`isStrictOf > returns properly named function 2`] = `"isStrictOf(isObjectOf({a: a}))"`; - -snapshot[`isStrictOf > returns properly named function 3`] = ` -"isStrictOf(isObjectOf({ - a: isStrictOf(isObjectOf({ - b: isStrictOf(isObjectOf({c: isBoolean})) - })) -}))" -`; - snapshot[`isPartialOf > returns properly named function 1`] = ` "isObjectOf({ a: isOptionalOf(isNumber), @@ -62,6 +23,23 @@ snapshot[`isPartialOf > returns properly named function 2`] = ` })" `; +snapshot[`isOneOf > returns properly named function 1`] = ` +"isOneOf([ + isNumber, + isString, + isBoolean +])" +`; + +snapshot[`isOmitOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + c: isBoolean +})" +`; + +snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; + snapshot[`isPickOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, diff --git a/is/factory.ts b/is/factory.ts index 5af51b9..d9889dc 100644 --- a/is/factory.ts +++ b/is/factory.ts @@ -6,9 +6,14 @@ import { isMap, isRecord, isSet, + isUndefined, type Primitive, } from "./core.ts"; -import { setPredicateMetadata, type WithMetadata } from "../metadata.ts"; +import { + getPredicateMetadata, + setPredicateMetadata, + type WithMetadata, +} from "../metadata.ts"; /** * Return a type predicate function that returns `true` if the type of `x` is `T[]`. @@ -548,7 +553,7 @@ export function isObjectOf< ); } -export type Optional = { +type Optional = { optional: true; }; @@ -570,6 +575,94 @@ export type IsObjectOfMetadata = { args: [Parameters[0]]; }; +/** + * Return a type predicate function that returns `true` if the type of `x` is `T` or `undefined`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.OptionalOf(is.String); + * const a: unknown = "a"; + * if (isMyType(a)) { + * // a is narrowed to string | undefined + * const _: string | undefined = a; + * } + * ``` + */ +export function isOptionalOf( + pred: Predicate, +): Predicate & Optional & WithMetadata { + if ((pred as Partial).optional) { + return pred as + & Predicate + & Optional + & WithMetadata; + } + return Object.defineProperties( + setPredicateMetadata( + (x: unknown): x is Predicate => isUndefined(x) || pred(x), + { name: "isOptionalOf", args: [pred] }, + ), + { optional: { value: true as const } }, + ) as Predicate & Optional & WithMetadata; +} + +type IsOptionalOfMetadata = { + name: "isOptionalOf"; + args: Parameters; +}; + +/** + * Return a type predicate function that returns `true` if the type of `x` is strictly follow the `ObjectOf`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * If `is.OptionalOf()` is specified in the predicate function, the property becomes optional. + * + * The number of keys of `x` must be equal to the number of non optional keys of `predObj`. This is equivalent to + * the deprecated `options.strict` in `isObjectOf()`. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.StrictOf(is.ObjectOf({ + * a: is.Number, + * b: is.String, + * c: is.OptionalOf(is.Boolean), + * })); + * const a: unknown = { a: 0, b: "a", other: "other" }; + * if (isMyType(a)) { + * // This block will not be executed because of "other" key in `a`. + * } + * ``` + */ +export function isStrictOf>( + pred: + & Predicate + & WithMetadata, +): + & Predicate + & WithMetadata { + const { args } = getPredicateMetadata(pred); + const s = new Set(Object.keys(args[0])); + return setPredicateMetadata( + (x: unknown): x is T => { + if (!pred(x)) return false; + // deno-lint-ignore no-explicit-any + const ks = Object.keys(x as any); + return ks.length <= s.size && ks.every((k) => s.has(k)); + }, + { name: "isStrictOf", args: [pred] }, + ); +} + +type IsStrictOfMetadata = { + name: "isStrictOf"; + args: Parameters; +}; + /** * Return `true` if the type of `x` is instance of `ctor`. * @@ -669,10 +762,12 @@ export default { LiteralOneOf: isLiteralOneOf, MapOf: isMapOf, ObjectOf: isObjectOf, + OptionalOf: isOptionalOf, ReadonlyTupleOf: isReadonlyTupleOf, ReadonlyUniformTupleOf: isReadonlyUniformTupleOf, RecordOf: isRecordOf, SetOf: isSetOf, + StrictOf: isStrictOf, TupleOf: isTupleOf, UniformTupleOf: isUniformTupleOf, }; diff --git a/is/factory_test.ts b/is/factory_test.ts index 669b4d8..0453f48 100644 --- a/is/factory_test.ts +++ b/is/factory_test.ts @@ -8,7 +8,21 @@ import { import { assertType } from "https://deno.land/std@0.211.0/testing/types.ts"; import { type Equal, stringify } from "./_testutil.ts"; import { type Predicate } from "./type.ts"; -import { isArray, isBoolean, isNumber, isString } from "./core.ts"; +import { + isArray, + isAsyncFunction, + isBigInt, + isBoolean, + isFunction, + isNull, + isNumber, + isRecord, + isSet, + isString, + isSymbol, + isSyncFunction, + isUndefined, +} from "./core.ts"; import is, { isArrayOf, isInstanceOf, @@ -16,13 +30,16 @@ import is, { isLiteralOneOf, isMapOf, isObjectOf, + isOptionalOf, isReadonlyTupleOf, isReadonlyUniformTupleOf, isRecordOf, isSetOf, + isStrictOf, isTupleOf, isUniformTupleOf, } from "./factory.ts"; +import { isOneOf } from "./utility.ts"; const examples = { string: ["", "Hello world"], @@ -616,6 +633,235 @@ Deno.test("isObjectOf", async (t) => { ); }); +Deno.test("isOptionalOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isOptionalOf(isNumber).name); + // Nesting does nothing + await assertSnapshot(t, isOptionalOf(isOptionalOf(isNumber)).name); + }); + await t.step("returns proper type predicate", () => { + const a: unknown = undefined; + if (isOptionalOf(isNumber)(a)) { + assertType>(true); + } + }); + await t.step("with isString", async (t) => { + await testWithExamples(t, isOptionalOf(isString), { + validExamples: ["string", "undefined"], + }); + }); + await t.step("with isNumber", async (t) => { + await testWithExamples(t, isOptionalOf(isNumber), { + validExamples: ["number", "undefined"], + }); + }); + await t.step("with isBigInt", async (t) => { + await testWithExamples(t, isOptionalOf(isBigInt), { + validExamples: ["bigint", "undefined"], + }); + }); + await t.step("with isBoolean", async (t) => { + await testWithExamples(t, isOptionalOf(isBoolean), { + validExamples: ["boolean", "undefined"], + }); + }); + await t.step("with isArray", async (t) => { + await testWithExamples(t, isOptionalOf(isArray), { + validExamples: ["array", "undefined"], + }); + }); + await t.step("with isSet", async (t) => { + await testWithExamples(t, isOptionalOf(isSet), { + validExamples: ["set", "undefined"], + }); + }); + await t.step("with isRecord", async (t) => { + await testWithExamples(t, isOptionalOf(isRecord), { + validExamples: ["record", "undefined"], + }); + }); + await t.step("with isFunction", async (t) => { + await testWithExamples(t, isOptionalOf(isFunction), { + validExamples: ["syncFunction", "asyncFunction", "undefined"], + }); + }); + await t.step("with isSyncFunction", async (t) => { + await testWithExamples(t, isOptionalOf(isSyncFunction), { + validExamples: ["syncFunction", "undefined"], + }); + }); + await t.step("with isAsyncFunction", async (t) => { + await testWithExamples(t, isOptionalOf(isAsyncFunction), { + validExamples: ["asyncFunction", "undefined"], + }); + }); + await t.step("with isNull", async (t) => { + await testWithExamples(t, isOptionalOf(isNull), { + validExamples: ["null", "undefined"], + }); + }); + await t.step("with isUndefined", async (t) => { + await testWithExamples(t, isOptionalOf(isUndefined), { + validExamples: ["undefined"], + }); + }); + await t.step("with isSymbol", async (t) => { + await testWithExamples(t, isOptionalOf(isSymbol), { + validExamples: ["symbol", "undefined"], + }); + }); +}); + +Deno.test("isStrictOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot( + t, + isStrictOf(isObjectOf({ a: isNumber, b: isString, c: isBoolean })).name, + ); + await assertSnapshot( + t, + isStrictOf(isObjectOf({ a: (_x): _x is string => false })).name, + ); + // Nested + await assertSnapshot( + t, + isStrictOf( + isObjectOf({ + a: isStrictOf( + isObjectOf({ b: isStrictOf(isObjectOf({ c: isBoolean })) }), + ), + }), + ).name, + ); + }); + await t.step("returns proper type predicate", () => { + const predObj = { + a: isNumber, + b: isString, + c: isBoolean, + }; + const a: unknown = { a: 0, b: "a", c: true }; + if (isStrictOf(isObjectOf(predObj))(a)) { + assertType>(true); + } + }); + await t.step("returns true on T object", () => { + const predObj = { + a: isNumber, + b: isString, + c: isBoolean, + }; + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: true }), + true, + ); + }); + await t.step("returns false on non T object", () => { + const predObj = { + a: isNumber, + b: isString, + c: isBoolean, + }; + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: "" }), + false, + "Object have a different type property", + ); + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a" }), + false, + "Object does not have one property", + ); + assertEquals( + isStrictOf(isObjectOf(predObj))({ + a: 0, + b: "a", + c: true, + d: "invalid", + }), + false, + "Object have an unknown property", + ); + }); + await testWithExamples( + t, + isStrictOf(isObjectOf({ a: (_: unknown): _ is unknown => false })), + { excludeExamples: ["record"] }, + ); + await t.step("with optional properties", async (t) => { + await t.step("returns proper type predicate", () => { + const predObj = { + a: isNumber, + b: isOneOf([isString, isUndefined]), + c: isOptionalOf(isBoolean), + }; + const a: unknown = { a: 0, b: "a" }; + if (isStrictOf(isObjectOf(predObj))(a)) { + assertType< + Equal + >(true); + } + }); + await t.step("returns true on T object", () => { + const predObj = { + a: isNumber, + b: isOneOf([isString, isUndefined]), + c: isOptionalOf(isBoolean), + }; + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: true }), + true, + ); + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a" }), + true, + "Object does not have an optional property", + ); + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: undefined }), + true, + "Object has `undefined` as value of optional property", + ); + }); + await t.step("returns false on non T object", () => { + const predObj = { + a: isNumber, + b: isOneOf([isString, isUndefined]), + c: isOptionalOf(isBoolean), + }; + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: "" }), + false, + "Object have a different type property", + ); + assertEquals( + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: null }), + false, + "Object has `null` as value of optional property", + ); + assertEquals( + isStrictOf(isObjectOf(predObj))({ + a: 0, + b: "a", + c: true, + d: "invalid", + }), + false, + "Object have an unknown property", + ); + assertEquals( + isStrictOf(isObjectOf(predObj))({ + a: 0, + b: "a", + d: "invalid", + }), + false, + "Object have the same number of properties but an unknown property exists", + ); + }); + }); +}); + Deno.test("isInstanceOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot(t, isInstanceOf(Date).name); diff --git a/is/utility.ts b/is/utility.ts index b66031f..cd85a4a 100644 --- a/is/utility.ts +++ b/is/utility.ts @@ -1,10 +1,9 @@ import type { FlatType, UnionToIntersection } from "../_typeutil.ts"; import type { Predicate } from "./type.ts"; -import { isUndefined } from "./core.ts"; import { isObjectOf, type IsObjectOfMetadata, - type Optional, + isOptionalOf, } from "./factory.ts"; import { getPredicateMetadata, @@ -118,94 +117,6 @@ type IsAllOfMetadata = { args: Parameters; }; -/** - * Return a type predicate function that returns `true` if the type of `x` is `T` or `undefined`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.OptionalOf(is.String); - * const a: unknown = "a"; - * if (isMyType(a)) { - * // a is narrowed to string | undefined - * const _: string | undefined = a; - * } - * ``` - */ -export function isOptionalOf( - pred: Predicate, -): Predicate & Optional & WithMetadata { - if ((pred as Partial).optional) { - return pred as - & Predicate - & Optional - & WithMetadata; - } - return Object.defineProperties( - setPredicateMetadata( - (x: unknown): x is Predicate => isUndefined(x) || pred(x), - { name: "isOptionalOf", args: [pred] }, - ), - { optional: { value: true as const } }, - ) as Predicate & Optional & WithMetadata; -} - -type IsOptionalOfMetadata = { - name: "isOptionalOf"; - args: Parameters; -}; - -/** - * Return a type predicate function that returns `true` if the type of `x` is strictly follow the `ObjectOf`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * If `is.OptionalOf()` is specified in the predicate function, the property becomes optional. - * - * The number of keys of `x` must be equal to the number of non optional keys of `predObj`. This is equivalent to - * the deprecated `options.strict` in `isObjectOf()`. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.StrictOf(is.ObjectOf({ - * a: is.Number, - * b: is.String, - * c: is.OptionalOf(is.Boolean), - * })); - * const a: unknown = { a: 0, b: "a", other: "other" }; - * if (isMyType(a)) { - * // This block will not be executed because of "other" key in `a`. - * } - * ``` - */ -export function isStrictOf>( - pred: - & Predicate - & WithMetadata, -): - & Predicate - & WithMetadata { - const { args } = getPredicateMetadata(pred); - const s = new Set(Object.keys(args[0])); - return setPredicateMetadata( - (x: unknown): x is T => { - if (!pred(x)) return false; - // deno-lint-ignore no-explicit-any - const ks = Object.keys(x as any); - return ks.length <= s.size && ks.every((k) => s.has(k)); - }, - { name: "isStrictOf", args: [pred] }, - ); -} - -type IsStrictOfMetadata = { - name: "isStrictOf"; - args: Parameters; -}; - /** * Return a type predicate function that returns `true` if the type of `x` is `Partial>`. * @@ -327,8 +238,6 @@ export default { AllOf: isAllOf, OmitOf: isOmitOf, OneOf: isOneOf, - OptionalOf: isOptionalOf, PartialOf: isPartialOf, PickOf: isPickOf, - StrictOf: isStrictOf, }; diff --git a/is/utility_test.ts b/is/utility_test.ts index 08e2bcd..d7cd387 100644 --- a/is/utility_test.ts +++ b/is/utility_test.ts @@ -8,30 +8,14 @@ import { import { assertType } from "https://deno.land/std@0.211.0/testing/types.ts"; import { type Equal, stringify } from "./_testutil.ts"; import { type Predicate, type PredicateType } from "./type.ts"; -import { - isArray, - isAsyncFunction, - isBigInt, - isBoolean, - isFunction, - isNull, - isNumber, - isRecord, - isSet, - isString, - isSymbol, - isSyncFunction, - isUndefined, -} from "./core.ts"; +import { isBoolean, isNumber, isString } from "./core.ts"; import { isObjectOf } from "./factory.ts"; import is, { isAllOf, isOmitOf, isOneOf, - isOptionalOf, isPartialOf, isPickOf, - isStrictOf, } from "./utility.ts"; const examples = { @@ -166,235 +150,6 @@ Deno.test("isAllOf", async (t) => { }); }); -Deno.test("isOptionalOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isOptionalOf(isNumber).name); - // Nesting does nothing - await assertSnapshot(t, isOptionalOf(isOptionalOf(isNumber)).name); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = undefined; - if (isOptionalOf(isNumber)(a)) { - assertType>(true); - } - }); - await t.step("with isString", async (t) => { - await testWithExamples(t, isOptionalOf(isString), { - validExamples: ["string", "undefined"], - }); - }); - await t.step("with isNumber", async (t) => { - await testWithExamples(t, isOptionalOf(isNumber), { - validExamples: ["number", "undefined"], - }); - }); - await t.step("with isBigInt", async (t) => { - await testWithExamples(t, isOptionalOf(isBigInt), { - validExamples: ["bigint", "undefined"], - }); - }); - await t.step("with isBoolean", async (t) => { - await testWithExamples(t, isOptionalOf(isBoolean), { - validExamples: ["boolean", "undefined"], - }); - }); - await t.step("with isArray", async (t) => { - await testWithExamples(t, isOptionalOf(isArray), { - validExamples: ["array", "undefined"], - }); - }); - await t.step("with isSet", async (t) => { - await testWithExamples(t, isOptionalOf(isSet), { - validExamples: ["set", "undefined"], - }); - }); - await t.step("with isRecord", async (t) => { - await testWithExamples(t, isOptionalOf(isRecord), { - validExamples: ["record", "undefined"], - }); - }); - await t.step("with isFunction", async (t) => { - await testWithExamples(t, isOptionalOf(isFunction), { - validExamples: ["syncFunction", "asyncFunction", "undefined"], - }); - }); - await t.step("with isSyncFunction", async (t) => { - await testWithExamples(t, isOptionalOf(isSyncFunction), { - validExamples: ["syncFunction", "undefined"], - }); - }); - await t.step("with isAsyncFunction", async (t) => { - await testWithExamples(t, isOptionalOf(isAsyncFunction), { - validExamples: ["asyncFunction", "undefined"], - }); - }); - await t.step("with isNull", async (t) => { - await testWithExamples(t, isOptionalOf(isNull), { - validExamples: ["null", "undefined"], - }); - }); - await t.step("with isUndefined", async (t) => { - await testWithExamples(t, isOptionalOf(isUndefined), { - validExamples: ["undefined"], - }); - }); - await t.step("with isSymbol", async (t) => { - await testWithExamples(t, isOptionalOf(isSymbol), { - validExamples: ["symbol", "undefined"], - }); - }); -}); - -Deno.test("isStrictOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot( - t, - isStrictOf(isObjectOf({ a: isNumber, b: isString, c: isBoolean })).name, - ); - await assertSnapshot( - t, - isStrictOf(isObjectOf({ a: (_x): _x is string => false })).name, - ); - // Nested - await assertSnapshot( - t, - isStrictOf( - isObjectOf({ - a: isStrictOf( - isObjectOf({ b: isStrictOf(isObjectOf({ c: isBoolean })) }), - ), - }), - ).name, - ); - }); - await t.step("returns proper type predicate", () => { - const predObj = { - a: isNumber, - b: isString, - c: isBoolean, - }; - const a: unknown = { a: 0, b: "a", c: true }; - if (isStrictOf(isObjectOf(predObj))(a)) { - assertType>(true); - } - }); - await t.step("returns true on T object", () => { - const predObj = { - a: isNumber, - b: isString, - c: isBoolean, - }; - assertEquals( - isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: true }), - true, - ); - }); - await t.step("returns false on non T object", () => { - const predObj = { - a: isNumber, - b: isString, - c: isBoolean, - }; - assertEquals( - isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: "" }), - false, - "Object have a different type property", - ); - assertEquals( - isStrictOf(isObjectOf(predObj))({ a: 0, b: "a" }), - false, - "Object does not have one property", - ); - assertEquals( - isStrictOf(isObjectOf(predObj))({ - a: 0, - b: "a", - c: true, - d: "invalid", - }), - false, - "Object have an unknown property", - ); - }); - await testWithExamples( - t, - isStrictOf(isObjectOf({ a: (_: unknown): _ is unknown => false })), - { excludeExamples: ["record"] }, - ); - await t.step("with optional properties", async (t) => { - await t.step("returns proper type predicate", () => { - const predObj = { - a: isNumber, - b: isOneOf([isString, isUndefined]), - c: isOptionalOf(isBoolean), - }; - const a: unknown = { a: 0, b: "a" }; - if (isStrictOf(isObjectOf(predObj))(a)) { - assertType< - Equal - >(true); - } - }); - await t.step("returns true on T object", () => { - const predObj = { - a: isNumber, - b: isOneOf([isString, isUndefined]), - c: isOptionalOf(isBoolean), - }; - assertEquals( - isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: true }), - true, - ); - assertEquals( - isStrictOf(isObjectOf(predObj))({ a: 0, b: "a" }), - true, - "Object does not have an optional property", - ); - assertEquals( - isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: undefined }), - true, - "Object has `undefined` as value of optional property", - ); - }); - await t.step("returns false on non T object", () => { - const predObj = { - a: isNumber, - b: isOneOf([isString, isUndefined]), - c: isOptionalOf(isBoolean), - }; - assertEquals( - isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: "" }), - false, - "Object have a different type property", - ); - assertEquals( - isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: null }), - false, - "Object has `null` as value of optional property", - ); - assertEquals( - isStrictOf(isObjectOf(predObj))({ - a: 0, - b: "a", - c: true, - d: "invalid", - }), - false, - "Object have an unknown property", - ); - assertEquals( - isStrictOf(isObjectOf(predObj))({ - a: 0, - b: "a", - d: "invalid", - }), - false, - "Object have the same number of properties but an unknown property exists", - ); - }); - }); -}); - Deno.test("isPartialOf", async (t) => { const pred = isObjectOf({ a: isNumber, From 39f1d8d9ad6dba376ac9dc680eecce66853b13a1 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 22:26:56 +0900 Subject: [PATCH 18/31] :muscle: Use `isStrictOf` in `isObjectOf` for deprecated option --- is/factory.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/is/factory.ts b/is/factory.ts index d9889dc..cb12bae 100644 --- a/is/factory.ts +++ b/is/factory.ts @@ -530,16 +530,8 @@ export function isObjectOf< options?: { strict?: boolean }, ): Predicate> & WithMetadata { if (options?.strict) { - const keys = new Set(Object.keys(predObj)); - const pred = isObjectOf(predObj); - return setPredicateMetadata( - (x: unknown): x is ObjectOf => { - if (!pred(x)) return false; - const ks = Object.keys(x); - return ks.length <= keys.size && ks.every((k) => keys.has(k)); - }, - { name: "isObjectOf", args: [predObj] }, - ); + // deno-lint-ignore no-explicit-any + return isStrictOf(isObjectOf(predObj)) as any; } return setPredicateMetadata( (x: unknown): x is ObjectOf => { From c4ff5619045ab084eb3eaa6ff391c376f84b9754 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 22:27:18 +0900 Subject: [PATCH 19/31] :rocket: Improve `isObjectOf` detection performance a bit --- is/factory.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/is/factory.ts b/is/factory.ts index cb12bae..faace62 100644 --- a/is/factory.ts +++ b/is/factory.ts @@ -533,9 +533,16 @@ export function isObjectOf< // deno-lint-ignore no-explicit-any return isStrictOf(isObjectOf(predObj)) as any; } + const requiredKeys = Object.entries(predObj) + .filter(([_, v]) => !isOptional(v)) + .map(([k]) => k); return setPredicateMetadata( (x: unknown): x is ObjectOf => { if (!isRecord(x)) return false; + // Check required keys + const s = new Set(Object.keys(x)); + if (requiredKeys.some((k) => !s.has(k))) return false; + // Check each values for (const k in predObj) { if (!predObj[k](x[k])) return false; } @@ -545,6 +552,10 @@ export function isObjectOf< ); } +function isOptional(x: unknown): x is Optional { + return (x as Partial).optional === true; +} + type Optional = { optional: true; }; From 992f2510e0878581e3ec1ad55910519e30897db6 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 22:46:45 +0900 Subject: [PATCH 20/31] :rocket: Simplify and improve performance of `isAllOf` Predicates functions other than `isObjectOf` do not make sense with `isAllOf`. Therefore, the implementation of `isAllOf` has been simplified to internally utilize `isObjectOf`. --- is/__snapshots__/factory_test.ts.snap | 172 +++++++++++++------------- is/__snapshots__/utility_test.ts.snap | 36 +++--- is/utility.ts | 23 ++-- is_bench.ts | 5 +- 4 files changed, 120 insertions(+), 116 deletions(-) diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index adefe67..cf37d5d 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,18 +1,64 @@ export const snapshot = {}; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ + isNumber, + isString, + isBoolean +])" +`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ + isNumber, + isString, + isBoolean + ]) + ]) +])" +`; + +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyTupleOf([ + isNumber, + isString, + isBoolean +])" +`; -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; + +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyTupleOf([ + isReadonlyTupleOf([ + isReadonlyTupleOf([ + isNumber, + isString, + isBoolean + ]) + ]) +])" +`; + +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; + +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; + +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; + +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` "isReadonlyTupleOf([ @@ -42,11 +88,27 @@ snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUnifor snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; +snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; + +snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; + +snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; + +snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; + +snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; + +snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; + +snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; + +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; snapshot[`isStrictOf > returns properly named function 1`] = ` "isStrictOf(isObjectOf({ @@ -66,6 +128,18 @@ snapshot[`isStrictOf > returns properly named function 3`] = ` }))" `; +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; + +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; + +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; + snapshot[`isObjectOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, @@ -84,6 +158,10 @@ snapshot[`isObjectOf > returns properly named function 3`] = ` })" `; +snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; + +snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; + snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ isNumber, @@ -105,81 +183,3 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` ], isArray) ])" `; - -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ - isNumber, - isString, - isBoolean -])" -`; - -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; - -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ - isNumber, - isString, - isBoolean - ]) - ]) -])" -`; - -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ - isNumber, - isString, - isBoolean -])" -`; - -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; - -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ - isNumber, - isString, - isBoolean - ]) - ]) -])" -`; - -snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; - -snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; - -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; - -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; - -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; - -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; - -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; - -snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; - -snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; - -snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; - -snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; - -snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; - -snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; - -snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; - -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; - -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index ed1e6e3..4d7f804 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,9 +1,19 @@ export const snapshot = {}; -snapshot[`isAllOf > returns properly named function 1`] = ` -"isAllOf([ - isObjectOf({a: isNumber}), - isObjectOf({b: isString}) +snapshot[`isOmitOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + c: isBoolean +})" +`; + +snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; + +snapshot[`isOneOf > returns properly named function 1`] = ` +"isOneOf([ + isNumber, + isString, + isBoolean ])" `; @@ -23,28 +33,18 @@ snapshot[`isPartialOf > returns properly named function 2`] = ` })" `; -snapshot[`isOneOf > returns properly named function 1`] = ` -"isOneOf([ - isNumber, - isString, - isBoolean -])" -`; - -snapshot[`isOmitOf > returns properly named function 1`] = ` +snapshot[`isPickOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, c: isBoolean })" `; -snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; +snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; -snapshot[`isPickOf > returns properly named function 1`] = ` +snapshot[`isAllOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, - c: isBoolean + b: isString })" `; - -snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; diff --git a/is/utility.ts b/is/utility.ts index cd85a4a..f5b3db2 100644 --- a/is/utility.ts +++ b/is/utility.ts @@ -100,23 +100,24 @@ type IsOneOfMetadata = { * ``` */ export function isAllOf< - T extends readonly [Predicate, ...Predicate[]], + T extends readonly [ + Predicate & WithMetadata, + ...(Predicate & WithMetadata)[], + ], >( preds: T, -): Predicate> & WithMetadata { - return setPredicateMetadata( - (x: unknown): x is AllOf => preds.every((pred) => pred(x)), - { name: "isAllOf", args: [preds] }, - ); +): Predicate> & WithMetadata { + const predObj = {}; + preds.forEach((pred) => { + Object.assign(predObj, getPredicateMetadata(pred).args[0]); + }); + return isObjectOf(predObj) as + & Predicate> + & WithMetadata; } type AllOf = UnionToIntersection>; -type IsAllOfMetadata = { - name: "isAllOf"; - args: Parameters; -}; - /** * Return a type predicate function that returns `true` if the type of `x` is `Partial>`. * diff --git a/is_bench.ts b/is_bench.ts index adad9e2..d814879 100644 --- a/is_bench.ts +++ b/is_bench.ts @@ -513,7 +513,10 @@ Deno.bench({ }, }); -const predsAll = [is.String, is.Number, is.Boolean] as const; +const predsAll = [ + is.ObjectOf({ a: is.Number }), + is.ObjectOf({ b: is.String }), +] as const; Deno.bench({ name: "is.AllOf", fn: () => { From 317618afabce7e7cdd7df55bf30fbb4e99082a8c Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 22:49:43 +0900 Subject: [PATCH 21/31] :muscle: Use `export type` instead --- is.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/is.ts b/is.ts index 5700029..0c62217 100644 --- a/is.ts +++ b/is.ts @@ -2,7 +2,7 @@ import core from "./is/core.ts"; import factory from "./is/factory.ts"; import utility from "./is/utility.ts"; -export * from "./is/type.ts"; +export type * from "./is/type.ts"; export * from "./is/core.ts"; export * from "./is/factory.ts"; export * from "./is/utility.ts"; From da4f708a2036c090d10ba7dfb58d8498124d23cc Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 23:08:41 +0900 Subject: [PATCH 22/31] :+1: Rename `isOneOf/isAllOf` to `isUnionOf/isIntersectionOf` --- is/__snapshots__/factory_test.ts.snap | 200 +++++++++++++------------- is/__snapshots__/utility_test.ts.snap | 28 ++-- is/factory_test.ts | 8 +- is/utility.ts | 69 ++++++--- is/utility_test.ts | 34 ++--- 5 files changed, 185 insertions(+), 154 deletions(-) diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index cf37d5d..d979b23 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,86 +1,28 @@ export const snapshot = {}; -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ - isNumber, - isString, - isBoolean -])" -`; - -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; - -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ - isNumber, - isString, - isBoolean - ]) - ]) -])" -`; - -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; - -snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; +snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; -snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; +snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ - isNumber, - isString, - isBoolean -])" -`; +snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; +snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ - isNumber, - isString, - isBoolean - ]) - ]) -])" -`; +snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; +snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; +snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ - isNumber, - isString, - isBoolean -], isArray)" -`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ - isNumber, - isString, - isBoolean - ], isArray) - ], isArray) -], isArray)" -`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; @@ -88,27 +30,9 @@ snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUnifor snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; - -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; - -snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; - -snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; - -snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; - -snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; - -snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; - -snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; - -snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; - -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; snapshot[`isStrictOf > returns properly named function 1`] = ` "isStrictOf(isObjectOf({ @@ -128,17 +52,9 @@ snapshot[`isStrictOf > returns properly named function 3`] = ` }))" `; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; - -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; - -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; snapshot[`isObjectOf > returns properly named function 1`] = ` "isObjectOf({ @@ -158,6 +74,58 @@ snapshot[`isObjectOf > returns properly named function 3`] = ` })" `; +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyTupleOf([ + isNumber, + isString, + isBoolean +], isArray)" +`; + +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; + +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyTupleOf([ + isReadonlyTupleOf([ + isReadonlyTupleOf([ + isNumber, + isString, + isBoolean + ], isArray) + ], isArray) +], isArray)" +`; + +snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; + +snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; + +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; + +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; + +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ + isNumber, + isString, + isBoolean +])" +`; + +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; + +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ + isNumber, + isString, + isBoolean + ]) + ]) +])" +`; + snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; @@ -183,3 +151,35 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` ], isArray) ])" `; + +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; + +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; + +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; + +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; + +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyTupleOf([ + isNumber, + isString, + isBoolean +])" +`; + +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; + +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyTupleOf([ + isReadonlyTupleOf([ + isReadonlyTupleOf([ + isNumber, + isString, + isBoolean + ]) + ]) +])" +`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index 4d7f804..ea61c03 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,22 +1,31 @@ export const snapshot = {}; -snapshot[`isOmitOf > returns properly named function 1`] = ` +snapshot[`isPickOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, c: isBoolean })" `; -snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; +snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; -snapshot[`isOneOf > returns properly named function 1`] = ` -"isOneOf([ +snapshot[`isUnionOf > returns properly named function 1`] = ` +"isUnionOf([ isNumber, isString, isBoolean ])" `; +snapshot[`isOmitOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + c: isBoolean +})" +`; + +snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; + snapshot[`isPartialOf > returns properly named function 1`] = ` "isObjectOf({ a: isOptionalOf(isNumber), @@ -33,16 +42,7 @@ snapshot[`isPartialOf > returns properly named function 2`] = ` })" `; -snapshot[`isPickOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - c: isBoolean -})" -`; - -snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; - -snapshot[`isAllOf > returns properly named function 1`] = ` +snapshot[`isIntersectionOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, b: isString diff --git a/is/factory_test.ts b/is/factory_test.ts index 0453f48..509c090 100644 --- a/is/factory_test.ts +++ b/is/factory_test.ts @@ -39,7 +39,7 @@ import is, { isTupleOf, isUniformTupleOf, } from "./factory.ts"; -import { isOneOf } from "./utility.ts"; +import { isUnionOf } from "./utility.ts"; const examples = { string: ["", "Hello world"], @@ -792,7 +792,7 @@ Deno.test("isStrictOf", async (t) => { await t.step("returns proper type predicate", () => { const predObj = { a: isNumber, - b: isOneOf([isString, isUndefined]), + b: isUnionOf([isString, isUndefined]), c: isOptionalOf(isBoolean), }; const a: unknown = { a: 0, b: "a" }; @@ -805,7 +805,7 @@ Deno.test("isStrictOf", async (t) => { await t.step("returns true on T object", () => { const predObj = { a: isNumber, - b: isOneOf([isString, isUndefined]), + b: isUnionOf([isString, isUndefined]), c: isOptionalOf(isBoolean), }; assertEquals( @@ -826,7 +826,7 @@ Deno.test("isStrictOf", async (t) => { await t.step("returns false on non T object", () => { const predObj = { a: isNumber, - b: isOneOf([isString, isUndefined]), + b: isUnionOf([isString, isUndefined]), c: isOptionalOf(isBoolean), }; assertEquals( diff --git a/is/utility.ts b/is/utility.ts index f5b3db2..6be2943 100644 --- a/is/utility.ts +++ b/is/utility.ts @@ -12,14 +12,14 @@ import { } from "../metadata.ts"; /** - * Return a type predicate function that returns `true` if the type of `x` is `OneOf`. + * Return a type predicate function that returns `true` if the type of `x` is `UnionOf`. * * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. * * ```ts * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; * - * const isMyType = is.OneOf([is.Number, is.String, is.Boolean]); + * const isMyType = is.UnionOf([is.Number, is.String, is.Boolean]); * const a: unknown = 0; * if (isMyType(a)) { * // a is narrowed to number | string | boolean @@ -34,7 +34,7 @@ import { * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; * * const preds = [is.Number, is.String, is.Boolean] as const; - * const isMyType = is.OneOf(preds); + * const isMyType = is.UnionOf(preds); * const a: unknown = 0; * if (isMyType(a)) { * // a is narrowed to number | string | boolean @@ -42,35 +42,48 @@ import { * } * ``` */ -export function isOneOf< +export function isUnionOf< T extends readonly [Predicate, ...Predicate[]], >( preds: T, -): Predicate> & WithMetadata { +): Predicate> & WithMetadata { return setPredicateMetadata( - (x: unknown): x is OneOf => preds.some((pred) => pred(x)), - { name: "isOneOf", args: [preds] }, + (x: unknown): x is UnionOf => preds.some((pred) => pred(x)), + { name: "isUnionOf", args: [preds] }, ); } -type OneOf = T extends readonly [Predicate, ...infer R] - ? U | OneOf +type UnionOf = T extends readonly [Predicate, ...infer R] + ? U | UnionOf : never; -type IsOneOfMetadata = { - name: "isOneOf"; - args: Parameters; +type IsUnionOfMetadata = { + name: "isUnionOf"; + args: Parameters; }; /** - * Return a type predicate function that returns `true` if the type of `x` is `AllOf`. + * Return a type predicate function that returns `true` if the type of `x` is `UnionOf`. + * + * @deprecated Use `isUnionOf` instead. + */ +export function isOneOf< + T extends readonly [Predicate, ...Predicate[]], +>( + preds: T, +): Predicate> & WithMetadata { + return isUnionOf(preds); +} + +/** + * Return a type predicate function that returns `true` if the type of `x` is `IntersectionOf`. * * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. * * ```ts * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; * - * const isMyType = is.AllOf([ + * const isMyType = is.IntersectionOf([ * is.ObjectOf({ a: is.Number }), * is.ObjectOf({ b: is.String }), * ]); @@ -91,7 +104,7 @@ type IsOneOfMetadata = { * is.ObjectOf({ a: is.Number }), * is.ObjectOf({ b: is.String }), * ] as const - * const isMyType = is.AllOf(preds); + * const isMyType = is.IntersectionOf(preds); * const a: unknown = { a: 0, b: "a" }; * if (isMyType(a)) { * // a is narrowed to { a: number } & { b: string } @@ -99,24 +112,40 @@ type IsOneOfMetadata = { * } * ``` */ -export function isAllOf< +export function isIntersectionOf< T extends readonly [ Predicate & WithMetadata, ...(Predicate & WithMetadata)[], ], >( preds: T, -): Predicate> & WithMetadata { +): Predicate> & WithMetadata { const predObj = {}; preds.forEach((pred) => { Object.assign(predObj, getPredicateMetadata(pred).args[0]); }); return isObjectOf(predObj) as - & Predicate> + & Predicate> & WithMetadata; } -type AllOf = UnionToIntersection>; +type IntersectionOf = UnionToIntersection>; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `IntersectionOf`. + * + * @deprecated Use `isIntersectionOf` instead. + */ +export function isAllOf< + T extends readonly [ + Predicate & WithMetadata, + ...(Predicate & WithMetadata)[], + ], +>( + preds: T, +): Predicate> & WithMetadata { + return isIntersectionOf(preds); +} /** * Return a type predicate function that returns `true` if the type of `x` is `Partial>`. @@ -237,8 +266,10 @@ export function isOmitOf< export default { AllOf: isAllOf, + IntersectionOf: isIntersectionOf, OmitOf: isOmitOf, OneOf: isOneOf, PartialOf: isPartialOf, PickOf: isPickOf, + UnionOf: isUnionOf, }; diff --git a/is/utility_test.ts b/is/utility_test.ts index d7cd387..498448d 100644 --- a/is/utility_test.ts +++ b/is/utility_test.ts @@ -11,11 +11,11 @@ import { type Predicate, type PredicateType } from "./type.ts"; import { isBoolean, isNumber, isString } from "./core.ts"; import { isObjectOf } from "./factory.ts"; import is, { - isAllOf, + isIntersectionOf, isOmitOf, - isOneOf, isPartialOf, isPickOf, + isUnionOf, } from "./utility.ts"; const examples = { @@ -66,14 +66,14 @@ async function testWithExamples( } } -Deno.test("isOneOf", async (t) => { +Deno.test("isUnionOf", async (t) => { await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isOneOf([isNumber, isString, isBoolean]).name); + await assertSnapshot(t, isUnionOf([isNumber, isString, isBoolean]).name); }); await t.step("returns proper type predicate", () => { const preds = [isNumber, isString, isBoolean] as const; const a: unknown = [0, "a", true]; - if (isOneOf(preds)(a)) { + if (isUnionOf(preds)(a)) { assertType>(true); } }); @@ -84,29 +84,29 @@ Deno.test("isOneOf", async (t) => { type Bar = PredicateType; const preds = [isFoo, isBar] as const; const a: unknown = [0, "a", true]; - if (isOneOf(preds)(a)) { + if (isUnionOf(preds)(a)) { assertType>(true); } }); await t.step("returns true on one of T", () => { const preds = [isNumber, isString, isBoolean] as const; - assertEquals(isOneOf(preds)(0), true); - assertEquals(isOneOf(preds)("a"), true); - assertEquals(isOneOf(preds)(true), true); + assertEquals(isUnionOf(preds)(0), true); + assertEquals(isUnionOf(preds)("a"), true); + assertEquals(isUnionOf(preds)(true), true); }); await t.step("returns false on non of T", async (t) => { const preds = [isNumber, isString, isBoolean] as const; - await testWithExamples(t, isOneOf(preds), { + await testWithExamples(t, isUnionOf(preds), { excludeExamples: ["number", "string", "boolean"], }); }); }); -Deno.test("isAllOf", async (t) => { +Deno.test("isIntersectionOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot( t, - isAllOf([ + isIntersectionOf([ isObjectOf({ a: isNumber }), isObjectOf({ b: isString }), ]).name, @@ -118,7 +118,7 @@ Deno.test("isAllOf", async (t) => { isObjectOf({ b: isString }), ] as const; const a: unknown = { a: 0, b: "a" }; - if (isAllOf(preds)(a)) { + if (isIntersectionOf(preds)(a)) { assertType>(true); } }); @@ -127,7 +127,7 @@ Deno.test("isAllOf", async (t) => { isObjectOf({ a: isNumber }), isObjectOf({ b: isString }), ] as const; - assertEquals(isAllOf(preds)({ a: 0, b: "a" }), true); + assertEquals(isIntersectionOf(preds)({ a: 0, b: "a" }), true); }); await t.step("returns false on non of T", async (t) => { const preds = [ @@ -135,16 +135,16 @@ Deno.test("isAllOf", async (t) => { isObjectOf({ b: isString }), ] as const; assertEquals( - isAllOf(preds)({ a: 0, b: 0 }), + isIntersectionOf(preds)({ a: 0, b: 0 }), false, "Some properties has wrong type", ); assertEquals( - isAllOf(preds)({ a: 0 }), + isIntersectionOf(preds)({ a: 0 }), false, "Some properties does not exists", ); - await testWithExamples(t, isAllOf(preds), { + await testWithExamples(t, isIntersectionOf(preds), { excludeExamples: ["record"], }); }); From e6d16cf03a224e9449dffa792f5cdcca2714c6e9 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 23:28:48 +0900 Subject: [PATCH 23/31] :+1: Avoid exposing `IsObjectOfMetadata` type --- is/factory.ts | 2 +- is/utility.ts | 9 ++++----- metadata.ts | 2 ++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/is/factory.ts b/is/factory.ts index faace62..901d5f8 100644 --- a/is/factory.ts +++ b/is/factory.ts @@ -573,7 +573,7 @@ type ObjectOf>> = FlatType< } >; -export type IsObjectOfMetadata = { +type IsObjectOfMetadata = { name: "isObjectOf"; args: [Parameters[0]]; }; diff --git a/is/utility.ts b/is/utility.ts index 6be2943..c51d7ef 100644 --- a/is/utility.ts +++ b/is/utility.ts @@ -1,16 +1,15 @@ import type { FlatType, UnionToIntersection } from "../_typeutil.ts"; import type { Predicate } from "./type.ts"; +import { isObjectOf, isOptionalOf } from "./factory.ts"; import { - isObjectOf, - type IsObjectOfMetadata, - isOptionalOf, -} from "./factory.ts"; -import { + type GetMetadata, getPredicateMetadata, setPredicateMetadata, type WithMetadata, } from "../metadata.ts"; +type IsObjectOfMetadata = GetMetadata>; + /** * Return a type predicate function that returns `true` if the type of `x` is `UnionOf`. * diff --git a/metadata.ts b/metadata.ts index 9ed40a1..5f0e516 100644 --- a/metadata.ts +++ b/metadata.ts @@ -41,3 +41,5 @@ export function getPredicateMetadata( ): M { return object[metadataKey]; } + +export type GetMetadata = T extends WithMetadata ? M : never; From 6beb9546381bd66f1c21b37a55a0b23602a04ff9 Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 23:33:15 +0900 Subject: [PATCH 24/31] :+1: Rename and expose members of `metadata.ts` --- is/factory.ts | 38 +++++++++++++++++++------------------- is/utility.ts | 14 +++++++------- metadata.ts | 23 +++++++++++++++++++---- mod.ts | 1 + 4 files changed, 46 insertions(+), 30 deletions(-) diff --git a/is/factory.ts b/is/factory.ts index 901d5f8..1150819 100644 --- a/is/factory.ts +++ b/is/factory.ts @@ -10,8 +10,8 @@ import { type Primitive, } from "./core.ts"; import { - getPredicateMetadata, - setPredicateMetadata, + getPredicateFactoryMetadata, + setPredicateFactoryMetadata, type WithMetadata, } from "../metadata.ts"; @@ -34,7 +34,7 @@ import { export function isArrayOf( pred: Predicate, ): Predicate & WithMetadata { - return setPredicateMetadata( + return setPredicateFactoryMetadata( (x: unknown): x is T[] => isArray(x) && x.every(pred), { name: "isArrayOf", args: [pred] }, ); @@ -64,7 +64,7 @@ type IsArrayOfMetadata = { export function isSetOf( pred: Predicate, ): Predicate> & WithMetadata { - return setPredicateMetadata( + return setPredicateFactoryMetadata( (x: unknown): x is Set => { if (!isSet(x)) return false; for (const v of x.values()) { @@ -152,7 +152,7 @@ export function isTupleOf< & Predicate | [...TupleOf, ...PredicateType]> & WithMetadata { if (!predElse) { - return setPredicateMetadata( + return setPredicateFactoryMetadata( (x: unknown): x is TupleOf => { if (!isArray(x) || x.length !== predTup.length) { return false; @@ -162,7 +162,7 @@ export function isTupleOf< { name: "isTupleOf", args: [predTup] }, ); } else { - return setPredicateMetadata( + return setPredicateFactoryMetadata( (x: unknown): x is [...TupleOf, ...PredicateType] => { if (!isArray(x) || x.length < predTup.length) { return false; @@ -258,12 +258,12 @@ export function isReadonlyTupleOf< > & WithMetadata { if (!predElse) { - return setPredicateMetadata( + return setPredicateFactoryMetadata( isTupleOf(predTup) as Predicate>, { name: "isReadonlyTupleOf", args: [predTup] }, ); } else { - return setPredicateMetadata( + return setPredicateFactoryMetadata( isTupleOf(predTup, predElse) as unknown as Predicate< readonly [...ReadonlyTupleOf, ...PredicateType] >, @@ -317,7 +317,7 @@ export function isUniformTupleOf( n: N, pred: Predicate = isAny, ): Predicate> & WithMetadata { - return setPredicateMetadata( + return setPredicateFactoryMetadata( (x: unknown): x is UniformTupleOf => { if (!isArray(x) || x.length !== n) { return false; @@ -375,7 +375,7 @@ export function isReadonlyUniformTupleOf( ): & Predicate> & WithMetadata { - return setPredicateMetadata( + return setPredicateFactoryMetadata( isUniformTupleOf(n, pred) as Predicate>, { name: "isReadonlyUniformTupleOf", args: [n, pred] }, ); @@ -426,7 +426,7 @@ export function isRecordOf( pred: Predicate, predKey?: Predicate, ): Predicate> & WithMetadata { - return setPredicateMetadata( + return setPredicateFactoryMetadata( (x: unknown): x is Record => { if (!isRecord(x)) return false; for (const k in x) { @@ -477,7 +477,7 @@ export function isMapOf( pred: Predicate, predKey?: Predicate, ): Predicate> & WithMetadata { - return setPredicateMetadata( + return setPredicateFactoryMetadata( (x: unknown): x is Map => { if (!isMap(x)) return false; for (const entry of x.entries()) { @@ -536,7 +536,7 @@ export function isObjectOf< const requiredKeys = Object.entries(predObj) .filter(([_, v]) => !isOptional(v)) .map(([k]) => k); - return setPredicateMetadata( + return setPredicateFactoryMetadata( (x: unknown): x is ObjectOf => { if (!isRecord(x)) return false; // Check required keys @@ -604,7 +604,7 @@ export function isOptionalOf( & WithMetadata; } return Object.defineProperties( - setPredicateMetadata( + setPredicateFactoryMetadata( (x: unknown): x is Predicate => isUndefined(x) || pred(x), { name: "isOptionalOf", args: [pred] }, ), @@ -648,9 +648,9 @@ export function isStrictOf>( ): & Predicate & WithMetadata { - const { args } = getPredicateMetadata(pred); + const { args } = getPredicateFactoryMetadata(pred); const s = new Set(Object.keys(args[0])); - return setPredicateMetadata( + return setPredicateFactoryMetadata( (x: unknown): x is T => { if (!pred(x)) return false; // deno-lint-ignore no-explicit-any @@ -686,7 +686,7 @@ type IsStrictOfMetadata = { export function isInstanceOf unknown>( ctor: T, ): Predicate> & WithMetadata { - return setPredicateMetadata( + return setPredicateFactoryMetadata( (x: unknown): x is InstanceType => x instanceof ctor, { name: "isInstanceOf", args: [ctor] }, ); @@ -716,7 +716,7 @@ type IsInstanceOfMetadata = { export function isLiteralOf( literal: T, ): Predicate & WithMetadata { - return setPredicateMetadata( + return setPredicateFactoryMetadata( (x: unknown): x is T => x === literal, { name: "isLiteralOf", args: [literal] }, ); @@ -747,7 +747,7 @@ export function isLiteralOneOf( literals: T, ): Predicate & WithMetadata { const s = new Set(literals); - return setPredicateMetadata( + return setPredicateFactoryMetadata( (x: unknown): x is T[number] => s.has(x as T[number]), { name: "isLiteralOneOf", args: [literals] }, ); diff --git a/is/utility.ts b/is/utility.ts index c51d7ef..22590e3 100644 --- a/is/utility.ts +++ b/is/utility.ts @@ -3,8 +3,8 @@ import type { Predicate } from "./type.ts"; import { isObjectOf, isOptionalOf } from "./factory.ts"; import { type GetMetadata, - getPredicateMetadata, - setPredicateMetadata, + getPredicateFactoryMetadata, + setPredicateFactoryMetadata, type WithMetadata, } from "../metadata.ts"; @@ -46,7 +46,7 @@ export function isUnionOf< >( preds: T, ): Predicate> & WithMetadata { - return setPredicateMetadata( + return setPredicateFactoryMetadata( (x: unknown): x is UnionOf => preds.some((pred) => pred(x)), { name: "isUnionOf", args: [preds] }, ); @@ -121,7 +121,7 @@ export function isIntersectionOf< ): Predicate> & WithMetadata { const predObj = {}; preds.forEach((pred) => { - Object.assign(predObj, getPredicateMetadata(pred).args[0]); + Object.assign(predObj, getPredicateFactoryMetadata(pred).args[0]); }); return isObjectOf(predObj) as & Predicate> @@ -174,7 +174,7 @@ export function isPartialOf< ): & Predicate>> & WithMetadata { - const { args } = getPredicateMetadata(pred); + const { args } = getPredicateFactoryMetadata(pred); const predObj = Object.fromEntries( Object.entries(args[0]).map(([k, v]) => [k, isOptionalOf(v)]), ); @@ -214,7 +214,7 @@ export function isPickOf< & Predicate>> & WithMetadata { const s = new Set(keys); - const { args } = getPredicateMetadata(pred); + const { args } = getPredicateFactoryMetadata(pred); const predObj = Object.fromEntries( Object.entries(args[0]).filter(([k]) => s.has(k as K)), ); @@ -254,7 +254,7 @@ export function isOmitOf< & Predicate>> & WithMetadata { const s = new Set(keys); - const { args } = getPredicateMetadata(pred); + const { args } = getPredicateFactoryMetadata(pred); const predObj = Object.fromEntries( Object.entries(args[0]).filter(([k]) => !s.has(k as K)), ); diff --git a/metadata.ts b/metadata.ts index 5f0e516..4029e0a 100644 --- a/metadata.ts +++ b/metadata.ts @@ -3,18 +3,27 @@ import { inspect } from "./inspect.ts"; const metadataKey = "__unknownutil_metadata"; +/** + * A type that has metadata. + */ export type WithMetadata = { [metadataKey]: T; }; -export type PredicateMetadata = { +/** + * Metadata of a predicate factory function. + */ +export type PredicateFactoryMetadata = { name: string; args: unknown[]; }; -export function setPredicateMetadata< +/** + * Set metadata to a predicate factory function. + */ +export function setPredicateFactoryMetadata< P extends Predicate, - M extends PredicateMetadata, + M extends PredicateFactoryMetadata, >( pred: P, metadata: M, @@ -36,10 +45,16 @@ export function setPredicateMetadata< }) as P & WithMetadata; } -export function getPredicateMetadata( +/** + * Get metadata from a predicate factory function. + */ +export function getPredicateFactoryMetadata( object: WithMetadata, ): M { return object[metadataKey]; } +/** + * Get metadata type function + */ export type GetMetadata = T extends WithMetadata ? M : never; diff --git a/mod.ts b/mod.ts index 189a1df..fe2f767 100644 --- a/mod.ts +++ b/mod.ts @@ -130,6 +130,7 @@ */ export * from "./is.ts"; +export * from "./metadata.ts"; export * from "./util.ts"; import is from "./is.ts"; From 7a9b8844ee2cffd01b844818e12a73911fefaf4a Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 23:54:20 +0900 Subject: [PATCH 25/31] :+1: Refine `isOptionalOf` in `annotation.ts` Eliminated redundant assignment of optional attributes since metadata is stored in the Predicate factory function. However, isObjectOf and type functions are checked for optional attributes to ensure backward compatibility. --- is.ts | 3 + is/__snapshots__/annotation_test.ts.snap | 5 + is/__snapshots__/factory_test.ts.snap | 134 +++++++++--------- is/__snapshots__/utility_test.ts.snap | 44 +++--- is/annotation.ts | 65 +++++++++ is/annotation_test.ts | 172 +++++++++++++++++++++++ is/factory.ts | 64 ++------- is/factory_test.ts | 97 +------------ is/utility.ts | 3 +- metadata.ts | 23 ++- 10 files changed, 366 insertions(+), 244 deletions(-) create mode 100644 is/__snapshots__/annotation_test.ts.snap create mode 100644 is/annotation.ts create mode 100644 is/annotation_test.ts diff --git a/is.ts b/is.ts index 0c62217..5c6155b 100644 --- a/is.ts +++ b/is.ts @@ -1,13 +1,16 @@ +import annotation from "./is/annotation.ts"; import core from "./is/core.ts"; import factory from "./is/factory.ts"; import utility from "./is/utility.ts"; export type * from "./is/type.ts"; +export * from "./is/annotation.ts"; export * from "./is/core.ts"; export * from "./is/factory.ts"; export * from "./is/utility.ts"; export default { + ...annotation, ...core, ...factory, ...utility, diff --git a/is/__snapshots__/annotation_test.ts.snap b/is/__snapshots__/annotation_test.ts.snap new file mode 100644 index 0000000..f9401a0 --- /dev/null +++ b/is/__snapshots__/annotation_test.ts.snap @@ -0,0 +1,5 @@ +export const snapshot = {}; + +snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; + +snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index d979b23..b6953c6 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,22 +1,8 @@ export const snapshot = {}; -snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; - -snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; - -snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; - -snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; - -snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; - -snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; - -snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; - -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; @@ -24,32 +10,30 @@ snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `" snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; -snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; - -snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; - -snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; - -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; -snapshot[`isStrictOf > returns properly named function 1`] = ` -"isStrictOf(isObjectOf({ - a: isNumber, - b: isString, - c: isBoolean -}))" +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyTupleOf([ + isNumber, + isString, + isBoolean +], isArray)" `; -snapshot[`isStrictOf > returns properly named function 2`] = `"isStrictOf(isObjectOf({a: a}))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isStrictOf > returns properly named function 3`] = ` -"isStrictOf(isObjectOf({ - a: isStrictOf(isObjectOf({ - b: isStrictOf(isObjectOf({c: isBoolean})) - })) -}))" +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyTupleOf([ + isReadonlyTupleOf([ + isReadonlyTupleOf([ + isNumber, + isString, + isBoolean + ], isArray) + ], isArray) +], isArray)" `; snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; @@ -74,35 +58,31 @@ snapshot[`isObjectOf > returns properly named function 3`] = ` })" `; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ - isNumber, - isString, - isBoolean -], isArray)" -`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ - isNumber, - isString, - isBoolean - ], isArray) - ], isArray) -], isArray)" -`; +snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; -snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; +snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; -snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; +snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; +snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; +snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; + +snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; + +snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; + +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; + +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; + +snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; + +snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ @@ -126,10 +106,6 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` ])" `; -snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; - -snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; - snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ isNumber, @@ -154,13 +130,23 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; +snapshot[`isStrictOf > returns properly named function 1`] = ` +"isStrictOf(isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +}))" +`; -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; +snapshot[`isStrictOf > returns properly named function 2`] = `"isStrictOf(isObjectOf({a: a}))"`; -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; +snapshot[`isStrictOf > returns properly named function 3`] = ` +"isStrictOf(isObjectOf({ + a: isStrictOf(isObjectOf({ + b: isStrictOf(isObjectOf({c: isBoolean})) + })) +}))" +`; snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` "isReadonlyTupleOf([ @@ -183,3 +169,13 @@ snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` ]) ])" `; + +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; + +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; + +snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; + +snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; + +snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index ea61c03..7a946ea 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,13 +1,27 @@ export const snapshot = {}; -snapshot[`isPickOf > returns properly named function 1`] = ` +snapshot[`isPartialOf > returns properly named function 1`] = ` "isObjectOf({ - a: isNumber, - c: isBoolean + a: isOptionalOf(isNumber), + b: isOptionalOf(isString), + c: isOptionalOf(isBoolean) })" `; -snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; +snapshot[`isPartialOf > returns properly named function 2`] = ` +"isObjectOf({ + a: isOptionalOf(isNumber), + b: isOptionalOf(isString), + c: isOptionalOf(isBoolean) +})" +`; + +snapshot[`isIntersectionOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString +})" +`; snapshot[`isUnionOf > returns properly named function 1`] = ` "isUnionOf([ @@ -26,25 +40,11 @@ snapshot[`isOmitOf > returns properly named function 1`] = ` snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; -snapshot[`isPartialOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isOptionalOf(isNumber), - b: isOptionalOf(isString), - c: isOptionalOf(isBoolean) -})" -`; - -snapshot[`isPartialOf > returns properly named function 2`] = ` -"isObjectOf({ - a: isOptionalOf(isNumber), - b: isOptionalOf(isString), - c: isOptionalOf(isBoolean) -})" -`; - -snapshot[`isIntersectionOf > returns properly named function 1`] = ` +snapshot[`isPickOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, - b: isString + c: isBoolean })" `; + +snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; diff --git a/is/annotation.ts b/is/annotation.ts new file mode 100644 index 0000000..8c24152 --- /dev/null +++ b/is/annotation.ts @@ -0,0 +1,65 @@ +import type { Predicate } from "./type.ts"; +import { + getMetadata, + type PredicateFactoryMetadata, + setPredicateFactoryMetadata, + type WithMetadata, +} from "../metadata.ts"; + +/** + * Return `true` if the type of predicate function `x` is annotated as `Optional` + */ +export function isOptional

>( + x: P, +): x is P & WithMetadata { + const m = getMetadata(x); + if (m == null) return false; + return (m as PredicateFactoryMetadata).name === "isOptionalOf"; +} + +/** + * Return an `Optional` annotated type predicate function that returns `true` if the type of `x` is `T` or `undefined`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.OptionalOf(is.String); + * const a: unknown = "a"; + * if (isMyType(a)) { + * // a is narrowed to string | undefined + * const _: string | undefined = a; + * } + * ``` + */ +export function isOptionalOf( + pred: Predicate, +): + & Predicate + & WithMetadata { + if (isOptional(pred)) { + return pred as + & Predicate + & WithMetadata; + } + return Object.defineProperties( + setPredicateFactoryMetadata( + (x: unknown): x is Predicate => x === undefined || pred(x), + { name: "isOptionalOf", args: [pred] }, + ), + { optional: { value: true as const } }, + ) as + & Predicate + & WithMetadata; +} + +type IsOptionalOfMetadata = { + name: "isOptionalOf"; + args: Parameters; +}; + +export default { + Optional: isOptional, + OptionalOf: isOptionalOf, +}; diff --git a/is/annotation_test.ts b/is/annotation_test.ts new file mode 100644 index 0000000..390214c --- /dev/null +++ b/is/annotation_test.ts @@ -0,0 +1,172 @@ +import { + assertEquals, + assertStrictEquals, +} from "https://deno.land/std@0.211.0/assert/mod.ts"; +import { + assertSnapshot, +} from "https://deno.land/std@0.211.0/testing/snapshot.ts"; +import { assertType } from "https://deno.land/std@0.211.0/testing/types.ts"; +import { type Equal, stringify } from "./_testutil.ts"; +import { type Predicate } from "./type.ts"; +import { + isArray, + isAsyncFunction, + isBigInt, + isBoolean, + isFunction, + isNull, + isNumber, + isRecord, + isSet, + isString, + isSymbol, + isSyncFunction, + isUndefined, +} from "./core.ts"; +import is, { isOptionalOf } from "./annotation.ts"; + +const examples = { + string: ["", "Hello world"], + number: [0, 1234567890], + bigint: [0n, 1234567890n], + boolean: [true, false], + array: [[], [0, 1, 2], ["a", "b", "c"], [0, "a", true]], + set: [new Set(), new Set([0, 1, 2]), new Set(["a", "b", "c"])], + record: [{}, { a: 0, b: 1, c: 2 }, { a: "a", b: "b", c: "c" }], + map: [ + new Map(), + new Map([["a", 0], ["b", 1], ["c", 2]]), + new Map([["a", "a"], ["b", "b"], ["c", "c"]]), + ], + syncFunction: [function a() {}, () => {}], + asyncFunction: [async function b() {}, async () => {}], + null: [null], + undefined: [undefined], + symbol: [Symbol("a"), Symbol("b"), Symbol("c")], + date: [new Date(1690248225000), new Date(0)], + promise: [new Promise(() => {})], +} as const; + +async function testWithExamples( + t: Deno.TestContext, + pred: Predicate, + opts?: { + validExamples?: (keyof typeof examples)[]; + excludeExamples?: (keyof typeof examples)[]; + }, +): Promise { + const { validExamples = [], excludeExamples = [] } = opts ?? {}; + const exampleEntries = (Object.entries(examples) as unknown as [ + name: keyof typeof examples, + example: unknown[], + ][]).filter(([k]) => !excludeExamples.includes(k)); + for (const [name, example] of exampleEntries) { + const expect = validExamples.includes(name); + for (const v of example) { + await t.step( + `returns ${expect} on ${stringify(v)}`, + () => { + assertEquals(pred(v), expect); + }, + ); + } + } +} + +Deno.test("isOptionalOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isOptionalOf(isNumber).name); + // Nesting does nothing + await assertSnapshot(t, isOptionalOf(isOptionalOf(isNumber)).name); + }); + await t.step("returns proper type predicate", () => { + const a: unknown = undefined; + if (isOptionalOf(isNumber)(a)) { + assertType>(true); + } + }); + await t.step("with isString", async (t) => { + await testWithExamples(t, isOptionalOf(isString), { + validExamples: ["string", "undefined"], + }); + }); + await t.step("with isNumber", async (t) => { + await testWithExamples(t, isOptionalOf(isNumber), { + validExamples: ["number", "undefined"], + }); + }); + await t.step("with isBigInt", async (t) => { + await testWithExamples(t, isOptionalOf(isBigInt), { + validExamples: ["bigint", "undefined"], + }); + }); + await t.step("with isBoolean", async (t) => { + await testWithExamples(t, isOptionalOf(isBoolean), { + validExamples: ["boolean", "undefined"], + }); + }); + await t.step("with isArray", async (t) => { + await testWithExamples(t, isOptionalOf(isArray), { + validExamples: ["array", "undefined"], + }); + }); + await t.step("with isSet", async (t) => { + await testWithExamples(t, isOptionalOf(isSet), { + validExamples: ["set", "undefined"], + }); + }); + await t.step("with isRecord", async (t) => { + await testWithExamples(t, isOptionalOf(isRecord), { + validExamples: ["record", "undefined"], + }); + }); + await t.step("with isFunction", async (t) => { + await testWithExamples(t, isOptionalOf(isFunction), { + validExamples: ["syncFunction", "asyncFunction", "undefined"], + }); + }); + await t.step("with isSyncFunction", async (t) => { + await testWithExamples(t, isOptionalOf(isSyncFunction), { + validExamples: ["syncFunction", "undefined"], + }); + }); + await t.step("with isAsyncFunction", async (t) => { + await testWithExamples(t, isOptionalOf(isAsyncFunction), { + validExamples: ["asyncFunction", "undefined"], + }); + }); + await t.step("with isNull", async (t) => { + await testWithExamples(t, isOptionalOf(isNull), { + validExamples: ["null", "undefined"], + }); + }); + await t.step("with isUndefined", async (t) => { + await testWithExamples(t, isOptionalOf(isUndefined), { + validExamples: ["undefined"], + }); + }); + await t.step("with isSymbol", async (t) => { + await testWithExamples(t, isOptionalOf(isSymbol), { + validExamples: ["symbol", "undefined"], + }); + }); +}); + +Deno.test("is", async (t) => { + const mod = await import("./annotation.ts"); + const casesOfAliasAndIsFunction = Object.entries(mod) + .filter(([k, _]) => k.startsWith("is")) + .map(([k, v]) => [k.slice(2), v] as const); + for (const [alias, fn] of casesOfAliasAndIsFunction) { + await t.step(`defines \`${alias}\` function`, () => { + assertStrictEquals(is[alias as keyof typeof is], fn); + }); + } + await t.step( + "only has entries that are the same as the `is*` function aliases", + () => { + const aliases = casesOfAliasAndIsFunction.map(([a]) => a).sort(); + assertEquals(Object.keys(is).sort(), aliases); + }, + ); +}); diff --git a/is/factory.ts b/is/factory.ts index 1150819..bcbc140 100644 --- a/is/factory.ts +++ b/is/factory.ts @@ -1,15 +1,16 @@ import type { FlatType } from "../_typeutil.ts"; import type { Predicate, PredicateType } from "./type.ts"; +import { isOptional, isOptionalOf } from "./annotation.ts"; import { isAny, isArray, isMap, isRecord, isSet, - isUndefined, type Primitive, } from "./core.ts"; import { + type GetMetadata, getPredicateFactoryMetadata, setPredicateFactoryMetadata, type WithMetadata, @@ -534,7 +535,7 @@ export function isObjectOf< return isStrictOf(isObjectOf(predObj)) as any; } const requiredKeys = Object.entries(predObj) - .filter(([_, v]) => !isOptional(v)) + .filter(([_, v]) => !isWithOptional(v)) .map(([k]) => k); return setPredicateFactoryMetadata( (x: unknown): x is ObjectOf => { @@ -552,23 +553,26 @@ export function isObjectOf< ); } -function isOptional(x: unknown): x is Optional { - return (x as Partial).optional === true; -} +type WithOptional = + | WithMetadata>> + | { optional: true }; // For backward compatibility -type Optional = { - optional: true; -}; +function isWithOptional>( + pred: T, +): pred is T & WithOptional { + // deno-lint-ignore no-explicit-any + return isOptional(pred) || (pred as any).optional === true; +} type ObjectOf>> = FlatType< // Non optional & { - [K in keyof T as T[K] extends Optional ? never : K]: T[K] extends + [K in keyof T as T[K] extends WithOptional ? never : K]: T[K] extends Predicate ? U : never; } // Optional & { - [K in keyof T as T[K] extends Optional ? K : never]?: T[K] extends + [K in keyof T as T[K] extends WithOptional ? K : never]?: T[K] extends Predicate ? U : never; } >; @@ -578,45 +582,6 @@ type IsObjectOfMetadata = { args: [Parameters[0]]; }; -/** - * Return a type predicate function that returns `true` if the type of `x` is `T` or `undefined`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; - * - * const isMyType = is.OptionalOf(is.String); - * const a: unknown = "a"; - * if (isMyType(a)) { - * // a is narrowed to string | undefined - * const _: string | undefined = a; - * } - * ``` - */ -export function isOptionalOf( - pred: Predicate, -): Predicate & Optional & WithMetadata { - if ((pred as Partial).optional) { - return pred as - & Predicate - & Optional - & WithMetadata; - } - return Object.defineProperties( - setPredicateFactoryMetadata( - (x: unknown): x is Predicate => isUndefined(x) || pred(x), - { name: "isOptionalOf", args: [pred] }, - ), - { optional: { value: true as const } }, - ) as Predicate & Optional & WithMetadata; -} - -type IsOptionalOfMetadata = { - name: "isOptionalOf"; - args: Parameters; -}; - /** * Return a type predicate function that returns `true` if the type of `x` is strictly follow the `ObjectOf`. * @@ -765,7 +730,6 @@ export default { LiteralOneOf: isLiteralOneOf, MapOf: isMapOf, ObjectOf: isObjectOf, - OptionalOf: isOptionalOf, ReadonlyTupleOf: isReadonlyTupleOf, ReadonlyUniformTupleOf: isReadonlyUniformTupleOf, RecordOf: isRecordOf, diff --git a/is/factory_test.ts b/is/factory_test.ts index 509c090..7a91a14 100644 --- a/is/factory_test.ts +++ b/is/factory_test.ts @@ -8,21 +8,8 @@ import { import { assertType } from "https://deno.land/std@0.211.0/testing/types.ts"; import { type Equal, stringify } from "./_testutil.ts"; import { type Predicate } from "./type.ts"; -import { - isArray, - isAsyncFunction, - isBigInt, - isBoolean, - isFunction, - isNull, - isNumber, - isRecord, - isSet, - isString, - isSymbol, - isSyncFunction, - isUndefined, -} from "./core.ts"; +import { isOptionalOf } from "./annotation.ts"; +import { isArray, isBoolean, isNumber, isString, isUndefined } from "./core.ts"; import is, { isArrayOf, isInstanceOf, @@ -30,7 +17,6 @@ import is, { isLiteralOneOf, isMapOf, isObjectOf, - isOptionalOf, isReadonlyTupleOf, isReadonlyUniformTupleOf, isRecordOf, @@ -633,85 +619,6 @@ Deno.test("isObjectOf", async (t) => { ); }); -Deno.test("isOptionalOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isOptionalOf(isNumber).name); - // Nesting does nothing - await assertSnapshot(t, isOptionalOf(isOptionalOf(isNumber)).name); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = undefined; - if (isOptionalOf(isNumber)(a)) { - assertType>(true); - } - }); - await t.step("with isString", async (t) => { - await testWithExamples(t, isOptionalOf(isString), { - validExamples: ["string", "undefined"], - }); - }); - await t.step("with isNumber", async (t) => { - await testWithExamples(t, isOptionalOf(isNumber), { - validExamples: ["number", "undefined"], - }); - }); - await t.step("with isBigInt", async (t) => { - await testWithExamples(t, isOptionalOf(isBigInt), { - validExamples: ["bigint", "undefined"], - }); - }); - await t.step("with isBoolean", async (t) => { - await testWithExamples(t, isOptionalOf(isBoolean), { - validExamples: ["boolean", "undefined"], - }); - }); - await t.step("with isArray", async (t) => { - await testWithExamples(t, isOptionalOf(isArray), { - validExamples: ["array", "undefined"], - }); - }); - await t.step("with isSet", async (t) => { - await testWithExamples(t, isOptionalOf(isSet), { - validExamples: ["set", "undefined"], - }); - }); - await t.step("with isRecord", async (t) => { - await testWithExamples(t, isOptionalOf(isRecord), { - validExamples: ["record", "undefined"], - }); - }); - await t.step("with isFunction", async (t) => { - await testWithExamples(t, isOptionalOf(isFunction), { - validExamples: ["syncFunction", "asyncFunction", "undefined"], - }); - }); - await t.step("with isSyncFunction", async (t) => { - await testWithExamples(t, isOptionalOf(isSyncFunction), { - validExamples: ["syncFunction", "undefined"], - }); - }); - await t.step("with isAsyncFunction", async (t) => { - await testWithExamples(t, isOptionalOf(isAsyncFunction), { - validExamples: ["asyncFunction", "undefined"], - }); - }); - await t.step("with isNull", async (t) => { - await testWithExamples(t, isOptionalOf(isNull), { - validExamples: ["null", "undefined"], - }); - }); - await t.step("with isUndefined", async (t) => { - await testWithExamples(t, isOptionalOf(isUndefined), { - validExamples: ["undefined"], - }); - }); - await t.step("with isSymbol", async (t) => { - await testWithExamples(t, isOptionalOf(isSymbol), { - validExamples: ["symbol", "undefined"], - }); - }); -}); - Deno.test("isStrictOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot( diff --git a/is/utility.ts b/is/utility.ts index 22590e3..f8f6537 100644 --- a/is/utility.ts +++ b/is/utility.ts @@ -1,6 +1,7 @@ import type { FlatType, UnionToIntersection } from "../_typeutil.ts"; import type { Predicate } from "./type.ts"; -import { isObjectOf, isOptionalOf } from "./factory.ts"; +import { isOptionalOf } from "./annotation.ts"; +import { isObjectOf } from "./factory.ts"; import { type GetMetadata, getPredicateFactoryMetadata, diff --git a/metadata.ts b/metadata.ts index 4029e0a..1e12571 100644 --- a/metadata.ts +++ b/metadata.ts @@ -10,6 +10,20 @@ export type WithMetadata = { [metadataKey]: T; }; +/** + * Get typeof the metadata + */ +export type GetMetadata = T extends WithMetadata ? M : never; + +/** + * Get metadata from the given value + */ +export function getMetadata(v: unknown): T | undefined { + if (v == null) return undefined; + // deno-lint-ignore no-explicit-any + return (v as any)[metadataKey]; +} + /** * Metadata of a predicate factory function. */ @@ -49,12 +63,7 @@ export function setPredicateFactoryMetadata< * Get metadata from a predicate factory function. */ export function getPredicateFactoryMetadata( - object: WithMetadata, + v: WithMetadata, ): M { - return object[metadataKey]; + return getMetadata(v) as M; } - -/** - * Get metadata type function - */ -export type GetMetadata = T extends WithMetadata ? M : never; From cef9f087370f5955e42758efb2041b5519d85149 Mon Sep 17 00:00:00 2001 From: Alisue Date: Tue, 13 Feb 2024 01:36:45 +0900 Subject: [PATCH 26/31] :+1: Add `isReadonlyOf` in `annotation.ts` --- is/__snapshots__/annotation_test.ts.snap | 4 + is/__snapshots__/factory_test.ts.snap | 172 +++++++++++------------ is/__snapshots__/utility_test.ts.snap | 24 ++-- is/annotation.ts | 49 +++++++ is/annotation_test.ts | 30 +++- 5 files changed, 180 insertions(+), 99 deletions(-) diff --git a/is/__snapshots__/annotation_test.ts.snap b/is/__snapshots__/annotation_test.ts.snap index f9401a0..8b9b635 100644 --- a/is/__snapshots__/annotation_test.ts.snap +++ b/is/__snapshots__/annotation_test.ts.snap @@ -3,3 +3,7 @@ export const snapshot = {}; snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; + +snapshot[`isReadonlyOf > returns properly named function 1`] = `"isReadonlyOf(isNumber)"`; + +snapshot[`isReadonlyOf > returns properly named function 2`] = `"isReadonlyOf(isReadonlyOf(isNumber))"`; diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index b6953c6..d8543e0 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,18 +1,32 @@ export const snapshot = {}; -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; - -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ + isNumber, + isString, + isBoolean +], isArray)" +`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ + isNumber, + isString, + isBoolean + ], isArray) + ], isArray) +])" +`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` "isReadonlyTupleOf([ @@ -36,31 +50,13 @@ snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` ], isArray)" `; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; - -snapshot[`isObjectOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - b: isString, - c: isBoolean -})" -`; - -snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; -snapshot[`isObjectOf > returns properly named function 3`] = ` -"isObjectOf({ - a: isObjectOf({ - b: isObjectOf({c: isBoolean}) - }) -})" -`; +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; @@ -76,59 +72,11 @@ snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(u snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; - -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; - -snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; - -snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; - -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ - isNumber, - isString, - isBoolean -])" -`; - -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; - -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ - isNumber, - isString, - isBoolean - ]) - ]) -])" -`; - -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ - isNumber, - isString, - isBoolean -], isArray)" -`; - -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ - isNumber, - isString, - isBoolean - ], isArray) - ], isArray) -])" -`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; snapshot[`isStrictOf > returns properly named function 1`] = ` "isStrictOf(isObjectOf({ @@ -148,6 +96,16 @@ snapshot[`isStrictOf > returns properly named function 3`] = ` }))" `; +snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; + +snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; + +snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; + +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; + +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; + snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` "isReadonlyTupleOf([ isNumber, @@ -170,12 +128,54 @@ snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` ])" `; -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; -snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ + isNumber, + isString, + isBoolean +])" +`; -snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; -snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ + isNumber, + isString, + isBoolean + ]) + ]) +])" +`; + +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; + +snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; + +snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; + +snapshot[`isObjectOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +})" +`; + +snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; + +snapshot[`isObjectOf > returns properly named function 3`] = ` +"isObjectOf({ + a: isObjectOf({ + b: isObjectOf({c: isBoolean}) + }) +})" +`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index 7a946ea..4329ef4 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,5 +1,13 @@ export const snapshot = {}; +snapshot[`isUnionOf > returns properly named function 1`] = ` +"isUnionOf([ + isNumber, + isString, + isBoolean +])" +`; + snapshot[`isPartialOf > returns properly named function 1`] = ` "isObjectOf({ a: isOptionalOf(isNumber), @@ -23,28 +31,20 @@ snapshot[`isIntersectionOf > returns properly named function 1`] = ` })" `; -snapshot[`isUnionOf > returns properly named function 1`] = ` -"isUnionOf([ - isNumber, - isString, - isBoolean -])" -`; - -snapshot[`isOmitOf > returns properly named function 1`] = ` +snapshot[`isPickOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, c: isBoolean })" `; -snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; +snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; -snapshot[`isPickOf > returns properly named function 1`] = ` +snapshot[`isOmitOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, c: isBoolean })" `; -snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; +snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; diff --git a/is/annotation.ts b/is/annotation.ts index 8c24152..fb52324 100644 --- a/is/annotation.ts +++ b/is/annotation.ts @@ -59,7 +59,56 @@ type IsOptionalOfMetadata = { args: Parameters; }; +/** + * Return `true` if the type of predicate function `x` is annotated as `Readonly` + */ +export function isReadonly

>( + x: P, +): x is P & WithMetadata { + const m = getMetadata(x); + if (m == null) return false; + return (m as PredicateFactoryMetadata).name === "isReadonlyOf"; +} + +/** + * Return an `Readonly` annotated type predicate function that returns `true` if the type of `x` is `T`. + * + * Note that this function does nothing but annotate the predicate function as `Readonly`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.ReadonlyOf(is.TupleOf([is.String, is.Number])); + * const a: unknown = ["a", 1]; + * if (isMyType(a)) { + * // a is narrowed to readonly [string, number] + * const _: readonly [string, number] = a; + * } + * ``` + */ +export function isReadonlyOf( + pred: Predicate, +): + & Predicate> + & WithMetadata { + return setPredicateFactoryMetadata( + (x: unknown): x is Readonly => pred(x), + { name: "isReadonlyOf", args: [pred] }, + ) as + & Predicate> + & WithMetadata; +} + +type IsReadonlyOfMetadata = { + name: "isReadonlyOf"; + args: Parameters; +}; + export default { Optional: isOptional, OptionalOf: isOptionalOf, + Readonly: isReadonly, + ReadonlyOf: isReadonlyOf, }; diff --git a/is/annotation_test.ts b/is/annotation_test.ts index 390214c..9561de3 100644 --- a/is/annotation_test.ts +++ b/is/annotation_test.ts @@ -23,7 +23,8 @@ import { isSyncFunction, isUndefined, } from "./core.ts"; -import is, { isOptionalOf } from "./annotation.ts"; +import { isObjectOf, isTupleOf, isUniformTupleOf } from "./factory.ts"; +import is, { isOptionalOf, isReadonlyOf } from "./annotation.ts"; const examples = { string: ["", "Hello world"], @@ -152,6 +153,33 @@ Deno.test("isOptionalOf", async (t) => { }); }); +Deno.test("isReadonlyOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isReadonlyOf(isNumber).name); + // Nesting does nothing + await assertSnapshot(t, isReadonlyOf(isReadonlyOf(isNumber)).name); + }); + await t.step("returns proper type predicate", () => { + const a: unknown = undefined; + if (isReadonlyOf(isNumber)(a)) { + assertType>>(true); + } + if (isReadonlyOf(isTupleOf([isString, isNumber, isBoolean]))(a)) { + assertType>>(true); + } + if (isReadonlyOf(isUniformTupleOf(3, isString))(a)) { + assertType>>(true); + } + if ( + isReadonlyOf(isObjectOf({ a: isString, b: isNumber, c: isBoolean }))(a) + ) { + assertType< + Equal> + >(true); + } + }); +}); + Deno.test("is", async (t) => { const mod = await import("./annotation.ts"); const casesOfAliasAndIsFunction = Object.entries(mod) From 3abe6f97c6224953b7dc7b80b7cdd0395905d074 Mon Sep 17 00:00:00 2001 From: Alisue Date: Tue, 13 Feb 2024 01:44:38 +0900 Subject: [PATCH 27/31] :muscle: Use `isReadonlyOf` in `isReadonlyTupleOf/isReadonlyUniformTupleOf` --- is/__snapshots__/annotation_test.ts.snap | 8 +- is/__snapshots__/factory_test.ts.snap | 186 +++++++++++------------ is/__snapshots__/utility_test.ts.snap | 26 ++-- is/factory.ts | 73 +++------ 4 files changed, 135 insertions(+), 158 deletions(-) diff --git a/is/__snapshots__/annotation_test.ts.snap b/is/__snapshots__/annotation_test.ts.snap index 8b9b635..e99df56 100644 --- a/is/__snapshots__/annotation_test.ts.snap +++ b/is/__snapshots__/annotation_test.ts.snap @@ -1,9 +1,9 @@ export const snapshot = {}; -snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; - -snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; - snapshot[`isReadonlyOf > returns properly named function 1`] = `"isReadonlyOf(isNumber)"`; snapshot[`isReadonlyOf > returns properly named function 2`] = `"isReadonlyOf(isReadonlyOf(isNumber))"`; + +snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; + +snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index d8543e0..0f5708b 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,137 +1,143 @@ export const snapshot = {}; -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyOf(isTupleOf([ isNumber, isString, isBoolean -], isArray)" +]))" `; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)]))"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ isNumber, isString, isBoolean - ], isArray) - ], isArray) -])" + ])) + ])) +]))" `; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; +snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; +snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; +snapshot[`isStrictOf > returns properly named function 1`] = ` +"isStrictOf(isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +}))" +`; + +snapshot[`isStrictOf > returns properly named function 2`] = `"isStrictOf(isObjectOf({a: a}))"`; + +snapshot[`isStrictOf > returns properly named function 3`] = ` +"isStrictOf(isObjectOf({ + a: isStrictOf(isObjectOf({ + b: isStrictOf(isObjectOf({c: isBoolean})) + })) +}))" +`; + +snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; + +snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; + +snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; + +snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; + +snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; + +snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; + +snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ +"isReadonlyOf(isTupleOf([ isNumber, isString, isBoolean -], isArray)" +], isArray))" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)], isArrayOf(isString))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)], isArrayOf(isString)))"`; snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ +"isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ isNumber, isString, isBoolean - ], isArray) - ], isArray) -], isArray)" + ], isArray)) + ], isArray)) +], isArray))" `; -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; - -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; - snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; -snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; - -snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; - -snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; - -snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; - -snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; - -snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; -snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyUniformTupleOf(3, isAny)"`; +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; -snapshot[`isStrictOf > returns properly named function 1`] = ` -"isStrictOf(isObjectOf({ +snapshot[`isObjectOf > returns properly named function 1`] = ` +"isObjectOf({ a: isNumber, b: isString, c: isBoolean -}))" +})" `; -snapshot[`isStrictOf > returns properly named function 2`] = `"isStrictOf(isObjectOf({a: a}))"`; +snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; -snapshot[`isStrictOf > returns properly named function 3`] = ` -"isStrictOf(isObjectOf({ - a: isStrictOf(isObjectOf({ - b: isStrictOf(isObjectOf({c: isBoolean})) - })) -}))" +snapshot[`isObjectOf > returns properly named function 3`] = ` +"isObjectOf({ + a: isObjectOf({ + b: isObjectOf({c: isBoolean}) + }) +})" `; -snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; - -snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; - -snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; - -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ isNumber, isString, isBoolean -])" +], isArray)" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ isNumber, isString, isBoolean - ]) - ]) + ], isArray) + ], isArray) ])" `; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; - -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; - snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ isNumber, @@ -154,28 +160,22 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` ])" `; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; +snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; +snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; -snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; +snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; -snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; -snapshot[`isObjectOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - b: isString, - c: isBoolean -})" -`; +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; -snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyOf(isUniformTupleOf(3, isAny))"`; -snapshot[`isObjectOf > returns properly named function 3`] = ` -"isObjectOf({ - a: isObjectOf({ - b: isObjectOf({c: isBoolean}) - }) -})" -`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyOf(isUniformTupleOf(3, isNumber))"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyOf(isUniformTupleOf(3, (anonymous)))"`; + +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; + +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index 4329ef4..b23c4b0 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,13 +1,5 @@ export const snapshot = {}; -snapshot[`isUnionOf > returns properly named function 1`] = ` -"isUnionOf([ - isNumber, - isString, - isBoolean -])" -`; - snapshot[`isPartialOf > returns properly named function 1`] = ` "isObjectOf({ a: isOptionalOf(isNumber), @@ -24,13 +16,15 @@ snapshot[`isPartialOf > returns properly named function 2`] = ` })" `; -snapshot[`isIntersectionOf > returns properly named function 1`] = ` +snapshot[`isOmitOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, - b: isString + c: isBoolean })" `; +snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; + snapshot[`isPickOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, @@ -40,11 +34,17 @@ snapshot[`isPickOf > returns properly named function 1`] = ` snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; -snapshot[`isOmitOf > returns properly named function 1`] = ` +snapshot[`isIntersectionOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, - c: isBoolean + b: isString })" `; -snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; +snapshot[`isUnionOf > returns properly named function 1`] = ` +"isUnionOf([ + isNumber, + isString, + isBoolean +])" +`; diff --git a/is/factory.ts b/is/factory.ts index bcbc140..052728c 100644 --- a/is/factory.ts +++ b/is/factory.ts @@ -1,6 +1,11 @@ import type { FlatType } from "../_typeutil.ts"; import type { Predicate, PredicateType } from "./type.ts"; -import { isOptional, isOptionalOf } from "./annotation.ts"; +import { + isOptional, + isOptionalOf, + isReadonly, + isReadonlyOf, +} from "./annotation.ts"; import { isAny, isArray, @@ -16,6 +21,8 @@ import { type WithMetadata, } from "../metadata.ts"; +type IsReadonlyOfMetadata = GetMetadata>; + /** * Return a type predicate function that returns `true` if the type of `x` is `T[]`. * @@ -187,7 +194,9 @@ type IsTupleOfMetadata = { }; /** - * Return a type predicate function that returns `true` if the type of `x` is `ReadonlyTupleOf`. + * Return a type predicate function that returns `true` if the type of `x` is `Readonly>`. + * + * @deprecated Use `is.ReadonlyOf(is.TupleOf(...))` instead. * * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. * @@ -237,7 +246,7 @@ export function isReadonlyTupleOf< T extends readonly [Predicate, ...Predicate[]], >( predTup: T, -): Predicate> & WithMetadata; +): Predicate>> & WithMetadata; export function isReadonlyTupleOf< T extends readonly [Predicate, ...Predicate[]], E extends Predicate, @@ -245,8 +254,8 @@ export function isReadonlyTupleOf< predTup: T, predElse: E, ): - & Predicate, ...PredicateType]> - & WithMetadata; + & Predicate, ...PredicateType]>> + & WithMetadata; export function isReadonlyTupleOf< T extends readonly [Predicate, ...Predicate[]], E extends Predicate, @@ -255,36 +264,17 @@ export function isReadonlyTupleOf< predElse?: E, ): & Predicate< - ReadonlyTupleOf | readonly [...ReadonlyTupleOf, ...PredicateType] + | Readonly> + | Readonly<[...TupleOf, ...PredicateType]> > - & WithMetadata { + & WithMetadata { if (!predElse) { - return setPredicateFactoryMetadata( - isTupleOf(predTup) as Predicate>, - { name: "isReadonlyTupleOf", args: [predTup] }, - ); + return isReadonlyOf(isTupleOf(predTup)); } else { - return setPredicateFactoryMetadata( - isTupleOf(predTup, predElse) as unknown as Predicate< - readonly [...ReadonlyTupleOf, ...PredicateType] - >, - { name: "isReadonlyTupleOf", args: [predTup, predElse] }, - ); + return isReadonlyOf(isTupleOf(predTup, predElse)); } } -type ReadonlyTupleOf = { - [P in keyof T]: T[P] extends Predicate ? U : never; -}; - -type IsReadonlyTupleOfMetadata = { - name: "isReadonlyTupleOf"; - args: [ - Parameters[0], - Parameters[1]?, - ]; -}; - /** * Return a type predicate function that returns `true` if the type of `x` is `UniformTupleOf`. * @@ -342,7 +332,9 @@ type IsUniformTupleOfMetadata = { }; /** - * Return a type predicate function that returns `true` if the type of `x` is `ReadonlyUniformTupleOf`. + * Return a type predicate function that returns `true` if the type of `x` is `Readonly>`. + * + * @deprecated Use `is.ReadonlyOf(is.UniformTupleOf(...))` instead. * * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. * @@ -374,26 +366,11 @@ export function isReadonlyUniformTupleOf( n: N, pred: Predicate = isAny, ): - & Predicate> - & WithMetadata { - return setPredicateFactoryMetadata( - isUniformTupleOf(n, pred) as Predicate>, - { name: "isReadonlyUniformTupleOf", args: [n, pred] }, - ); + & Predicate>> + & WithMetadata { + return isReadonlyOf(isUniformTupleOf(n, pred)); } -type ReadonlyUniformTupleOf< - T, - N extends number, - R extends readonly T[] = [], -> = R["length"] extends N ? R - : ReadonlyUniformTupleOf; - -type IsReadonlyUniformTupleOfMetadata = { - name: "isReadonlyUniformTupleOf"; - args: Parameters; -}; - /** * Return a type predicate function that returns `true` if the type of `x` is `Record`. * From eb4d1612c01701d40ded496b1817c1a54862505a Mon Sep 17 00:00:00 2001 From: Alisue Date: Tue, 13 Feb 2024 02:03:48 +0900 Subject: [PATCH 28/31] :+1: Add `isUnwrapOptionalOf` and `isUnwrapReadonlyOf` --- _typeutil.ts | 2 + is/__snapshots__/annotation_test.ts.snap | 10 ++ is/__snapshots__/factory_test.ts.snap | 194 +++++++++++------------ is/__snapshots__/utility_test.ts.snap | 30 ++-- is/annotation.ts | 64 ++++++++ is/annotation_test.ts | 138 +++++++++++++++- 6 files changed, 325 insertions(+), 113 deletions(-) diff --git a/_typeutil.ts b/_typeutil.ts index 1728384..4ef080f 100644 --- a/_typeutil.ts +++ b/_typeutil.ts @@ -6,3 +6,5 @@ export type UnionToIntersection = (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; + +export type Writable = { -readonly [P in keyof T]: T[P] }; diff --git a/is/__snapshots__/annotation_test.ts.snap b/is/__snapshots__/annotation_test.ts.snap index e99df56..8cb7799 100644 --- a/is/__snapshots__/annotation_test.ts.snap +++ b/is/__snapshots__/annotation_test.ts.snap @@ -1,5 +1,11 @@ export const snapshot = {}; +snapshot[`isUnwrapOptionalOf > returns properly named function 1`] = `"isNumber"`; + +snapshot[`isUnwrapOptionalOf > returns properly named function 2`] = `"isNumber"`; + +snapshot[`isUnwrapOptionalOf > returns properly named function 3`] = `"isNumber"`; + snapshot[`isReadonlyOf > returns properly named function 1`] = `"isReadonlyOf(isNumber)"`; snapshot[`isReadonlyOf > returns properly named function 2`] = `"isReadonlyOf(isReadonlyOf(isNumber))"`; @@ -7,3 +13,7 @@ snapshot[`isReadonlyOf > returns properly named function 2`] = `"isReadonlyOf snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; + +snapshot[`isUnwrapReadonlyOf > returns properly named function 1`] = `"isNumber"`; + +snapshot[`isUnwrapReadonlyOf > returns properly named function 2`] = `"isReadonlyOf(isNumber)"`; diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index 0f5708b..95e79c5 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,98 +1,18 @@ export const snapshot = {}; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyOf(isTupleOf([ - isNumber, - isString, - isBoolean -]))" -`; - -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)]))"`; - -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyOf(isTupleOf([ - isReadonlyOf(isTupleOf([ - isReadonlyOf(isTupleOf([ - isNumber, - isString, - isBoolean - ])) - ])) -]))" -`; - -snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; - -snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; - -snapshot[`isStrictOf > returns properly named function 1`] = ` -"isStrictOf(isObjectOf({ - a: isNumber, - b: isString, - c: isBoolean -}))" -`; - -snapshot[`isStrictOf > returns properly named function 2`] = `"isStrictOf(isObjectOf({a: a}))"`; - -snapshot[`isStrictOf > returns properly named function 3`] = ` -"isStrictOf(isObjectOf({ - a: isStrictOf(isObjectOf({ - b: isStrictOf(isObjectOf({c: isBoolean})) - })) -}))" -`; - -snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; - -snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; - -snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; - -snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; - -snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; - -snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; - -snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; - -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyOf(isTupleOf([ - isNumber, - isString, - isBoolean -], isArray))" -`; - -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)], isArrayOf(isString)))"`; - -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyOf(isTupleOf([ - isReadonlyOf(isTupleOf([ - isReadonlyOf(isTupleOf([ - isNumber, - isString, - isBoolean - ], isArray)) - ], isArray)) -], isArray))" -`; - snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; +snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; +snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; +snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; snapshot[`isObjectOf > returns properly named function 1`] = ` "isObjectOf({ @@ -112,10 +32,6 @@ snapshot[`isObjectOf > returns properly named function 3`] = ` })" `; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; - snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ isNumber, @@ -138,6 +54,34 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` ])" `; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyOf(isUniformTupleOf(3, isAny))"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyOf(isUniformTupleOf(3, isNumber))"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyOf(isUniformTupleOf(3, (anonymous)))"`; + +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyOf(isTupleOf([ + isNumber, + isString, + isBoolean +], isArray))" +`; + +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)], isArrayOf(isString)))"`; + +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ + isNumber, + isString, + isBoolean + ], isArray)) + ], isArray)) +], isArray))" +`; + snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ isNumber, @@ -160,22 +104,78 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` ])" `; -snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; -snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; -snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; +snapshot[`isStrictOf > returns properly named function 1`] = ` +"isStrictOf(isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +}))" +`; + +snapshot[`isStrictOf > returns properly named function 2`] = `"isStrictOf(isObjectOf({a: a}))"`; + +snapshot[`isStrictOf > returns properly named function 3`] = ` +"isStrictOf(isObjectOf({ + a: isStrictOf(isObjectOf({ + b: isStrictOf(isObjectOf({c: isBoolean})) + })) +}))" +`; + +snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; + +snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; + +snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; + +snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; + +snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; + +snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; + +snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; + +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; + +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyOf(isUniformTupleOf(3, isAny))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyOf(isTupleOf([ + isNumber, + isString, + isBoolean +]))" +`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyOf(isUniformTupleOf(3, isNumber))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)]))"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyOf(isUniformTupleOf(3, (anonymous)))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ + isNumber, + isString, + isBoolean + ])) + ])) +]))" +`; -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; +snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; +snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; + +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; + +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index b23c4b0..fe35c36 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,5 +1,12 @@ export const snapshot = {}; +snapshot[`isIntersectionOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString +})" +`; + snapshot[`isPartialOf > returns properly named function 1`] = ` "isObjectOf({ a: isOptionalOf(isNumber), @@ -16,6 +23,14 @@ snapshot[`isPartialOf > returns properly named function 2`] = ` })" `; +snapshot[`isUnionOf > returns properly named function 1`] = ` +"isUnionOf([ + isNumber, + isString, + isBoolean +])" +`; + snapshot[`isOmitOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, @@ -33,18 +48,3 @@ snapshot[`isPickOf > returns properly named function 1`] = ` `; snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; - -snapshot[`isIntersectionOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - b: isString -})" -`; - -snapshot[`isUnionOf > returns properly named function 1`] = ` -"isUnionOf([ - isNumber, - isString, - isBoolean -])" -`; diff --git a/is/annotation.ts b/is/annotation.ts index fb52324..6062847 100644 --- a/is/annotation.ts +++ b/is/annotation.ts @@ -1,6 +1,8 @@ import type { Predicate } from "./type.ts"; +import type { Writable } from "../_typeutil.ts"; import { getMetadata, + getPredicateFactoryMetadata, type PredicateFactoryMetadata, setPredicateFactoryMetadata, type WithMetadata, @@ -59,6 +61,36 @@ type IsOptionalOfMetadata = { args: Parameters; }; +/** + * Return an `Optional` un-annotated type predicate function that returns `true` if the type of `x` is `T`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.UnwrapOptionalOf(is.OptionalOf(is.String)); + * const a: unknown = "a"; + * if (isMyType(a)) { + * // a is narrowed to string + * const _: string = a; + * } + * ``` + */ +export function isUnwrapOptionalOf

>( + pred: P, +): UnwrapOptionalOf

{ + if (!isOptional(pred)) return pred as UnwrapOptionalOf

; + const { args } = getPredicateFactoryMetadata(pred); + return args[0] as UnwrapOptionalOf

; +} + +type UnwrapOptionalOf = T extends + Predicate & WithMetadata + ? Predicate + : T extends Predicate ? T + : never; + /** * Return `true` if the type of predicate function `x` is annotated as `Readonly` */ @@ -106,9 +138,41 @@ type IsReadonlyOfMetadata = { args: Parameters; }; +/** + * Return an `Readonly` un-annotated type predicate function that returns `true` if the type of `x` is `T`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.UnwrapReadonlyOf(is.ReadonlyOf(is.TupleOf([is.String, is.Number]))); + * const a: unknown = ["a", 1]; + * if (isMyType(a)) { + * // a is narrowed to [string, number] + * const _: [string, number] = a; + * } + * ``` + */ +export function isUnwrapReadonlyOf

>( + pred: P, +): UnwrapReadonlyOf

{ + if (!isReadonly(pred)) return pred as UnwrapReadonlyOf

; + const { args } = getPredicateFactoryMetadata(pred); + return args[0] as UnwrapReadonlyOf

; +} + +type UnwrapReadonlyOf = T extends + Predicate & WithMetadata + ? Predicate> + : T extends Predicate ? T + : never; + export default { Optional: isOptional, OptionalOf: isOptionalOf, Readonly: isReadonly, ReadonlyOf: isReadonlyOf, + UnwrapOptionalOf: isUnwrapOptionalOf, + UnwrapReadonlyOf: isUnwrapReadonlyOf, }; diff --git a/is/annotation_test.ts b/is/annotation_test.ts index 9561de3..de68d76 100644 --- a/is/annotation_test.ts +++ b/is/annotation_test.ts @@ -24,7 +24,12 @@ import { isUndefined, } from "./core.ts"; import { isObjectOf, isTupleOf, isUniformTupleOf } from "./factory.ts"; -import is, { isOptionalOf, isReadonlyOf } from "./annotation.ts"; +import is, { + isOptionalOf, + isReadonlyOf, + isUnwrapOptionalOf, + isUnwrapReadonlyOf, +} from "./annotation.ts"; const examples = { string: ["", "Hello world"], @@ -153,6 +158,101 @@ Deno.test("isOptionalOf", async (t) => { }); }); +Deno.test("isUnwrapOptionalOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isUnwrapOptionalOf(isOptionalOf(isNumber)).name); + // Non optional does nothing + await assertSnapshot(t, isUnwrapOptionalOf(isNumber).name); + // Nesting does nothing + await assertSnapshot( + t, + isUnwrapOptionalOf(isUnwrapOptionalOf(isOptionalOf(isNumber))).name, + ); + }); + await t.step("returns proper type predicate", () => { + const a: unknown = undefined; + if (isUnwrapOptionalOf(isOptionalOf(isNumber))(a)) { + assertType>(true); + } + if (isUnwrapOptionalOf(isNumber)(a)) { + assertType>(true); + } + }); + await t.step("with isString", async (t) => { + await testWithExamples(t, isUnwrapOptionalOf(isOptionalOf(isString)), { + validExamples: ["string"], + }); + }); + await t.step("with isNumber", async (t) => { + await testWithExamples(t, isUnwrapOptionalOf(isOptionalOf(isNumber)), { + validExamples: ["number"], + }); + }); + await t.step("with isBigInt", async (t) => { + await testWithExamples(t, isUnwrapOptionalOf(isOptionalOf(isBigInt)), { + validExamples: ["bigint"], + }); + }); + await t.step("with isBoolean", async (t) => { + await testWithExamples(t, isUnwrapOptionalOf(isOptionalOf(isBoolean)), { + validExamples: ["boolean"], + }); + }); + await t.step("with isArray", async (t) => { + await testWithExamples(t, isUnwrapOptionalOf(isOptionalOf(isArray)), { + validExamples: ["array"], + }); + }); + await t.step("with isSet", async (t) => { + await testWithExamples(t, isUnwrapOptionalOf(isOptionalOf(isSet)), { + validExamples: ["set"], + }); + }); + await t.step("with isRecord", async (t) => { + await testWithExamples(t, isUnwrapOptionalOf(isOptionalOf(isRecord)), { + validExamples: ["record"], + }); + }); + await t.step("with isFunction", async (t) => { + await testWithExamples(t, isUnwrapOptionalOf(isOptionalOf(isFunction)), { + validExamples: ["syncFunction", "asyncFunction"], + }); + }); + await t.step("with isSyncFunction", async (t) => { + await testWithExamples( + t, + isUnwrapOptionalOf(isOptionalOf(isSyncFunction)), + { + validExamples: ["syncFunction"], + }, + ); + }); + await t.step("with isAsyncFunction", async (t) => { + await testWithExamples( + t, + isUnwrapOptionalOf(isOptionalOf(isAsyncFunction)), + { + validExamples: ["asyncFunction"], + }, + ); + }); + await t.step("with isNull", async (t) => { + await testWithExamples(t, isUnwrapOptionalOf(isOptionalOf(isNull)), { + validExamples: ["null"], + }); + }); + await t.step("with isUndefined", async (t) => { + await testWithExamples(t, isUnwrapOptionalOf(isOptionalOf(isUndefined)), { + validExamples: ["undefined"], + }); + }); + await t.step("with isSymbol", async (t) => { + await testWithExamples(t, isUnwrapOptionalOf(isOptionalOf(isSymbol)), { + validExamples: ["symbol"], + }); + }); +}); + Deno.test("isReadonlyOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot(t, isReadonlyOf(isNumber).name); @@ -180,6 +280,42 @@ Deno.test("isReadonlyOf", async (t) => { }); }); +Deno.test("isUnwrapReadonlyOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isUnwrapReadonlyOf(isReadonlyOf(isNumber)).name); + // Nesting does nothing + await assertSnapshot( + t, + isUnwrapReadonlyOf(isReadonlyOf(isReadonlyOf(isNumber))).name, + ); + }); + await t.step("returns proper type predicate", () => { + const a: unknown = undefined; + if (isUnwrapReadonlyOf(isReadonlyOf(isNumber))(a)) { + assertType>(true); + } + if ( + isUnwrapReadonlyOf( + isReadonlyOf(isTupleOf([isString, isNumber, isBoolean])), + )(a) + ) { + assertType>(true); + } + if (isUnwrapReadonlyOf(isReadonlyOf(isUniformTupleOf(3, isString)))(a)) { + assertType>(true); + } + if ( + isUnwrapReadonlyOf( + isReadonlyOf(isObjectOf({ a: isString, b: isNumber, c: isBoolean })), + )(a) + ) { + assertType< + Equal + >(true); + } + }); +}); + Deno.test("is", async (t) => { const mod = await import("./annotation.ts"); const casesOfAliasAndIsFunction = Object.entries(mod) From 0d24ddde905ef55e067b139c8a0a2b808f32183f Mon Sep 17 00:00:00 2001 From: Alisue Date: Tue, 13 Feb 2024 02:11:53 +0900 Subject: [PATCH 29/31] :+1: Add `isRequiredOf` --- is/__snapshots__/annotation_test.ts.snap | 8 +- is/__snapshots__/factory_test.ts.snap | 164 +++++++++++------------ is/__snapshots__/utility_test.ts.snap | 62 ++++++--- is/utility.ts | 41 +++++- is/utility_test.ts | 49 ++++++- 5 files changed, 216 insertions(+), 108 deletions(-) diff --git a/is/__snapshots__/annotation_test.ts.snap b/is/__snapshots__/annotation_test.ts.snap index 8cb7799..1af18e5 100644 --- a/is/__snapshots__/annotation_test.ts.snap +++ b/is/__snapshots__/annotation_test.ts.snap @@ -10,10 +10,10 @@ snapshot[`isReadonlyOf > returns properly named function 1`] = `"isReadonlyOf snapshot[`isReadonlyOf > returns properly named function 2`] = `"isReadonlyOf(isReadonlyOf(isNumber))"`; -snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; - -snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; - snapshot[`isUnwrapReadonlyOf > returns properly named function 1`] = `"isNumber"`; snapshot[`isUnwrapReadonlyOf > returns properly named function 2`] = `"isReadonlyOf(isNumber)"`; + +snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; + +snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index 95e79c5..254e535 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,5 +1,9 @@ export const snapshot = {}; +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; + +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; + snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; @@ -10,55 +14,7 @@ snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUnifor snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; - -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; - -snapshot[`isObjectOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - b: isString, - c: isBoolean -})" -`; - -snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; - -snapshot[`isObjectOf > returns properly named function 3`] = ` -"isObjectOf({ - a: isObjectOf({ - b: isObjectOf({c: isBoolean}) - }) -})" -`; - -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ - isNumber, - isString, - isBoolean -], isArray)" -`; - -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; - -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ - isNumber, - isString, - isBoolean - ], isArray) - ], isArray) -])" -`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyOf(isUniformTupleOf(3, isAny))"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyOf(isUniformTupleOf(3, isNumber))"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyOf(isUniformTupleOf(3, (anonymous)))"`; +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` "isReadonlyOf(isTupleOf([ @@ -82,32 +38,28 @@ snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` ], isArray))" `; -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyOf(isTupleOf([ isNumber, isString, isBoolean -])" +]))" `; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)]))"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ isNumber, isString, isBoolean - ]) - ]) -])" + ])) + ])) +]))" `; -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; - -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; - snapshot[`isStrictOf > returns properly named function 1`] = ` "isStrictOf(isObjectOf({ a: isNumber, @@ -126,6 +78,16 @@ snapshot[`isStrictOf > returns properly named function 3`] = ` }))" `; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyOf(isUniformTupleOf(3, isAny))"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyOf(isUniformTupleOf(3, isNumber))"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyOf(isUniformTupleOf(3, (anonymous)))"`; + +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; + +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; + snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; @@ -140,42 +102,80 @@ snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(u snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ + isNumber, + isString, + isBoolean +])" +`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ + isNumber, + isString, + isBoolean + ]) + ]) +])" +`; -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyOf(isTupleOf([ +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; + +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ isNumber, isString, isBoolean -]))" +], isArray)" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)]))"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyOf(isTupleOf([ - isReadonlyOf(isTupleOf([ - isReadonlyOf(isTupleOf([ +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ isNumber, isString, isBoolean - ])) - ])) -]))" + ], isArray) + ], isArray) +])" +`; + +snapshot[`isObjectOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +})" +`; + +snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; + +snapshot[`isObjectOf > returns properly named function 3`] = ` +"isObjectOf({ + a: isObjectOf({ + b: isObjectOf({c: isBoolean}) + }) +})" `; snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; + +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index fe35c36..9b78afe 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -1,16 +1,43 @@ export const snapshot = {}; -snapshot[`isIntersectionOf > returns properly named function 1`] = ` +snapshot[`isRequiredOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, - b: isString + b: isUnionOf([ + isString, + isUndefined + ]), + c: isBoolean })" `; +snapshot[`isRequiredOf > returns properly named function 2`] = ` +"isObjectOf({ + a: isNumber, + b: isUnionOf([ + isString, + isUndefined + ]), + c: isBoolean +})" +`; + +snapshot[`isPickOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + c: isBoolean +})" +`; + +snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; + snapshot[`isPartialOf > returns properly named function 1`] = ` "isObjectOf({ a: isOptionalOf(isNumber), - b: isOptionalOf(isString), + b: isOptionalOf(isUnionOf([ + isString, + isUndefined + ])), c: isOptionalOf(isBoolean) })" `; @@ -18,17 +45,19 @@ snapshot[`isPartialOf > returns properly named function 1`] = ` snapshot[`isPartialOf > returns properly named function 2`] = ` "isObjectOf({ a: isOptionalOf(isNumber), - b: isOptionalOf(isString), + b: isOptionalOf(isUnionOf([ + isString, + isUndefined + ])), c: isOptionalOf(isBoolean) })" `; -snapshot[`isUnionOf > returns properly named function 1`] = ` -"isUnionOf([ - isNumber, - isString, - isBoolean -])" +snapshot[`isIntersectionOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString +})" `; snapshot[`isOmitOf > returns properly named function 1`] = ` @@ -40,11 +69,10 @@ snapshot[`isOmitOf > returns properly named function 1`] = ` snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; -snapshot[`isPickOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - c: isBoolean -})" +snapshot[`isUnionOf > returns properly named function 1`] = ` +"isUnionOf([ + isNumber, + isString, + isBoolean +])" `; - -snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; diff --git a/is/utility.ts b/is/utility.ts index f8f6537..e4dcaa5 100644 --- a/is/utility.ts +++ b/is/utility.ts @@ -1,6 +1,6 @@ import type { FlatType, UnionToIntersection } from "../_typeutil.ts"; import type { Predicate } from "./type.ts"; -import { isOptionalOf } from "./annotation.ts"; +import { isOptionalOf, isUnwrapOptionalOf } from "./annotation.ts"; import { isObjectOf } from "./factory.ts"; import { type GetMetadata, @@ -147,6 +147,42 @@ export function isAllOf< return isIntersectionOf(preds); } +/** + * Return a type predicate function that returns `true` if the type of `x` is `Required>`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```typescript + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isMyType = is.RequiredOf(is.ObjectOf({ + * a: is.Number, + * b: is.UnionOf([is.String, is.Undefined]), + * c: is.OptionalOf(is.Boolean), + * })); + * const a: unknown = { a: 0, b: "b", c: true, other: "other" }; + * if (isMyType(a)) { + * // 'a' is narrowed to { a: number; b: string | undefined; c: boolean } + * const _: { a: number; b: string | undefined; c: boolean } = a; + * } + * ``` + */ +export function isRequiredOf< + T extends Record, +>( + pred: Predicate & WithMetadata, +): + & Predicate>> + & WithMetadata { + const { args } = getPredicateFactoryMetadata(pred); + const predObj = Object.fromEntries( + Object.entries(args[0]).map(([k, v]) => [k, isUnwrapOptionalOf(v)]), + ); + return isObjectOf(predObj) as + & Predicate>> + & WithMetadata; +} + /** * Return a type predicate function that returns `true` if the type of `x` is `Partial>`. * @@ -157,7 +193,7 @@ export function isAllOf< * * const isMyType = is.PartialOf(is.ObjectOf({ * a: is.Number, - * b: is.String, + * b: is.UnionOf([is.String, is.Undefined]), * c: is.OptionalOf(is.Boolean), * })); * const a: unknown = { a: undefined, other: "other" }; @@ -271,5 +307,6 @@ export default { OneOf: isOneOf, PartialOf: isPartialOf, PickOf: isPickOf, + RequiredOf: isRequiredOf, UnionOf: isUnionOf, }; diff --git a/is/utility_test.ts b/is/utility_test.ts index 498448d..11ff12e 100644 --- a/is/utility_test.ts +++ b/is/utility_test.ts @@ -8,13 +8,15 @@ import { import { assertType } from "https://deno.land/std@0.211.0/testing/types.ts"; import { type Equal, stringify } from "./_testutil.ts"; import { type Predicate, type PredicateType } from "./type.ts"; -import { isBoolean, isNumber, isString } from "./core.ts"; +import { isOptionalOf } from "./annotation.ts"; +import { isBoolean, isNumber, isString, isUndefined } from "./core.ts"; import { isObjectOf } from "./factory.ts"; import is, { isIntersectionOf, isOmitOf, isPartialOf, isPickOf, + isRequiredOf, isUnionOf, } from "./utility.ts"; @@ -150,11 +152,52 @@ Deno.test("isIntersectionOf", async (t) => { }); }); +Deno.test("isRequiredOf", async (t) => { + const pred = isObjectOf({ + a: isNumber, + b: isUnionOf([isString, isUndefined]), + c: isOptionalOf(isBoolean), + }); + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isRequiredOf(pred).name); + // Nestable (no effect) + await assertSnapshot(t, isRequiredOf(isRequiredOf(pred)).name); + }); + await t.step("returns proper type predicate", () => { + const a: unknown = { a: 0, b: "a", c: true }; + if (isRequiredOf(pred)(a)) { + assertType< + Equal + >(true); + } + }); + await t.step("returns true on Required object", () => { + assertEquals( + isRequiredOf(pred)({ a: undefined, b: undefined, c: undefined }), + false, + "Object does not have required properties", + ); + assertEquals( + isRequiredOf(pred)({}), + false, + "Object does not have required properties", + ); + }); + await t.step("returns false on non Required object", () => { + assertEquals(isRequiredOf(pred)("a"), false, "Value is not an object"); + assertEquals( + isRequiredOf(pred)({ a: 0, b: "a", c: "" }), + false, + "Object have a different type property", + ); + }); +}); + Deno.test("isPartialOf", async (t) => { const pred = isObjectOf({ a: isNumber, - b: isString, - c: isBoolean, + b: isUnionOf([isString, isUndefined]), + c: isOptionalOf(isBoolean), }); await t.step("returns properly named function", async (t) => { await assertSnapshot(t, isPartialOf(pred).name); From 5d15f38a90576d5c955c71811497740ef27830cf Mon Sep 17 00:00:00 2001 From: Alisue Date: Mon, 12 Feb 2024 22:59:56 +0900 Subject: [PATCH 30/31] :memo: Update README and mod.ts --- README.md | 116 +++++++++++++- is/__snapshots__/annotation_test.ts.snap | 8 +- is/__snapshots__/factory_test.ts.snap | 184 +++++++++++------------ is/__snapshots__/utility_test.ts.snap | 24 +-- mod.ts | 116 +++++++++++++- 5 files changed, 334 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index f72d28e..b60191f 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,7 @@ if (is.String(a)) { } ``` -Additionally, `is*Of` (or `is.*Of`) functions return type predicate functions to -predicate types of `x` more precisely like: +For more complex types, you can use `is*Of` (or `is.*Of`) functions like: ```typescript import { @@ -44,7 +43,7 @@ const isArticle = is.ObjectOf({ title: is.String, body: is.String, refs: is.ArrayOf( - is.OneOf([ + is.UnionOf([ is.String, is.ObjectOf({ name: is.String, @@ -52,8 +51,11 @@ const isArticle = is.ObjectOf({ }), ]), ), + createTime: is.OptionalOf(is.InstanceOf(Date)), + updateTime: is.OptionalOf(is.InstanceOf(Date)), }); +// Infer the type of `Article` from the definition of `isArticle` type Article = PredicateType; const a: unknown = { @@ -76,6 +78,114 @@ if (isArticle(a)) { } ``` +Additionally, you can manipulate the predicate function returned from +`isObjectOf` with `isPickOf`, `isOmitOf`, `isPartialOf`, and `isRequiredOf` +similar to TypeScript's `Pick`, `Omit`, `Partial`, `Required` utility types. + +```typescript +import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + +const isArticle = is.ObjectOf({ + title: is.String, + body: is.String, + refs: is.ArrayOf( + is.UnionOf([ + is.String, + is.ObjectOf({ + name: is.String, + url: is.String, + }), + ]), + ), + createTime: is.OptionalOf(is.InstanceOf(Date)), + updateTime: is.OptionalOf(is.InstanceOf(Date)), +}); + +const isArticleCreateParams = is.PickOf(isArticle, ["title", "body", "refs"]); +// is equivalent to +//const isArticleCreateParams = is.ObjectOf({ +// title: is.String, +// body: is.String, +// refs: is.ArrayOf( +// is.UnionOf([ +// is.String, +// is.ObjectOf({ +// name: is.String, +// url: is.String, +// }), +// ]), +// ), +//}); + +const isArticleUpdateParams = is.OmitOf(isArticleCreateParams, ["title"]); +// is equivalent to +//const isArticleUpdateParams = is.ObjectOf({ +// body: is.String, +// refs: is.ArrayOf( +// is.UnionOf([ +// is.String, +// is.ObjectOf({ +// name: is.String, +// url: is.String, +// }), +// ]), +// ), +//}); + +const isArticlePatchParams = is.PartialOf(isArticleUpdateParams); +// is equivalent to +//const isArticlePatchParams = is.ObjectOf({ +// body: is.OptionalOf(is.String), +// refs: is.OptionalOf(is.ArrayOf( +// is.UnionOf([ +// is.String, +// is.ObjectOf({ +// name: is.String, +// url: is.String, +// }), +// ]), +// )), +//}); + +const isArticleAvailableParams = is.RequiredOf(isArticle); +// is equivalent to +//const isArticlePutParams = is.ObjectOf({ +// body: is.String, +// refs: is.ArrayOf( +// is.UnionOf([ +// is.String, +// is.ObjectOf({ +// name: is.String, +// url: is.String, +// }), +// ]), +// ), +// createTime: is.InstanceOf(Date), +// updateTime: is.InstanceOf(Date), +//}); +``` + +If you need an union type or an intersection type, use `isUnionOf` and +`isIntersectionOf` like: + +```typescript +import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + +const isFoo = is.ObjectOf({ + foo: is.String, +}); + +const isBar = is.ObjectOf({ + bar: is.String, +}); + +const isFooOrBar = is.UnionOf([isFoo, isBar]); +// { foo: string } | { bar: string } + +const isFooAndBar = is.IntersectionOf([isFoo, isBar]); +// { foo: string } & { bar: string } +``` + ### assert The `assert` function does nothing if a given value is expected type. Otherwise, diff --git a/is/__snapshots__/annotation_test.ts.snap b/is/__snapshots__/annotation_test.ts.snap index 1af18e5..49bd40e 100644 --- a/is/__snapshots__/annotation_test.ts.snap +++ b/is/__snapshots__/annotation_test.ts.snap @@ -6,6 +6,10 @@ snapshot[`isUnwrapOptionalOf > returns properly named function 2`] = `"isNumb snapshot[`isUnwrapOptionalOf > returns properly named function 3`] = `"isNumber"`; +snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; + +snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; + snapshot[`isReadonlyOf > returns properly named function 1`] = `"isReadonlyOf(isNumber)"`; snapshot[`isReadonlyOf > returns properly named function 2`] = `"isReadonlyOf(isReadonlyOf(isNumber))"`; @@ -13,7 +17,3 @@ snapshot[`isReadonlyOf > returns properly named function 2`] = `"isReadonlyOf snapshot[`isUnwrapReadonlyOf > returns properly named function 1`] = `"isNumber"`; snapshot[`isUnwrapReadonlyOf > returns properly named function 2`] = `"isReadonlyOf(isNumber)"`; - -snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; - -snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; diff --git a/is/__snapshots__/factory_test.ts.snap b/is/__snapshots__/factory_test.ts.snap index 254e535..bb4a8f6 100644 --- a/is/__snapshots__/factory_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,64 +1,66 @@ export const snapshot = {}; -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; - -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; - snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; -snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; - -snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; +snapshot[`isObjectOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +})" +`; -snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; +snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; +snapshot[`isObjectOf > returns properly named function 3`] = ` +"isObjectOf({ + a: isObjectOf({ + b: isObjectOf({c: isBoolean}) + }) +})" +`; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyOf(isTupleOf([ +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ isNumber, isString, isBoolean -], isArray))" +])" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)], isArrayOf(isString)))"`; +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyOf(isTupleOf([ - isReadonlyOf(isTupleOf([ - isReadonlyOf(isTupleOf([ +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ isNumber, isString, isBoolean - ], isArray)) - ], isArray)) -], isArray))" + ]) + ]) +])" `; -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyOf(isTupleOf([ - isNumber, - isString, - isBoolean -]))" -`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)]))"`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyOf(isTupleOf([ - isReadonlyOf(isTupleOf([ - isReadonlyOf(isTupleOf([ - isNumber, - isString, - isBoolean - ])) - ])) -]))" -`; +snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; + +snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; + +snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; + +snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; + +snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; + +snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; + +snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; snapshot[`isStrictOf > returns properly named function 1`] = ` "isStrictOf(isObjectOf({ @@ -78,55 +80,83 @@ snapshot[`isStrictOf > returns properly named function 3`] = ` }))" `; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyOf(isUniformTupleOf(3, isAny))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyOf(isTupleOf([ + isNumber, + isString, + isBoolean +], isArray))" +`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyOf(isUniformTupleOf(3, isNumber))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)], isArrayOf(isString)))"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyOf(isUniformTupleOf(3, (anonymous)))"`; +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ + isNumber, + isString, + isBoolean + ], isArray)) + ], isArray)) +], isArray))" +`; snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; -snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; -snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; -snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; -snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; +snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; -snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; +snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; -snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; -snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ +snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` +"isReadonlyOf(isTupleOf([ isNumber, isString, isBoolean -])" +]))" `; -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)]))"`; -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ +snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` +"isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ isNumber, isString, isBoolean - ]) - ]) -])" + ])) + ])) +]))" `; -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; +snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; +snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; + +snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; + +snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; + +snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyOf(isUniformTupleOf(3, isAny))"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyOf(isUniformTupleOf(3, isNumber))"`; + +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyOf(isUniformTupleOf(3, (anonymous)))"`; snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ @@ -149,33 +179,3 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` ], isArray) ])" `; - -snapshot[`isObjectOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - b: isString, - c: isBoolean -})" -`; - -snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; - -snapshot[`isObjectOf > returns properly named function 3`] = ` -"isObjectOf({ - a: isObjectOf({ - b: isObjectOf({c: isBoolean}) - }) -})" -`; - -snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; - -snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; - -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; - -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; - -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; - -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap index 9b78afe..2c78c36 100644 --- a/is/__snapshots__/utility_test.ts.snap +++ b/is/__snapshots__/utility_test.ts.snap @@ -22,14 +22,22 @@ snapshot[`isRequiredOf > returns properly named function 2`] = ` })" `; -snapshot[`isPickOf > returns properly named function 1`] = ` +snapshot[`isUnionOf > returns properly named function 1`] = ` +"isUnionOf([ + isNumber, + isString, + isBoolean +])" +`; + +snapshot[`isOmitOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, c: isBoolean })" `; -snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; +snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; snapshot[`isPartialOf > returns properly named function 1`] = ` "isObjectOf({ @@ -60,19 +68,11 @@ snapshot[`isIntersectionOf > returns properly named function 1`] = ` })" `; -snapshot[`isOmitOf > returns properly named function 1`] = ` +snapshot[`isPickOf > returns properly named function 1`] = ` "isObjectOf({ a: isNumber, c: isBoolean })" `; -snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; - -snapshot[`isUnionOf > returns properly named function 1`] = ` -"isUnionOf([ - isNumber, - isString, - isBoolean -])" -`; +snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; diff --git a/mod.ts b/mod.ts index fe2f767..51b7fc6 100644 --- a/mod.ts +++ b/mod.ts @@ -21,8 +21,7 @@ * } * ``` * - * Additionally, `is*Of` (or `is.*Of`) functions return type predicate functions to - * predicate types of `x` more precisely like: + * For more complex types, you can use `is*Of` (or `is.*Of`) functions like: * * ```typescript * import { @@ -34,7 +33,7 @@ * title: is.String, * body: is.String, * refs: is.ArrayOf( - * is.OneOf([ + * is.UnionOf([ * is.String, * is.ObjectOf({ * name: is.String, @@ -42,8 +41,11 @@ * }), * ]), * ), + * createTime: is.OptionalOf(is.InstanceOf(Date)), + * updateTime: is.OptionalOf(is.InstanceOf(Date)), * }); * + * // Infer the type of `Article` from the definition of `isArticle` * type Article = PredicateType; * * const a: unknown = { @@ -66,6 +68,114 @@ * } * ``` * + * Additionally, you can manipulate the predicate function returned from + * `isObjectOf` with `isPickOf`, `isOmitOf`, `isPartialOf`, and `isRequiredOf` + * similar to TypeScript's `Pick`, `Omit`, `Partial`, `Required` utility types. + * + * ```typescript + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isArticle = is.ObjectOf({ + * title: is.String, + * body: is.String, + * refs: is.ArrayOf( + * is.UnionOf([ + * is.String, + * is.ObjectOf({ + * name: is.String, + * url: is.String, + * }), + * ]), + * ), + * createTime: is.OptionalOf(is.InstanceOf(Date)), + * updateTime: is.OptionalOf(is.InstanceOf(Date)), + * }); + * + * const isArticleCreateParams = is.PickOf(isArticle, ["title", "body", "refs"]); + * // is equivalent to + * //const isArticleCreateParams = is.ObjectOf({ + * // title: is.String, + * // body: is.String, + * // refs: is.ArrayOf( + * // is.UnionOf([ + * // is.String, + * // is.ObjectOf({ + * // name: is.String, + * // url: is.String, + * // }), + * // ]), + * // ), + * //}); + * + * const isArticleUpdateParams = is.OmitOf(isArticleCreateParams, ["title"]); + * // is equivalent to + * //const isArticleUpdateParams = is.ObjectOf({ + * // body: is.String, + * // refs: is.ArrayOf( + * // is.UnionOf([ + * // is.String, + * // is.ObjectOf({ + * // name: is.String, + * // url: is.String, + * // }), + * // ]), + * // ), + * //}); + * + * const isArticlePatchParams = is.PartialOf(isArticleUpdateParams); + * // is equivalent to + * //const isArticlePatchParams = is.ObjectOf({ + * // body: is.OptionalOf(is.String), + * // refs: is.OptionalOf(is.ArrayOf( + * // is.UnionOf([ + * // is.String, + * // is.ObjectOf({ + * // name: is.String, + * // url: is.String, + * // }), + * // ]), + * // )), + * //}); + * + * const isArticleAvailableParams = is.RequiredOf(isArticle); + * // is equivalent to + * //const isArticlePutParams = is.ObjectOf({ + * // body: is.String, + * // refs: is.ArrayOf( + * // is.UnionOf([ + * // is.String, + * // is.ObjectOf({ + * // name: is.String, + * // url: is.String, + * // }), + * // ]), + * // ), + * // createTime: is.InstanceOf(Date), + * // updateTime: is.InstanceOf(Date), + * //}); + * ``` + * + * If you need an union type or an intersection type, use `isUnionOf` and `isIntersectionOf` + * like: + * + * ```typescript + * import { is } from "https://deno.land/x/unknownutil@$MODULE_VERSION/mod.ts"; + * + * const isFoo = is.ObjectOf({ + * foo: is.String, + * }); + * + * const isBar = is.ObjectOf({ + * bar: is.String, + * }); + * + * const isFooOrBar = is.UnionOf([isFoo, isBar]); + * // { foo: string } | { bar: string } + * + * const isFooAndBar = is.IntersectionOf([isFoo, isBar]); + * // { foo: string } & { bar: string } + * ``` + * * ### assert * * The `assert` function does nothing if a given value is expected type. Otherwise, From a3b2679e9954492311d60e549624398de280d816 Mon Sep 17 00:00:00 2001 From: Alisue Date: Tue, 13 Feb 2024 02:22:10 +0900 Subject: [PATCH 31/31] :shower: Remove unused imports --- is/factory.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/is/factory.ts b/is/factory.ts index 052728c..138de9e 100644 --- a/is/factory.ts +++ b/is/factory.ts @@ -1,11 +1,6 @@ import type { FlatType } from "../_typeutil.ts"; import type { Predicate, PredicateType } from "./type.ts"; -import { - isOptional, - isOptionalOf, - isReadonly, - isReadonlyOf, -} from "./annotation.ts"; +import { isOptional, isOptionalOf, isReadonlyOf } from "./annotation.ts"; import { isAny, isArray,