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/_typeutil.ts b/_typeutil.ts new file mode 100644 index 0000000..4ef080f --- /dev/null +++ b/_typeutil.ts @@ -0,0 +1,10 @@ +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; + +export type Writable = { -readonly [P in keyof T]: T[P] }; diff --git a/is.ts b/is.ts index a7ea3ee..5c6155b 100644 --- a/is.ts +++ b/is.ts @@ -1,1265 +1,17 @@ -import { inspect } from "./inspect.ts"; +import annotation from "./is/annotation.ts"; +import core from "./is/core.ts"; +import factory from "./is/factory.ts"; +import utility from "./is/utility.ts"; -/** - * 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 const isSet = Object.defineProperties(isInstanceOf(Set), { - name: { - value: "isSet", - }, -}); - -/** - * 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)})`, - }, - }, - ); -} - -/** - * 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 = { - -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 = { - [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)})`, - }, - }, - ); - } -} - -/** - * 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< - 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< - 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)})`, - }, - }, - ); -} - -/** - * Synonym of `Record` - */ -export type RecordOf = Record; - -/** - * 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 { - if (isNullish(x) || isArray(x) || isSet(x) || isMap(x)) { - return false; - } - return typeof x === "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 const isMap = Object.defineProperties(isInstanceOf(Map), { - name: { - value: "isMap", - }, -}); - -/** - * 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 FlatType = T extends RecordOf - ? { [K in keyof T]: FlatType } - : T; - -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< - & { - [K in Exclude>]: T[K] extends - Predicate ? U : never; - } - & { - [K in OptionalPredicateKeys]?: T[K] extends Predicate ? U - : never; - } ->; - -/** - * 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 RecordOf>, ->( - predObj: T, - { strict }: { strict?: boolean } = {}, -): Predicate> { - return Object.defineProperties( - strict ? isObjectOfStrict(predObj) : isObjectOfLoose(predObj), - { - name: { - get: () => `isObjectOf(${inspect(predObj)})`, - }, - }, - ); -} - -function isObjectOfLoose< - T extends RecordOf>, ->( - 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 RecordOf>, ->( - 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 Object.prototype.toString.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 Object.prototype.toString.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 || - ["string", "number", "bigint", "boolean", "symbol"].includes(typeof x); -} - -/** - * 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 { - return Object.defineProperties( - (x: unknown): x is T[number] => - literals.includes(x as unknown as T[number]), - { - name: { - get: () => `isLiteralOneOf(${inspect(literals)})`, - }, - }, - ); -} - -export 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 UnionToIntersection = - (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) - ? I - : never; -export 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)})`, - }, - }, - ); -} - -export type OptionalPredicate = Predicate & { - optional: true; -}; - -/** - * 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, -): OptionalPredicate { - return Object.defineProperties( - (x: unknown): x is Predicate => isUndefined(x) || pred(x), - { - optional: { - value: true as const, - }, - name: { - get: () => `isOptionalOf(${inspect(pred)})`, - }, - }, - ) as OptionalPredicate; -} +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 { - 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, - InstanceOf: isInstanceOf, - Null: isNull, - Undefined: isUndefined, - Nullish: isNullish, - Symbol: isSymbol, - Primitive: isPrimitive, - LiteralOf: isLiteralOf, - LiteralOneOf: isLiteralOneOf, - OneOf: isOneOf, - AllOf: isAllOf, - OptionalOf: isOptionalOf, + ...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..49bd40e --- /dev/null +++ b/is/__snapshots__/annotation_test.ts.snap @@ -0,0 +1,19 @@ +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[`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[`isUnwrapReadonlyOf > returns properly named function 1`] = `"isNumber"`; + +snapshot[`isUnwrapReadonlyOf > returns properly named function 2`] = `"isReadonlyOf(isNumber)"`; diff --git a/__snapshots__/is_test.ts.snap b/is/__snapshots__/factory_test.ts.snap similarity index 78% rename from __snapshots__/is_test.ts.snap rename to is/__snapshots__/factory_test.ts.snap index 0fcb167..bb4a8f6 100644 --- a/__snapshots__/is_test.ts.snap +++ b/is/__snapshots__/factory_test.ts.snap @@ -1,12 +1,26 @@ export const snapshot = {}; -snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; +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[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber)"`; +snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous))"`; +snapshot[`isObjectOf > returns properly named function 3`] = ` +"isObjectOf({ + a: isObjectOf({ + b: isObjectOf({c: isBoolean}) + }) +})" +`; snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ @@ -30,87 +44,119 @@ snapshot[`isTupleOf > returns properly named function 3`] = ` ])" `; -snapshot[`isOneOf > returns properly named function 1`] = ` -"isOneOf([ - isNumber, - isString, - isBoolean -])" +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; + +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((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[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; + +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[`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[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; +snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; +snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; +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[`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[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyTupleOf([ +"isReadonlyOf(isTupleOf([ isNumber, isString, isBoolean -])" +]))" `; -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyTupleOf([(anonymous)])"`; +snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)]))"`; snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([ +"isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ + isReadonlyOf(isTupleOf([ isNumber, isString, isBoolean - ]) - ]) -])" + ])) + ])) +]))" `; -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[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber)"`; +snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous))"`; +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[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; -snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; +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 1`] = `"isReadonlyOf(isUniformTupleOf(3, isAny))"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyUniformTupleOf(3, isNumber)"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyOf(isUniformTupleOf(3, isNumber))"`; -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyUniformTupleOf(3, (anonymous))"`; +snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyOf(isUniformTupleOf(3, (anonymous)))"`; snapshot[`isTupleOf > returns properly named function 1`] = ` "isTupleOf([ @@ -133,48 +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[`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[`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[`isAllOf > returns properly named function 1`] = ` -"isAllOf([ - isObjectOf({a: isNumber}), - isObjectOf({b: isString}) -])" -`; diff --git a/is/__snapshots__/utility_test.ts.snap b/is/__snapshots__/utility_test.ts.snap new file mode 100644 index 0000000..2c78c36 --- /dev/null +++ b/is/__snapshots__/utility_test.ts.snap @@ -0,0 +1,78 @@ +export const snapshot = {}; + +snapshot[`isRequiredOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isUnionOf([ + isString, + isUndefined + ]), + c: isBoolean +})" +`; + +snapshot[`isRequiredOf > returns properly named function 2`] = ` +"isObjectOf({ + a: isNumber, + b: isUnionOf([ + isString, + isUndefined + ]), + c: isBoolean +})" +`; + +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), + b: isOptionalOf(isUnionOf([ + isString, + isUndefined + ])), + c: isOptionalOf(isBoolean) +})" +`; + +snapshot[`isPartialOf > returns properly named function 2`] = ` +"isObjectOf({ + a: isOptionalOf(isNumber), + b: isOptionalOf(isUnionOf([ + isString, + isUndefined + ])), + c: isOptionalOf(isBoolean) +})" +`; + +snapshot[`isIntersectionOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString +})" +`; + +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/_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/annotation.ts b/is/annotation.ts new file mode 100644 index 0000000..6062847 --- /dev/null +++ b/is/annotation.ts @@ -0,0 +1,178 @@ +import type { Predicate } from "./type.ts"; +import type { Writable } from "../_typeutil.ts"; +import { + getMetadata, + getPredicateFactoryMetadata, + 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; +}; + +/** + * 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` + */ +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; +}; + +/** + * 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 new file mode 100644 index 0000000..de68d76 --- /dev/null +++ b/is/annotation_test.ts @@ -0,0 +1,336 @@ +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 { isObjectOf, isTupleOf, isUniformTupleOf } from "./factory.ts"; +import is, { + isOptionalOf, + isReadonlyOf, + isUnwrapOptionalOf, + isUnwrapReadonlyOf, +} 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("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); + // 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("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) + .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/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..138de9e --- /dev/null +++ b/is/factory.ts @@ -0,0 +1,712 @@ +import type { FlatType } from "../_typeutil.ts"; +import type { Predicate, PredicateType } from "./type.ts"; +import { isOptional, isOptionalOf, isReadonlyOf } from "./annotation.ts"; +import { + isAny, + isArray, + isMap, + isRecord, + isSet, + type Primitive, +} from "./core.ts"; +import { + type GetMetadata, + getPredicateFactoryMetadata, + setPredicateFactoryMetadata, + type WithMetadata, +} from "../metadata.ts"; + +type IsReadonlyOfMetadata = GetMetadata>; + +/** + * 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 & WithMetadata { + return setPredicateFactoryMetadata( + (x: unknown): x is T[] => isArray(x) && x.every(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`. + * + * 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> & WithMetadata { + return setPredicateFactoryMetadata( + (x: unknown): x is Set => { + if (!isSet(x)) return false; + for (const v of x.values()) { + if (!pred(v)) return false; + } + return true; + }, + { name: "isSetOf", args: [pred] }, + ); +} + +type IsSetOfMetadata = { + name: "isSetOf"; + args: Parameters; +}; + +/** + * 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> & WithMetadata; +export function isTupleOf< + T extends readonly [Predicate, ...Predicate[]], + E extends Predicate, +>( + predTup: T, + predElse: E, +): + & Predicate<[...TupleOf, ...PredicateType]> + & WithMetadata; +export function isTupleOf< + T extends readonly [Predicate, ...Predicate[]], + E extends Predicate, +>( + predTup: T, + predElse?: E, +): + & Predicate | [...TupleOf, ...PredicateType]> + & WithMetadata { + if (!predElse) { + return setPredicateFactoryMetadata( + (x: unknown): x is TupleOf => { + if (!isArray(x) || x.length !== predTup.length) { + return false; + } + return predTup.every((pred, i) => pred(x[i])); + }, + { name: "isTupleOf", args: [predTup] }, + ); + } else { + return setPredicateFactoryMetadata( + (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: "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 `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. + * + * ```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>> & WithMetadata; +export function isReadonlyTupleOf< + T extends readonly [Predicate, ...Predicate[]], + E extends Predicate, +>( + predTup: T, + predElse: E, +): + & Predicate, ...PredicateType]>> + & WithMetadata; +export function isReadonlyTupleOf< + T extends readonly [Predicate, ...Predicate[]], + E extends Predicate, +>( + predTup: T, + predElse?: E, +): + & Predicate< + | Readonly> + | Readonly<[...TupleOf, ...PredicateType]> + > + & WithMetadata { + if (!predElse) { + return isReadonlyOf(isTupleOf(predTup)); + } else { + return isReadonlyOf(isTupleOf(predTup, predElse)); + } +} + +/** + * 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> & WithMetadata { + return setPredicateFactoryMetadata( + (x: unknown): x is UniformTupleOf => { + if (!isArray(x) || x.length !== n) { + return false; + } + return x.every((v) => pred(v)); + }, + { 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 `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. + * + * ```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>> + & WithMetadata { + return isReadonlyOf(isUniformTupleOf(n, 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> & WithMetadata { + return setPredicateFactoryMetadata( + (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: "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`. + * + * 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> & WithMetadata { + return setPredicateFactoryMetadata( + (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: "isMapOf", args: [pred, predKey] }, + ); +} + +type IsMapOfMetadata = { + name: "isMapOf"; + args: Parameters; +}; + +/** + * 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. + * + * 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; + * } + * ``` + * + * The `option.strict` is deprecated. Use `isStrictOf()` instead. + */ +export function isObjectOf< + T extends Record>, +>( + predObj: T, + options?: { strict?: boolean }, +): Predicate> & WithMetadata { + if (options?.strict) { + // deno-lint-ignore no-explicit-any + return isStrictOf(isObjectOf(predObj)) as any; + } + const requiredKeys = Object.entries(predObj) + .filter(([_, v]) => !isWithOptional(v)) + .map(([k]) => k); + return setPredicateFactoryMetadata( + (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; + } + return true; + }, + { name: "isObjectOf", args: [predObj] }, + ); +} + +type WithOptional = + | WithMetadata>> + | { optional: true }; // For backward compatibility + +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 WithOptional ? never : K]: T[K] extends + Predicate ? U : never; + } + // Optional + & { + [K in keyof T as T[K] extends WithOptional ? K : never]?: T[K] extends + Predicate ? U : never; + } +>; + +type IsObjectOfMetadata = { + name: "isObjectOf"; + args: [Parameters[0]]; +}; + +/** + * 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 } = getPredicateFactoryMetadata(pred); + const s = new Set(Object.keys(args[0])); + return setPredicateFactoryMetadata( + (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`. + * + * 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> & WithMetadata { + return setPredicateFactoryMetadata( + (x: unknown): x is InstanceType => x instanceof 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`. + * + * 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 & WithMetadata { + return setPredicateFactoryMetadata( + (x: unknown): x is T => x === 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`. + * + * 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 & WithMetadata { + const s = new Set(literals); + return setPredicateFactoryMetadata( + (x: unknown): x is T[number] => s.has(x as T[number]), + { name: "isLiteralOneOf", args: [literals] }, + ); +} + +type IsLiteralOneOfMetadata = { + name: "isLiteralOneOf"; + args: Parameters; +}; + +export default { + ArrayOf: isArrayOf, + InstanceOf: isInstanceOf, + LiteralOf: isLiteralOf, + LiteralOneOf: isLiteralOneOf, + MapOf: isMapOf, + ObjectOf: isObjectOf, + ReadonlyTupleOf: isReadonlyTupleOf, + ReadonlyUniformTupleOf: isReadonlyUniformTupleOf, + RecordOf: isRecordOf, + SetOf: isSetOf, + StrictOf: isStrictOf, + TupleOf: isTupleOf, + UniformTupleOf: isUniformTupleOf, +}; diff --git a/is_test.ts b/is/factory_test.ts similarity index 66% rename from is_test.ts rename to is/factory_test.ts index d6d2244..7a91a14 100644 --- a/is_test.ts +++ b/is/factory_test.ts @@ -6,53 +6,26 @@ 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 { isOptionalOf } from "./annotation.ts"; +import { isArray, isBoolean, isNumber, isString, isUndefined } 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, + isStrictOf, isTupleOf, - 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 -// 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"; +import { isUnionOf } from "./utility.ts"; const examples = { string: ["", "Hello world"], @@ -76,15 +49,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, @@ -111,91 +75,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); @@ -222,10 +101,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); @@ -252,24 +127,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( @@ -312,11 +169,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 @@ -324,15 +181,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>( @@ -342,12 +199,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( @@ -356,7 +213,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), @@ -414,14 +271,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 @@ -429,14 +286,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< @@ -446,7 +303,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, @@ -454,7 +311,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, @@ -472,7 +329,7 @@ Deno.test("isReadonlyTupleOf", async (t) => { false, ); }); - const predElse = is.Array; + const predElse = isArray; await testWithExamples( t, isReadonlyTupleOf([(_: unknown): _ is unknown => true], predElse), @@ -482,27 +339,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); @@ -528,12 +364,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"], @@ -568,13 +404,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, ); }); @@ -583,12 +419,6 @@ Deno.test("isReadonlyUniformTupleOf", async (t) => { }); }); -Deno.test("isRecord", async (t) => { - await testWithExamples(t, isRecord, { - validExamples: ["record", "date", "promise"], - }); -}); - Deno.test("isRecordOf", async (t) => { await t.step("returns properly named function", async (t) => { await assertSnapshot(t, isRecordOf(isNumber).name); @@ -649,12 +479,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); @@ -715,15 +539,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( @@ -802,15 +617,93 @@ Deno.test("isObjectOf", async (t) => { isObjectOf({ a: (_: unknown): _ is unknown => false }), { excludeExamples: ["record"] }, ); +}); + +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]), + b: isUnionOf([isString, isUndefined]), c: isOptionalOf(isBoolean), }; const a: unknown = { a: 0, b: "a" }; - if (isObjectOf(predObj)(a)) { + if (isStrictOf(isObjectOf(predObj))(a)) { assertType< Equal >(true); @@ -819,103 +712,63 @@ Deno.test("isObjectOf", 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(isObjectOf(predObj)({ a: 0, b: "a", c: true }), true); assertEquals( - isObjectOf(predObj)({ a: 0, b: "a" }), + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: true }), true, - "Object does not have an optional property", ); assertEquals( - isObjectOf(predObj)({ a: 0, b: "a", c: undefined }), + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a" }), true, - "Object has `undefined` as value of optional property", - ); - assertEquals( - isObjectOf(predObj, { strict: true })({ a: 0, b: "a", c: true }), - true, - "Specify `{ strict: true }`", + "Object does not have an optional property", ); assertEquals( - isObjectOf(predObj, { strict: true })({ a: 0, b: "a" }), + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: undefined }), true, - "Specify `{ strict: true }` and object does not have one optional property", + "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]), + b: isUnionOf([isString, isUndefined]), c: isOptionalOf(isBoolean), }; assertEquals( - isObjectOf(predObj)({ a: 0, b: "a", c: "" }), + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: "" }), false, "Object have a different type property", ); assertEquals( - isObjectOf(predObj)({ a: 0, b: "a", c: null }), + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: null }), false, "Object has `null` as value of optional property", ); assertEquals( - isObjectOf(predObj, { strict: true })({ + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: true, d: "invalid", }), false, - "Specify `{ strict: true }` and object have an unknown property", + "Object have an unknown property", ); assertEquals( - isObjectOf(predObj, { strict: true })({ + isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", d: "invalid", }), false, - "Specify `{ strict: true }` and object have the same number of properties but an unknown property exists", + "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) => { await t.step("returns properly named function", async (t) => { await assertSnapshot(t, isInstanceOf(Date).name); @@ -958,38 +811,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); @@ -1039,169 +860,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", "date", "promise", "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..e4dcaa5 --- /dev/null +++ b/is/utility.ts @@ -0,0 +1,312 @@ +import type { FlatType, UnionToIntersection } from "../_typeutil.ts"; +import type { Predicate } from "./type.ts"; +import { isOptionalOf, isUnwrapOptionalOf } from "./annotation.ts"; +import { isObjectOf } from "./factory.ts"; +import { + type GetMetadata, + getPredicateFactoryMetadata, + setPredicateFactoryMetadata, + type WithMetadata, +} from "../metadata.ts"; + +type IsObjectOfMetadata = GetMetadata>; + +/** + * 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.UnionOf([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.UnionOf(preds); + * const a: unknown = 0; + * if (isMyType(a)) { + * // a is narrowed to number | string | boolean + * const _: number | string | boolean = a; + * } + * ``` + */ +export function isUnionOf< + T extends readonly [Predicate, ...Predicate[]], +>( + preds: T, +): Predicate> & WithMetadata { + return setPredicateFactoryMetadata( + (x: unknown): x is UnionOf => preds.some((pred) => pred(x)), + { name: "isUnionOf", args: [preds] }, + ); +} + +type UnionOf = T extends readonly [Predicate, ...infer R] + ? U | UnionOf + : never; + +type IsUnionOfMetadata = { + name: "isUnionOf"; + args: Parameters; +}; + +/** + * 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.IntersectionOf([ + * 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.IntersectionOf(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 isIntersectionOf< + T extends readonly [ + Predicate & WithMetadata, + ...(Predicate & WithMetadata)[], + ], +>( + preds: T, +): Predicate> & WithMetadata { + const predObj = {}; + preds.forEach((pred) => { + Object.assign(predObj, getPredicateFactoryMetadata(pred).args[0]); + }); + return isObjectOf(predObj) as + & Predicate> + & WithMetadata; +} + +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 `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>`. + * + * 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.UnionOf([is.String, is.Undefined]), + * 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 } = getPredicateFactoryMetadata(pred); + const predObj = Object.fromEntries( + Object.entries(args[0]).map(([k, v]) => [k, isOptionalOf(v)]), + ); + return isObjectOf(predObj) as + & Predicate>> + & 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 } = getPredicateFactoryMetadata(pred); + const predObj = Object.fromEntries( + Object.entries(args[0]).filter(([k]) => s.has(k as K)), + ); + return isObjectOf(predObj) as + & Predicate>> + & 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 } = getPredicateFactoryMetadata(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, + IntersectionOf: isIntersectionOf, + OmitOf: isOmitOf, + OneOf: isOneOf, + PartialOf: isPartialOf, + PickOf: isPickOf, + RequiredOf: isRequiredOf, + UnionOf: isUnionOf, +}; diff --git a/is/utility_test.ts b/is/utility_test.ts new file mode 100644 index 0000000..11ff12e --- /dev/null +++ b/is/utility_test.ts @@ -0,0 +1,339 @@ +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 { 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"; + +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("isUnionOf", async (t) => { + await t.step("returns properly named function", async (t) => { + 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 (isUnionOf(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 (isUnionOf(preds)(a)) { + assertType>(true); + } + }); + await t.step("returns true on one of T", () => { + const preds = [isNumber, isString, isBoolean] as const; + 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, isUnionOf(preds), { + excludeExamples: ["number", "string", "boolean"], + }); + }); +}); + +Deno.test("isIntersectionOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot( + t, + isIntersectionOf([ + 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 (isIntersectionOf(preds)(a)) { + assertType>(true); + } + }); + await t.step("returns true on all of T", () => { + const preds = [ + isObjectOf({ a: isNumber }), + isObjectOf({ b: isString }), + ] as const; + assertEquals(isIntersectionOf(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( + isIntersectionOf(preds)({ a: 0, b: 0 }), + false, + "Some properties has wrong type", + ); + assertEquals( + isIntersectionOf(preds)({ a: 0 }), + false, + "Some properties does not exists", + ); + await testWithExamples(t, isIntersectionOf(preds), { + excludeExamples: ["record"], + }); + }); +}); + +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: isUnionOf([isString, isUndefined]), + c: isOptionalOf(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("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("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) + .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_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: () => { diff --git a/metadata.ts b/metadata.ts new file mode 100644 index 0000000..1e12571 --- /dev/null +++ b/metadata.ts @@ -0,0 +1,69 @@ +import type { Predicate } from "./is/type.ts"; +import { inspect } from "./inspect.ts"; + +const metadataKey = "__unknownutil_metadata"; + +/** + * A type that has metadata. + */ +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. + */ +export type PredicateFactoryMetadata = { + name: string; + args: unknown[]; +}; + +/** + * Set metadata to a predicate factory function. + */ +export function setPredicateFactoryMetadata< + P extends Predicate, + M extends PredicateFactoryMetadata, +>( + 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; +} + +/** + * Get metadata from a predicate factory function. + */ +export function getPredicateFactoryMetadata( + v: WithMetadata, +): M { + return getMetadata(v) as M; +} diff --git a/mod.ts b/mod.ts index 189a1df..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, @@ -130,6 +240,7 @@ */ export * from "./is.ts"; +export * from "./metadata.ts"; export * from "./util.ts"; import is from "./is.ts";