diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml deleted file mode 100644 index d79ec5f..0000000 --- a/.github/workflows/npm.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: npm - -env: - DENO_VERSION: 1.x - NODE_VERSION: 16.x - -on: - push: - tags: - - "v*" - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: denoland/setup-deno@v1 - with: - deno-version: ${{ env.DENO_VERSION }} - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - registry-url: "https://registry.npmjs.org" - - name: Build - run: deno task build-npm - - name: Publish - run: | - cd npm - npm publish - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9b2ed39..59080f6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,20 +59,6 @@ jobs: deno bench timeout-minutes: 5 - build-npm: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: denoland/setup-deno@v1 - with: - deno-version: ${{ env.DENO_VERSION }} - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - registry-url: "https://registry.npmjs.org" - - name: Build - run: deno task build-npm - jsr-publish: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/udd.yml b/.github/workflows/update.yml similarity index 100% rename from .github/workflows/udd.yml rename to .github/workflows/update.yml diff --git a/is_bench.ts b/.scripts/bench.ts similarity index 70% rename from is_bench.ts rename to .scripts/bench.ts index 14f1750..75235f9 100644 --- a/is_bench.ts +++ b/.scripts/bench.ts @@ -1,4 +1,5 @@ -import { is } from "./is.ts"; +import { as } from "../as/mod.ts"; +import { is } from "../is/mod.ts"; const cs: unknown[] = [ "Hello world", @@ -60,10 +61,10 @@ Deno.bench({ }); Deno.bench({ - name: "is.BigInt", + name: "is.Bigint", fn: () => { for (const c of cs) { - is.BigInt(c); + is.Bigint(c); } }, }); @@ -176,46 +177,6 @@ Deno.bench({ }, }); -Deno.bench({ - name: "is.ReadonlyTupleOf", - fn: () => { - const pred = is.ReadonlyTupleOf(predTup); - for (const c of cs) { - pred(c); - } - }, -}); - -const isReadonlyTupleOfPred = is.ReadonlyTupleOf(predTup); -Deno.bench({ - name: "is.ReadonlyTupleOf (pre)", - fn: () => { - for (const c of cs) { - isReadonlyTupleOfPred(c); - } - }, -}); - -Deno.bench({ - name: "is.ReadonlyTupleOf", - fn: () => { - const pred = is.ReadonlyTupleOf(predTup, is.Array); - for (const c of cs) { - pred(c); - } - }, -}); - -const isReadonlyTupleOfElsePred = is.ReadonlyTupleOf(predTup, is.Array); -Deno.bench({ - name: "is.ReadonlyTupleOf (pre)", - fn: () => { - for (const c of cs) { - isReadonlyTupleOfElsePred(c); - } - }, -}); - Deno.bench({ name: "is.UniformTupleOf", fn: () => { @@ -236,26 +197,6 @@ Deno.bench({ }, }); -Deno.bench({ - name: "is.ReadonlyUniformTupleOf", - fn: () => { - const pred = is.ReadonlyUniformTupleOf(3, is.String); - for (const c of cs) { - pred(c); - } - }, -}); - -const isReadonlyUniformTupleOfPred = is.ReadonlyUniformTupleOf(3, is.String); -Deno.bench({ - name: "is.ReadonlyUniformTupleOf (pre)", - fn: () => { - for (const c of cs) { - isReadonlyUniformTupleOfPred(c); - } - }, -}); - Deno.bench({ name: "is.Record", fn: () => { @@ -328,15 +269,6 @@ Deno.bench({ } }, }); -Deno.bench({ - name: "is.ObjectOf (strict)", - fn: () => { - const pred = is.ObjectOf(predObj, { strict: true }); - for (const c of cs) { - pred(c); - } - }, -}); const isObjectOfPred = is.ObjectOf(predObj); Deno.bench({ @@ -348,16 +280,6 @@ Deno.bench({ }, }); -const isObjectOfStrictPred = is.ObjectOf(predObj, { strict: true }); -Deno.bench({ - name: "is.ObjectOf (pre, strict)", - fn: () => { - for (const c of cs) { - isObjectOfStrictPred(c); - } - }, -}); - Deno.bench({ name: "is.Function", fn: () => { @@ -492,67 +414,22 @@ Deno.bench({ }, }); -const predsOne = [is.String, is.Number, is.Boolean] as const; -Deno.bench({ - name: "is.OneOf", - fn: () => { - const pred = is.OneOf(predsOne); - for (const c of cs) { - pred(c); - } - }, -}); - -const isOneOfPred = is.OneOf(predsOne); -Deno.bench({ - name: "is.OneOf (pre)", - fn: () => { - for (const c of cs) { - isOneOfPred(c); - } - }, -}); - -const predsAll = [ - is.ObjectOf({ a: is.Number }), - is.ObjectOf({ b: is.String }), -] as const; -Deno.bench({ - name: "is.AllOf", - fn: () => { - const pred = is.AllOf(predsAll); - for (const c of cs) { - pred(c); - } - }, -}); - -const isAllOfPred = is.AllOf(predsAll); -Deno.bench({ - name: "is.AllOf (pre)", - fn: () => { - for (const c of cs) { - isAllOfPred(c); - } - }, -}); - Deno.bench({ - name: "is.OptionalOf", + name: "as.Optional", fn: () => { - const pred = is.OptionalOf(is.String); + const pred = as.Optional(is.String); for (const c of cs) { pred(c); } }, }); -const isOptionalOfPred = is.OptionalOf(is.String); +const asOptionalPred = as.Optional(is.String); Deno.bench({ - name: "is.OptionalOf (pre)", + name: "as.Optional (pre)", fn: () => { for (const c of cs) { - isOptionalOfPred(c); + asOptionalPred(c); } }, }); diff --git a/scripts/build_npm.ts b/.scripts/build_npm.ts similarity index 100% rename from scripts/build_npm.ts rename to .scripts/build_npm.ts diff --git a/README.md b/README.md index 411c0f2..b9d92be 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,19 @@ # unknownutil [![jsr](https://jsr.io/badges/@core/unknownutil)](https://jsr.io/@core/unknownutil) -[![npm](https://img.shields.io/npm/v/unknownutil?logo=npm&logoColor=white)](https://www.npmjs.com/package/unknownutil) -[![denoland](https://img.shields.io/github/v/release/jsr-core/unknownutil?logo=deno&label=denoland)](https://deno.land/x/unknownutil) -[![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/unknownutil/mod.ts) [![test](https://github.com/jsr-core/unknownutil/workflows/Test/badge.svg)](https://github.com/jsr-core/unknownutil/actions?query=workflow%3ATest) [![codecov](https://codecov.io/github/jsr-core/unknownutil/graph/badge.svg?token=pfbLRGU5AM)](https://codecov.io/github/jsr-core/unknownutil) A utility pack for handling `unknown` type. -[deno]: https://deno.land/ - -> [!WARNING] -> -> The package on [deno.land] and [npm] is deprecated. Use the package on -> [jsr.io] instead. -> -> ``` -> deno add @core/unknownutil -> npx jsr add @core/unknownutil -> ``` - -[deno.land]: https://deno.land/x/unknownutil -[npm]: https://www.npmjs.com/package/unknownutil [jsr.io]: https://jsr.io/@core/unknownutil ## Usage -It provides `is` module for type predicate functions and `assert`, `ensure`, and -`maybe` helper functions. +It provides `is` and `as` module for type predicate functions and `assert`, +`ensure`, and `maybe` helper functions. -### is\* +### is\* and as\* Type predicate function is a function which returns `true` if a given value is expected type. For example, `isString` (or `is.String`) returns `true` if a @@ -45,10 +28,11 @@ if (is.String(a)) { } ``` -For more complex types, you can use `is*Of` (or `is.*Of`) functions like: +For more complex types, you can use `is*Of` (or `is.*Of`) functions and `as*` +(or `as.*`) like: ```typescript -import { is, PredicateType } from "@core/unknownutil"; +import { as, is, PredicateType } from "@core/unknownutil"; const isArticle = is.ObjectOf({ title: is.String, @@ -62,8 +46,8 @@ const isArticle = is.ObjectOf({ }), ]), ), - createTime: is.OptionalOf(is.InstanceOf(Date)), - updateTime: is.OptionalOf(is.InstanceOf(Date)), + createTime: as.Optional(is.InstanceOf(Date)), + updateTime: as.Optional(is.InstanceOf(Date)), }); // Infer the type of `Article` from the definition of `isArticle` @@ -94,7 +78,7 @@ Additionally, you can manipulate the predicate function returned from similar to TypeScript's `Pick`, `Omit`, `Partial`, `Required` utility types. ```typescript -import { is } from "@core/unknownutil"; +import { as, is } from "@core/unknownutil"; const isArticle = is.ObjectOf({ title: is.String, @@ -108,8 +92,8 @@ const isArticle = is.ObjectOf({ }), ]), ), - createTime: is.OptionalOf(is.InstanceOf(Date)), - updateTime: is.OptionalOf(is.InstanceOf(Date)), + createTime: as.Optional(is.InstanceOf(Date)), + updateTime: as.Optional(is.InstanceOf(Date)), }); const isArticleCreateParams = is.PickOf(isArticle, ["title", "body", "refs"]); @@ -146,8 +130,8 @@ const isArticleUpdateParams = is.OmitOf(isArticleCreateParams, ["title"]); const isArticlePatchParams = is.PartialOf(isArticleUpdateParams); // is equivalent to //const isArticlePatchParams = is.ObjectOf({ -// body: is.OptionalOf(is.String), -// refs: is.OptionalOf(is.ArrayOf( +// body: as.Optional(is.String), +// refs: as.Optional(is.ArrayOf( // is.UnionOf([ // is.String, // is.ObjectOf({ @@ -248,11 +232,6 @@ const a: unknown = "Hello"; const _: string = maybe(a, is.String) ?? "default value"; ``` -## Migration - -See [GitHub Wiki](https://github.com/jsr-core/unknownutil/wiki) for migration to -v3 from v2 or v2 from v1. - ## License The code follows MIT license written in [LICENSE](./LICENSE). Contributors need diff --git a/__snapshots__/inspect_test.ts.snap b/__snapshots__/_inspect_test.ts.snap similarity index 100% rename from __snapshots__/inspect_test.ts.snap rename to __snapshots__/_inspect_test.ts.snap diff --git a/__snapshots__/is_test.ts.snap b/__snapshots__/is_test.ts.snap deleted file mode 100644 index 9da812f..0000000 --- a/__snapshots__/is_test.ts.snap +++ /dev/null @@ -1,366 +0,0 @@ -export const snapshot = {}; - -snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; - -snapshot[`isOptionalOf > returns properly named function 2`] = `"isOptionalOf(isNumber)"`; - -snapshot[`isUnwrapOptionalOf > returns properly named function 1`] = `"isNumber"`; - -snapshot[`isUnwrapOptionalOf > returns properly named function 2`] = `"isNumber"`; - -snapshot[`isUnwrapOptionalOf > returns properly named function 3`] = `"isNumber"`; - -snapshot[`isReadonlyOf > returns properly named function 1`] = `"isReadonlyOf(isNumber)"`; - -snapshot[`isReadonlyOf > returns properly named function 2`] = `"isReadonlyOf(isReadonlyOf(isNumber))"`; - -snapshot[`isUnwrapReadonlyOf > returns properly named function 1`] = `"isNumber"`; - -snapshot[`isUnwrapReadonlyOf > returns properly named function 2`] = `"isReadonlyOf(isNumber)"`; - -snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; - -snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; - -snapshot[`isSetOf > returns properly named function 1`] = `"isSetOf(isNumber)"`; - -snapshot[`isSetOf > returns properly named function 2`] = `"isSetOf((anonymous))"`; - -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ - isNumber, - isString, - isBoolean -])" -`; - -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; - -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ - isNumber, - isString, - isBoolean - ]) - ]) -])" -`; - -snapshot[`isTupleOf > returns properly named function 1`] = ` -"isTupleOf([ - isNumber, - isString, - isBoolean -], isArray)" -`; - -snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; - -snapshot[`isTupleOf > returns properly named function 3`] = ` -"isTupleOf([ - isTupleOf([ - isTupleOf([ - isNumber, - isString, - isBoolean - ], isArray) - ], isArray) -])" -`; - -snapshot[`isParametersOf > returns properly named function 1`] = ` -"isParametersOf([ - isNumber, - isString, - isOptionalOf(isBoolean) -])" -`; - -snapshot[`isParametersOf > returns properly named function 2`] = `"isParametersOf([(anonymous)])"`; - -snapshot[`isParametersOf > returns properly named function 3`] = `"isParametersOf([])"`; - -snapshot[`isParametersOf > returns properly named function 4`] = ` -"isParametersOf([ - isParametersOf([ - isParametersOf([ - isNumber, - isString, - isOptionalOf(isBoolean) - ]) - ]) -])" -`; - -snapshot[`isParametersOf > returns properly named function 1`] = ` -"isParametersOf([ - isNumber, - isString, - isOptionalOf(isBoolean) -], isArray)" -`; - -snapshot[`isParametersOf > returns properly named function 2`] = `"isParametersOf([(anonymous)], isArrayOf(isString))"`; - -snapshot[`isParametersOf > returns properly named function 3`] = `"isParametersOf([], isArrayOf(isString))"`; - -snapshot[`isParametersOf > returns properly named function 4`] = ` -"isParametersOf([ - isParametersOf([ - isParametersOf([ - isNumber, - isString, - isOptionalOf(isBoolean) - ], isArray) - ], isArray) -])" -`; - -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyOf(isTupleOf([ - isNumber, - isString, - isBoolean -]))" -`; - -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)]))"`; - -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyOf(isTupleOf([ - isReadonlyOf(isTupleOf([ - isReadonlyOf(isTupleOf([ - isNumber, - isString, - isBoolean - ])) - ])) -]))" -`; - -snapshot[`isReadonlyTupleOf > returns properly named function 1`] = ` -"isReadonlyOf(isTupleOf([ - isNumber, - isString, - isBoolean -], isArray))" -`; - -snapshot[`isReadonlyTupleOf > returns properly named function 2`] = `"isReadonlyOf(isTupleOf([(anonymous)], isArrayOf(isString)))"`; - -snapshot[`isReadonlyTupleOf > returns properly named function 3`] = ` -"isReadonlyOf(isTupleOf([ - isReadonlyOf(isTupleOf([ - isReadonlyOf(isTupleOf([ - isNumber, - isString, - isBoolean - ], isArray)) - ], isArray)) -], isArray))" -`; - -snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; - -snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; - -snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 1`] = `"isReadonlyOf(isUniformTupleOf(3, isAny))"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 2`] = `"isReadonlyOf(isUniformTupleOf(3, isNumber))"`; - -snapshot[`isReadonlyUniformTupleOf > returns properly named function 3`] = `"isReadonlyOf(isUniformTupleOf(3, (anonymous)))"`; - -snapshot[`isRecordObjectOf > returns properly named function 1`] = `"isRecordObjectOf(isNumber, undefined)"`; - -snapshot[`isRecordObjectOf > returns properly named function 2`] = `"isRecordObjectOf((anonymous), undefined)"`; - -snapshot[`isRecordObjectOf > returns properly named function 1`] = `"isRecordObjectOf(isNumber, isString)"`; - -snapshot[`isRecordObjectOf > returns properly named function 2`] = `"isRecordObjectOf((anonymous), isString)"`; - -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, undefined)"`; - -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), undefined)"`; - -snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber, isString)"`; - -snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous), isString)"`; - -snapshot[`isRecordLikeOf > returns properly named function 1`] = `"isRecordLikeOf(isNumber, undefined)"`; - -snapshot[`isRecordLikeOf > returns properly named function 2`] = `"isRecordLikeOf((anonymous), undefined)"`; - -snapshot[`isRecordLikeOf > returns properly named function 1`] = `"isRecordLikeOf(isNumber, isString)"`; - -snapshot[`isRecordLikeOf > returns properly named function 2`] = `"isRecordLikeOf((anonymous), isString)"`; - -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, undefined)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), undefined)"`; - -snapshot[`isMapOf > returns properly named function 1`] = `"isMapOf(isNumber, isString)"`; - -snapshot[`isMapOf > returns properly named function 2`] = `"isMapOf((anonymous), isString)"`; - -snapshot[`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[`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[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; - -snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; - -snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; - -snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; - -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[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; - -snapshot[`isUnionOf > returns properly named function 1`] = ` -"isUnionOf([ - isNumber, - isString, - isBoolean -])" -`; - -snapshot[`isIntersectionOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - b: isString -})" -`; - -snapshot[`isIntersectionOf > returns properly named function 2`] = ` -"isString" -`; - -snapshot[`isIntersectionOf > returns properly named function 3`] = ` -"isIntersectionOf([ - isFunction, - isObjectOf({b: isString}) -])" -`; - -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[`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[`isPickOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - c: isBoolean -})" -`; - -snapshot[`isPickOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; - -snapshot[`isOmitOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - c: isBoolean -})" -`; - -snapshot[`isOmitOf > returns properly named function 2`] = `"isObjectOf({a: isNumber})"`; - -snapshot[`isOneOf > returns properly named function 1`] = ` -"isUnionOf([ - isNumber, - isString, - isBoolean -])" -`; - -snapshot[`isAllOf > returns properly named function 1`] = ` -"isObjectOf({ - a: isNumber, - b: isString -})" -`; diff --git a/_annotation.ts b/_annotation.ts new file mode 100644 index 0000000..73fa314 --- /dev/null +++ b/_annotation.ts @@ -0,0 +1,56 @@ +import type { Predicate } from "./type.ts"; + +export type Fn = (...args: unknown[]) => unknown; + +export function annotate( + fn: F, + name: N, + value: V, +): F & { [K in N]: V } { + return Object.defineProperties(fn, { + [name]: { + value, + }, + }) as F & { [K in N]: V }; +} + +export function unannotate( + fn: F & { [K in N]: V }, + name: N, +): V { + return fn[name]; +} + +export function hasAnnotation( + fn: F, + name: N, +): fn is F & { [K in N]: unknown } { + // deno-lint-ignore no-explicit-any + return !!(fn as any)[name]; +} + +/** + * Annotation for optional. + */ +export type AsOptional = { + optional: Predicate; +}; + +/** + * Annotation for readonly. + */ +export type AsReadonly = { + readonly: Predicate; +}; + +/** + * Annotation for predObj. + */ +export type IsPredObj< + T extends Record> = Record< + PropertyKey, + Predicate + >, +> = { + predObj: T; +}; diff --git a/_funcutil.ts b/_funcutil.ts new file mode 100644 index 0000000..54e1cf6 --- /dev/null +++ b/_funcutil.ts @@ -0,0 +1,21 @@ +import { inspect } from "./_inspect.ts"; + +/** + * Rewrite the function name. + */ +export function rewriteName unknown>( + fn: F, + name: string, + ...args: unknown[] +): F { + let cachedName: string | undefined; + return Object.defineProperties(fn, { + name: { + get: () => { + if (cachedName) return cachedName; + cachedName = `${name}(${args.map((v) => inspect(v)).join(", ")})`; + return cachedName; + }, + }, + }); +} diff --git a/inspect.ts b/_inspect.ts similarity index 100% rename from inspect.ts rename to _inspect.ts diff --git a/inspect_test.ts b/_inspect_test.ts similarity index 97% rename from inspect_test.ts rename to _inspect_test.ts index eecceea..c2f0042 100644 --- a/inspect_test.ts +++ b/_inspect_test.ts @@ -1,5 +1,5 @@ import { assertSnapshot } from "@std/testing/snapshot"; -import { inspect } from "./inspect.ts"; +import { inspect } from "./_inspect.ts"; Deno.test("inspect", async (t) => { await t.step("string", async (t) => { diff --git a/_testutil.ts b/_testutil.ts index 11e9e6f..e323b78 100644 --- a/_testutil.ts +++ b/_testutil.ts @@ -1,3 +1,54 @@ +import { assertEquals } from "@std/assert"; +import type { Predicate } from "./type.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; + +export 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); + }, + ); + } + } +} + // 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 diff --git a/_typeutil.ts b/_typeutil.ts index de0b13f..a61941f 100644 --- a/_typeutil.ts +++ b/_typeutil.ts @@ -1,10 +1,3 @@ export type FlatType = T extends Record ? { [K in keyof T]: FlatType } : T; - -export type TupleToIntersection = T extends readonly [] ? never - : T extends readonly [infer U] ? U - : T extends readonly [infer U, ...infer R] ? U & TupleToIntersection - : never; - -export type Writable = { -readonly [P in keyof T]: T[P] }; diff --git a/as/__snapshots__/readonly_test.ts.snap b/as/__snapshots__/readonly_test.ts.snap new file mode 100644 index 0000000..0c89b33 --- /dev/null +++ b/as/__snapshots__/readonly_test.ts.snap @@ -0,0 +1,11 @@ +export const snapshot = {}; + +snapshot[`asReadonly > returns properly named function 1`] = `"asReadonly(isNumber)"`; + +snapshot[`asReadonly > returns properly named function 2`] = `"asReadonly(isNumber)"`; + +snapshot[`asUnreadonly > returns properly named function 1`] = `"isNumber"`; + +snapshot[`asUnreadonly > returns properly named function 2`] = `"isNumber"`; + +snapshot[`asUnreadonly > returns properly named function 3`] = `"isNumber"`; diff --git a/as/mod.ts b/as/mod.ts new file mode 100644 index 0000000..0f46786 --- /dev/null +++ b/as/mod.ts @@ -0,0 +1,12 @@ +import { asOptional, asUnoptional } from "./optional.ts"; +import { asReadonly, asUnreadonly } from "./readonly.ts"; + +/** + * Annotation collection for object predicate properties. + */ +export const as = { + Optional: asOptional, + Readonly: asReadonly, + Unoptional: asUnoptional, + Unreadonly: asUnreadonly, +}; diff --git a/as/mod_test.ts b/as/mod_test.ts new file mode 100644 index 0000000..b93e4aa --- /dev/null +++ b/as/mod_test.ts @@ -0,0 +1,34 @@ +import { assertEquals } from "@std/assert"; +import { globToRegExp } from "@std/path"; +import { as } from "./mod.ts"; + +const excludes = [ + "mod.ts", + "*_test.ts", +]; + +Deno.test("as", async (t) => { + // List all files under the directory + const names = await listAsFunctions(); + await t.step( + "must have all `as*` function aliases as entries", + () => { + assertEquals(Object.keys(as).sort(), names); + }, + ); +}); + +async function listAsFunctions(): Promise { + const patterns = excludes.map((p) => globToRegExp(p)); + const names: string[] = []; + for await (const entry of Deno.readDir(import.meta.dirname!)) { + if (!entry.isFile || !entry.name.endsWith(".ts")) continue; + if (patterns.some((p) => p.test(entry.name))) continue; + const mod = await import(import.meta.resolve(`./${entry.name}`)); + const isFunctionNames = Object.entries(mod) + .filter(([k, _]) => k.startsWith("as")) + .map(([k, _]) => k.slice(2)); + names.push(...isFunctionNames); + } + return names.toSorted(); +} diff --git a/as/optional.ts b/as/optional.ts new file mode 100644 index 0000000..acadffd --- /dev/null +++ b/as/optional.ts @@ -0,0 +1,107 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { Predicate, PredicateType } from "../type.ts"; +import { + annotate, + type AsOptional, + hasAnnotation, + unannotate, +} from "../_annotation.ts"; + +/** + * Annotate the given predicate function as optional. + * + * Use this function to annotate a predicate function of `predObj` in {@linkcode isObjectOf}. + * + * Note that the annotated predicate function will return `true` if the type of `x` is `T` or `undefined`, indicating that + * this function is not just for annotation but it also changes the behavior of the predicate function. + * + * Use {@linkcode asUnoptional} to remove the annotation. + * Use {@linkcode hasOptional} to check if a predicate function has annotated with this function. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { as, is } from "@core/unknownutil"; + * + * const isMyType = is.ObjectOf({ + * foo: as.Optional(is.String), + * }); + * const a: unknown = {}; + * if (isMyType(a)) { + * const _: {foo?: string} = a; + * } + * ``` + */ +export function asOptional

>( + pred: P, +): + & Extract>> + & Predicate | undefined> + & AsOptional> { + if (hasAnnotation(pred, "optional")) { + return pred as + & Extract>> + & Predicate | undefined> + & AsOptional>; + } + return rewriteName( + annotate( + (x) => x === undefined || pred(x), + "optional", + pred, + ), + "asOptional", + pred, + ) as unknown as + & Extract>> + & Predicate | undefined> + & AsOptional>; +} + +/** + * Unannotate the annotated predicate function with {@linkcode asOptional}. + * + * Use this function to unannotate a predicate function of `predObj` in {@linkcode isObjectOf}. + * + * Note that the annotated predicate function will return `true` if the type of `x` is `T`, indicating that + * this function is not just for annotation but it also changes the behavior of the predicate function. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { as, is } from "@core/unknownutil"; + * + * const isMyType = is.ObjectOf({ + * foo: as.Unoptional(as.Optional(is.String)), + * }); + * const a: unknown = {foo: "a"}; + * if (isMyType(a)) { + * const _: {foo: string} = a; + * } + * ``` + */ +export function asUnoptional< + P extends Predicate, + T extends P extends Predicate ? T + : P extends Predicate ? T + : never, +>(pred: P): Predicate { + if (!hasAnnotation(pred, "optional")) { + return pred as Predicate; + } + return unannotate(pred, "optional") as Predicate; +} + +/** + * Check if the given type predicate has optional annotation. + */ +export function hasOptional< + P extends Predicate, + T extends P extends Predicate ? T + : P extends Predicate ? T + : never, +>( + pred: P, +): pred is P & AsOptional { + return hasAnnotation(pred, "optional"); +} diff --git a/as/optional_test.ts b/as/optional_test.ts new file mode 100644 index 0000000..f1e73a4 --- /dev/null +++ b/as/optional_test.ts @@ -0,0 +1,155 @@ +import { assertEquals } from "@std/assert"; +import { assertType } from "@std/testing/types"; +import { type Equal, testWithExamples } from "../_testutil.ts"; +import { is } from "../is/mod.ts"; +import { asOptional, asUnoptional } from "./optional.ts"; + +Deno.test("asOptional", async (t) => { + await t.step("returns a property named predicate function", () => { + const pred = asOptional(is.Number); + assertEquals(typeof pred, "function"); + assertEquals(pred.name, "asOptional(isNumber)"); + }); + + await t.step("returns a property named predicate function (nested)", () => { + const pred = asOptional(asOptional(is.Number)); + assertEquals(typeof pred, "function"); + assertEquals(pred.name, "asOptional(isNumber)"); + }); + + await t.step("returns a proper predicate function", async (t) => { + const pred = asOptional(is.Number); + await testWithExamples(t, pred, { + validExamples: ["number", "undefined"], + }); + }); + + await t.step("with isObjectOf", async (t) => { + const pred = is.ObjectOf({ + a: is.Number, + b: asOptional(is.Number), + c: asOptional(asOptional(is.Number)), + }); + await t.step("predicated type is correct", () => { + const v: unknown = undefined; + if (pred(v)) { + assertType>( + true, + ); + } + }); + }); + + await t.step("with isTupleOf", async (t) => { + const pred = is.TupleOf([ + is.Number, + asOptional(is.Number), + asOptional(asOptional(is.Number)), + ]); + await t.step("predicated type is correct", () => { + const v: unknown = undefined; + if (pred(v)) { + assertType< + Equal + >( + true, + ); + } + }); + }); + + await t.step("with isParametersOf", async (t) => { + const pred = is.ParametersOf( + [ + is.Number, + asOptional(is.Number), + asOptional(asOptional(is.Number)), + ] as const, + ); + await t.step("predicated type is correct", () => { + const v: unknown = undefined; + if (pred(v)) { + assertType< + Equal + >( + true, + ); + } + }); + }); +}); + +Deno.test("asUnoptional", async (t) => { + await t.step("returns a property named predicate function", () => { + const pred = asUnoptional(asOptional(is.Number)); + assertEquals(typeof pred, "function"); + assertEquals(pred.name, "isNumber"); + }); + + await t.step("returns a property named predicate function (nested)", () => { + const pred = asUnoptional(asUnoptional(asOptional(is.Number))); + assertEquals(typeof pred, "function"); + assertEquals(pred.name, "isNumber"); + }); + + await t.step("returns a proper predicate function", async (t) => { + const pred = asUnoptional(asOptional(is.Number)); + await testWithExamples(t, pred, { + validExamples: ["number"], + }); + }); + + await t.step("with isObjectOf", async (t) => { + const pred = is.ObjectOf({ + a: is.Number, + b: asUnoptional(asOptional(is.Number)), + c: asUnoptional(asUnoptional(asOptional(is.Number))), + }); + await t.step("predicated type is correct", () => { + const v: unknown = undefined; + if (pred(v)) { + assertType>( + true, + ); + } + }); + }); + + await t.step("with isTupleOf", async (t) => { + const pred = is.TupleOf([ + is.Number, + asUnoptional(asOptional(is.Number)), + asUnoptional(asUnoptional(asOptional(is.Number))), + ]); + await t.step("predicated type is correct", () => { + const v: unknown = undefined; + if (pred(v)) { + assertType< + Equal + >( + true, + ); + } + }); + }); + + await t.step("with isParametersOf", async (t) => { + const pred = is.ParametersOf( + [ + is.Number, + asUnoptional(asOptional(is.Number)), + asUnoptional(asUnoptional(asOptional(is.Number))), + ] as const, + ); + await t.step("predicated type is correct", () => { + const v: unknown = undefined; + if (pred(v)) { + assertType< + Equal + >( + true, + ); + } + }); + }); +}); diff --git a/as/readonly.ts b/as/readonly.ts new file mode 100644 index 0000000..ea0293f --- /dev/null +++ b/as/readonly.ts @@ -0,0 +1,83 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { Predicate } from "../type.ts"; +import { + annotate, + type AsReadonly, + hasAnnotation, + unannotate, +} from "../_annotation.ts"; + +/** + * Annotate the given predicate function as readonly. + * + * Use this function to annotate a predicate function of `predObj` in {@linkcode isObjectOf}. + * + * Use {@linkcode asUnreadonly} to remove the annotation. + * Use {@linkcode hasReadonly} to check if a predicate function has annotated with this function. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { as, is } from "@core/unknownutil"; + * + * const isMyType = is.ObjectOf({ + * foo: as.Readonly(is.String), + * }); + * const a: unknown = {}; + * if (isMyType(a)) { + * const _: {readonly foo: string} = a; + * } + * ``` + */ +export function asReadonly

>( + pred: P, +): P & AsReadonly { + if (hasAnnotation(pred, "readonly")) { + return pred as P & AsReadonly; + } + return rewriteName( + annotate((x) => pred(x), "readonly", pred), + "asReadonly", + pred, + ) as unknown as P & AsReadonly; +} + +/** + * Unannotate the annotated predicate function with {@linkcode asReadonly}. + * + * Use this function to unannotate a predicate function of `predObj` in {@linkcode isObjectOf}. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { as, is } from "@core/unknownutil"; + * + * const isMyType = is.ObjectOf({ + * foo: as.Unreadonly(as.Readonly(is.String)), + * }); + * const a: unknown = {foo: "a"}; + * if (isMyType(a)) { + * const _: {foo: string} = a; + * } + * ``` + */ +export function asUnreadonly< + P extends Predicate, + T extends P extends Predicate ? T : never, +>(pred: P): Predicate { + if (!hasAnnotation(pred, "readonly")) { + return pred as Predicate; + } + return unannotate(pred, "readonly") as Predicate; +} + +/** + * Check if the given type predicate has readonly annotation. + */ +export function hasReadonly< + P extends Predicate, +>( + pred: P, +): pred is P & AsReadonly { + return hasAnnotation(pred, "readonly"); +} diff --git a/as/readonly_test.ts b/as/readonly_test.ts new file mode 100644 index 0000000..2bc6dd2 --- /dev/null +++ b/as/readonly_test.ts @@ -0,0 +1,157 @@ +import { assertEquals } from "@std/assert"; +import { assertType } from "@std/testing/types"; +import { type Equal, testWithExamples } from "../_testutil.ts"; +import { is } from "../is/mod.ts"; +import { asReadonly, asUnreadonly } from "./readonly.ts"; + +Deno.test("asReadonly", async (t) => { + await t.step("returns a property named predicate function", () => { + const pred = asReadonly(is.Number); + assertEquals(typeof pred, "function"); + assertEquals(pred.name, "asReadonly(isNumber)"); + }); + + await t.step("returns a property named predicate function (nested)", () => { + const pred = asReadonly(asReadonly(is.Number)); + assertEquals(typeof pred, "function"); + assertEquals(pred.name, "asReadonly(isNumber)"); + }); + + await t.step("returns a proper predicate function", async (t) => { + const pred = asReadonly(is.Number); + await testWithExamples(t, pred, { + validExamples: ["number"], + }); + }); + + await t.step("with isObjectOf", async (t) => { + const pred = is.ObjectOf({ + a: is.Number, + b: asReadonly(is.Number), + c: asReadonly(asReadonly(is.Number)), + }); + await t.step("predicated type is correct", () => { + const v: unknown = undefined; + if (pred(v)) { + assertType< + Equal + >( + true, + ); + } + }); + }); + + await t.step("with isTupleOf", async (t) => { + const pred = is.TupleOf([ + is.Number, + asReadonly(is.Number), + asReadonly(asReadonly(is.Number)), + ]); + await t.step("predicated type is correct", () => { + const v: unknown = undefined; + if (pred(v)) { + assertType< + Equal + >( + true, + ); + } + }); + }); + + await t.step("with isParametersOf", async (t) => { + const pred = is.ParametersOf( + [ + is.Number, + asReadonly(is.Number), + asReadonly(asReadonly(is.Number)), + ] as const, + ); + await t.step("predicated type is correct", () => { + const v: unknown = undefined; + if (pred(v)) { + assertType< + Equal + >( + true, + ); + } + }); + }); +}); + +Deno.test("asUnreadonly", async (t) => { + await t.step("returns a property named predicate function", () => { + const pred = asUnreadonly(asReadonly(is.Number)); + assertEquals(typeof pred, "function"); + assertEquals(pred.name, "isNumber"); + }); + + await t.step("returns a property named predicate function (nested)", () => { + const pred = asUnreadonly(asUnreadonly(asReadonly(is.Number))); + assertEquals(typeof pred, "function"); + assertEquals(pred.name, "isNumber"); + }); + + await t.step("returns a proper predicate function", async (t) => { + const pred = asUnreadonly(asReadonly(is.Number)); + await testWithExamples(t, pred, { + validExamples: ["number"], + }); + }); + + await t.step("with isObjectOf", async (t) => { + const pred = is.ObjectOf({ + a: is.Number, + b: asUnreadonly(asReadonly(is.Number)), + c: asUnreadonly(asUnreadonly(asReadonly(is.Number))), + }); + await t.step("predicated type is correct", () => { + const v: unknown = undefined; + if (pred(v)) { + assertType>( + true, + ); + } + }); + }); + + await t.step("with isTupleOf", async (t) => { + const pred = is.TupleOf([ + is.Number, + asUnreadonly(asReadonly(is.Number)), + asUnreadonly(asUnreadonly(asReadonly(is.Number))), + ]); + await t.step("predicated type is correct", () => { + const v: unknown = undefined; + if (pred(v)) { + assertType< + Equal + >( + true, + ); + } + }); + }); + + await t.step("with isParametersOf", async (t) => { + const pred = is.ParametersOf( + [ + is.Number, + asUnreadonly(asReadonly(is.Number)), + asUnreadonly(asUnreadonly(asReadonly(is.Number))), + ] as const, + ); + await t.step("predicated type is correct", () => { + const v: unknown = undefined; + if (pred(v)) { + assertType< + Equal + >( + true, + ); + } + }); + }); +}); diff --git a/util.ts b/assert.ts similarity index 59% rename from util.ts rename to assert.ts index a17d404..c76ad75 100644 --- a/util.ts +++ b/assert.ts @@ -1,11 +1,17 @@ -import type { Predicate } from "./is.ts"; +import type { Predicate } from "./type.ts"; +/** + * A factory function that generates assertion error messages. + */ export type AssertMessageFactory = ( x: unknown, pred: Predicate, name?: string, ) => string; +/** + * The default factory function used to generate assertion error messages. + */ export const defaultAssertMessageFactory: AssertMessageFactory = ( x, pred, @@ -26,7 +32,7 @@ let assertMessageFactory = defaultAssertMessageFactory; */ export class AssertError extends Error { /** - * Constructs a new `AssertError` instance. + * Constructs a new instance. * @param message The error message. */ constructor(message?: string) { @@ -42,8 +48,7 @@ export class AssertError extends Error { /** * Sets the factory function used to generate assertion error messages. - * @param factory The factory function. - * @example + * * ```ts * import { is, setAssertMessageFactory } from "@core/unknownutil"; * @@ -59,6 +64,8 @@ export class AssertError extends Error { * } * }); * ``` + * + * @param factory The factory function. */ export function setAssertMessageFactory(factory: AssertMessageFactory): void { assertMessageFactory = factory; @@ -67,6 +74,8 @@ export function setAssertMessageFactory(factory: AssertMessageFactory): void { /** * Asserts that the given value satisfies the provided predicate. * + * It throws {@linkcode AssertError} if the value does not satisfy the predicate. + * * ```ts * import { assert, is } from "@core/unknownutil"; * @@ -78,8 +87,7 @@ export function setAssertMessageFactory(factory: AssertMessageFactory): void { * @param x The value to be asserted. * @param pred The predicate function to test the value against. * @param options Optional configuration for the assertion. - * @returns Nothing. The function has a return type of `asserts x is T` to help TypeScript narrow down the type of `x` after the assertion. - * @throws {AssertError} If the value does not satisfy the predicate. + * @returns The function has a return type of `asserts x is T` to help TypeScript narrow down the type of `x` after the assertion. */ export function assert( x: unknown, @@ -92,49 +100,3 @@ export function assert( ); } } - -/** - * Ensures that the given value satisfies the provided predicate. - * - * ```ts - * import { ensure, is } from "@core/unknownutil"; - * - * const a: unknown = "hello"; - * const _: string = ensure(a, is.String); - * ``` - * - * @param x The value to be ensured. - * @param pred The predicate function to test the value against. - * @param options Optional configuration for the assertion. - * @returns The input value `x`. - * @throws {AssertError} If the value does not satisfy the predicate. - */ -export function ensure( - x: unknown, - pred: Predicate, - options: { message?: string; name?: string } = {}, -): T { - assert(x, pred, options); - return x; -} - -/** - * Returns the input value if it satisfies the provided predicate, or `undefined` otherwise. - * - * ```ts - * import { is, maybe } from "@core/unknownutil"; - * - * const a: unknown = "hello"; - * const _: string = maybe(a, is.String) ?? "default value"; - * ``` - * - * @param x The value to be tested. - * @param pred The predicate function to test the value against. - * @returns The input value `x` if it satisfies the predicate, or `undefined` otherwise. - */ -export function maybe( - x: unknown, - pred: Predicate, -): T | undefined { - return pred(x) ? x : undefined; -} diff --git a/assert_test.ts b/assert_test.ts new file mode 100644 index 0000000..3840b64 --- /dev/null +++ b/assert_test.ts @@ -0,0 +1,67 @@ +import { assertThrows } from "@std/assert"; +import { + assert, + AssertError, + defaultAssertMessageFactory, + setAssertMessageFactory, +} from "./assert.ts"; + +const x: unknown = Symbol("x"); + +function truePredicate(_x: unknown): _x is string { + return true; +} + +function falsePredicate(_x: unknown): _x is string { + return false; +} + +Deno.test("assert", async (t) => { + await t.step("does nothing on true predicate", () => { + assert(x, truePredicate); + }); + + await t.step("throws an `AssertError` on false predicate", () => { + assertThrows( + () => assert(x, falsePredicate), + AssertError, + `Expected a value that satisfies the predicate falsePredicate, got symbol: undefined`, + ); + }); + + await t.step( + "throws an `AssertError` on false predicate with a custom name", + () => { + assertThrows( + () => assert(x, falsePredicate, { name: "hello world" }), + AssertError, + `Expected hello world that satisfies the predicate falsePredicate, got symbol: undefined`, + ); + }, + ); + + await t.step( + "throws an `AssertError` with a custom message on false predicate", + () => { + assertThrows( + () => assert(x, falsePredicate, { message: "Hello" }), + AssertError, + "Hello", + ); + }, + ); +}); + +Deno.test("setAssertMessageFactory", async (t) => { + setAssertMessageFactory((x, pred) => `Hello ${typeof x} ${pred.name}`); + + await t.step("change `AssertError` message on `assert` failure", () => { + assertThrows( + () => assert(x, falsePredicate), + AssertError, + "Hello symbol falsePredicate", + ); + }); + + setAssertMessageFactory(defaultAssertMessageFactory); +}); diff --git a/deno.jsonc b/deno.jsonc index 1ba1f1a..885e967 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,15 +1,80 @@ { "name": "@core/unknownutil", "version": "0.0.0", - "exports": "./mod.ts", + "exports": { + ".": "./mod.ts", + "./type": "./type.ts", + "./assert": "./assert.ts", + "./ensure": "./ensure.ts", + "./maybe": "./maybe.ts", + "./as": "./as/mod.ts", + "./as/optional": "./as/optional.ts", + "./as/readonly": "./as/readonly.ts", + "./is": "./is/mod.ts", + "./is/any": "./is/any.ts", + "./is/array": "./is/array.ts", + "./is/array-of": "./is/array_of.ts", + "./is/async-function": "./is/async_function.ts", + "./is/bigint": "./is/bigint.ts", + "./is/boolean": "./is/boolean.ts", + "./is/function": "./is/function.ts", + "./is/instance-of": "./is/instance_of.ts", + "./is/intersection-of": "./is/intersection_of.ts", + "./is/literal-of": "./is/literal_of.ts", + "./is/literal-one-of": "./is/literal_one_of.ts", + "./is/map": "./is/map.ts", + "./is/map-of": "./is/map_of.ts", + "./is/mod": "./is/mod.ts", + "./is/null": "./is/null.ts", + "./is/nullish": "./is/nullish.ts", + "./is/number": "./is/number.ts", + "./is/object-of": "./is/object_of.ts", + "./is/omit-of": "./is/omit_of.ts", + "./is/parameters-of": "./is/parameters_of.ts", + "./is/partial-of": "./is/partial_of.ts", + "./is/pick-of": "./is/pick_of.ts", + "./is/primitive": "./is/primitive.ts", + "./is/readonly-of": "./is/readonly_of.ts", + "./is/record": "./is/record.ts", + "./is/record-object": "./is/record_object.ts", + "./is/record-object-of": "./is/record_object_of.ts", + "./is/record-of": "./is/record_of.ts", + "./is/required-of": "./is/required_of.ts", + "./is/set": "./is/set.ts", + "./is/set-of": "./is/set_of.ts", + "./is/strict-of": "./is/strict_of.ts", + "./is/string": "./is/string.ts", + "./is/symbol": "./is/symbol.ts", + "./is/sync-function": "./is/sync_function.ts", + "./is/tuple-of": "./is/tuple_of.ts", + "./is/undefined": "./is/undefined.ts", + "./is/uniform-tuple-of": "./is/uniform_tuple_of.ts", + "./is/union-of": "./is/union_of.ts", + "./is/unknown": "./is/unknown.ts" + }, + "exclude": [ + ".coverage/**" + ], + "publish": { + "include": [ + "**/*.ts", + "README.md", + "LICENSE" + ], + "exclude": [ + "**/*_test.ts", + ".*" + ] + }, "imports": { + "@core/unknownutil": "./mod.ts", "@deno/dnt": "jsr:@deno/dnt@^0.41.1", "@std/assert": "jsr:@std/assert@^0.221.0", - "@std/testing": "jsr:@std/testing@^0.221.0", - "@core/unknownutil": "./mod.ts" + "@std/jsonc": "jsr:@std/jsonc@^1.0.0", + "@std/path": "jsr:@std/path@^1.0.2", + "@std/testing": "jsr:@std/testing@^0.221.0" }, "tasks": { - "build-npm": "deno run -A scripts/build_npm.ts $(git describe --tags --always --dirty)", "check": "deno check **/*.ts", "test": "deno test -A --doc --parallel --shuffle", "test:coverage": "deno task test --coverage=.coverage", diff --git a/ensure.ts b/ensure.ts new file mode 100644 index 0000000..8587c03 --- /dev/null +++ b/ensure.ts @@ -0,0 +1,28 @@ +import type { Predicate } from "./type.ts"; +import { assert } from "./assert.ts"; + +/** + * Ensures that the given value satisfies the provided predicate. + * + * It throws {@linkcode AssertError} if the value does not satisfy the predicate. + * + * ```ts + * import { ensure, is } from "@core/unknownutil"; + * + * const a: unknown = "hello"; + * const _: string = ensure(a, is.String); + * ``` + * + * @param x The value to be ensured. + * @param pred The predicate function to test the value against. + * @param options Optional configuration for the assertion. + * @returns The input value `x`. + */ +export function ensure( + x: unknown, + pred: Predicate, + options: { message?: string; name?: string } = {}, +): T { + assert(x, pred, options); + return x; +} diff --git a/util_test.ts b/ensure_test.ts similarity index 53% rename from util_test.ts rename to ensure_test.ts index 8087ff9..8659fa0 100644 --- a/util_test.ts +++ b/ensure_test.ts @@ -1,12 +1,10 @@ import { assertStrictEquals, assertThrows } from "@std/assert"; import { - assert, AssertError, defaultAssertMessageFactory, - ensure, - maybe, setAssertMessageFactory, -} from "./util.ts"; +} from "./assert.ts"; +import { ensure } from "./ensure.ts"; const x: unknown = Symbol("x"); @@ -18,42 +16,6 @@ function falsePredicate(_x: unknown): _x is string { return false; } -Deno.test("assert", async (t) => { - await t.step("does nothing on true predicate", () => { - assert(x, truePredicate); - }); - - await t.step("throws an `AssertError` on false predicate", () => { - assertThrows( - () => assert(x, falsePredicate), - AssertError, - `Expected a value that satisfies the predicate falsePredicate, got symbol: undefined`, - ); - }); - - await t.step( - "throws an `AssertError` on false predicate with a custom name", - () => { - assertThrows( - () => assert(x, falsePredicate, { name: "hello world" }), - AssertError, - `Expected hello world that satisfies the predicate falsePredicate, got symbol: undefined`, - ); - }, - ); - - await t.step( - "throws an `AssertError` with a custom message on false predicate", - () => { - assertThrows( - () => assert(x, falsePredicate, { message: "Hello" }), - AssertError, - "Hello", - ); - }, - ); -}); - Deno.test("ensure", async (t) => { await t.step("returns `x` as-is on true predicate", () => { assertStrictEquals(ensure(x, truePredicate), x); @@ -90,27 +52,9 @@ Deno.test("ensure", async (t) => { ); }); -Deno.test("maybe", async (t) => { - await t.step("returns `x` as-is on true predicate", () => { - assertStrictEquals(maybe(x, truePredicate), x); - }); - - await t.step("returns `undefined` on false predicate", () => { - assertStrictEquals(maybe(x, falsePredicate), undefined); - }); -}); - Deno.test("setAssertMessageFactory", async (t) => { setAssertMessageFactory((x, pred) => `Hello ${typeof x} ${pred.name}`); - await t.step("change `AssertError` message on `assert` failure", () => { - assertThrows( - () => assert(x, falsePredicate), - AssertError, - "Hello symbol falsePredicate", - ); - }); - await t.step("change `AssertError` message on `ensure` failure", () => { assertThrows( () => ensure(x, falsePredicate), diff --git a/is.ts b/is.ts deleted file mode 100644 index df38b38..0000000 --- a/is.ts +++ /dev/null @@ -1,1850 +0,0 @@ -import type { FlatType, TupleToIntersection, Writable } from "./_typeutil.ts"; -import { - type GetMetadata, - getMetadata, - getPredicateFactoryMetadata, - type PredicateFactoryMetadata, - setPredicateFactoryMetadata, - type WithMetadata, -} from "./metadata.ts"; - -const objectToString = Object.prototype.toString; -const primitiveSet = new Set([ - "string", - "number", - "bigint", - "boolean", - "symbol", -]); - -/** - * A type predicate function. - */ -export type Predicate = (x: unknown) => x is T; - -/** - * A type predicated by Predicate. - * - * ```ts - * import { is, type PredicateType } from "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 an object instance that satisfies `Record`. - * - * Note that this function check if the `x` is an instance of `Object`. - * Use `isRecordLike` instead if you want to check if the `x` satisfies the `Record` type. - * - * ```ts - * import { is } from "@core/unknownutil"; - * - * const a: unknown = {"a": 0, "b": 1}; - * if (is.RecordObject(a)) { - * // a is narrowed to Record - * const _: Record = a; - * } - * - * const b: unknown = new Set(); - * if (is.RecordObject(b)) { - * // b is not a raw object, so it is not narrowed - * } - * ``` - */ -export function isRecordObject( - x: unknown, -): x is Record { - return x != null && typeof x === "object" && x.constructor === Object; -} - -/** - * Return `true` if the type of `x` satisfies `Record`. - * - * Note that this function returns `true` for ambiguous instances like `Set`, `Map`, `Date`, `Promise`, etc. - * - * ```ts - * import { is } from "@core/unknownutil"; - * - * const a: unknown = {"a": 0, "b": 1}; - * if (is.Record(a)) { - * // a is narrowed to Record - * const _: Record = a; - * } - * - * const b: unknown = new Set(); - * if (is.Record(b)) { - * // b is narrowed to Record - * const _: Record = b; - * } - * ``` - */ -export function isRecord( - x: unknown, -): x is Record { - return x != null && !Array.isArray(x) && typeof x === "object"; -} - -/** - * Return `true` if the type of `x` is like `Record`. - * - * @deprecated Use `is.Record` instead. - * ``` - */ -export function isRecordLike( - x: unknown, -): x is Record { - return x != null && !Array.isArray(x) && typeof x === "object"; -} - -/** - * Return `true` if the type of `x` is `Map`. - * - * ```ts - * import { is } from "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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); -} - -/** - * 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 "@core/unknownutil"; - * - * 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 T | undefined => 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 "@core/unknownutil"; - * - * 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` - * - * **This is unstable and may be removed in the future.** - */ -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`. - * - * **This is unstable and may be removed in the future.** - * - * 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 "@core/unknownutil"; - * - * 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`. - * - * **This is unstable and may be removed in the future.** - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "@core/unknownutil"; - * - * 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; - -/** - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 `ParametersOf` or `ParametersOf`. - * - * This is similar to `TupleOf` or `TupleOf`, but if `is.OptionalOf()` is specified at the trailing, the trailing elements becomes optional and makes variable-length tuple. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "@core/unknownutil"; - * - * const isMyType = is.ParametersOf([ - * is.Number, - * is.OptionalOf(is.String), - * is.Boolean, - * is.OptionalOf(is.Number), - * is.OptionalOf(is.String), - * is.OptionalOf(is.Boolean), - * ] as const); - * const a: unknown = [0, undefined, "a"]; - * if (isMyType(a)) { - * // a is narrowed to [number, string | undefined, boolean, number?, string?, boolean?] - * const _: [number, string | undefined, boolean, number?, string?, boolean?] = a; - * } - * ``` - * - * With `predElse`: - * - * ```ts - * import { is } from "@core/unknownutil"; - * - * const isMyType = is.ParametersOf( - * [ - * is.Number, - * is.OptionalOf(is.String), - * is.OptionalOf(is.Boolean), - * ] as const, - * 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 "@core/unknownutil"; - * - * const predTup = [is.Number, is.String, is.OptionalOf(is.Boolean)] as const; - * const isMyType = is.ParametersOf(predTup); - * const a: unknown = [0, "a"]; - * if (isMyType(a)) { - * // a is narrowed to [number, string, boolean?] - * const _: [number, string, boolean?] = a; - * } - * ``` - */ -export function isParametersOf< - T extends readonly [...Predicate[]], ->( - predTup: T, -): Predicate> & WithMetadata; -export function isParametersOf< - T extends readonly [...Predicate[]], - E extends Predicate, ->( - predTup: T, - predElse: E, -): - & Predicate<[...ParametersOf, ...PredicateType]> - & WithMetadata; -export function isParametersOf< - T extends readonly [...Predicate[]], - E extends Predicate, ->( - predTup: T, - predElse?: E, -): - & Predicate | [...ParametersOf, ...PredicateType]> - & WithMetadata { - const requiresLength = 1 + predTup.findLastIndex((pred) => !isOptional(pred)); - if (!predElse) { - return setPredicateFactoryMetadata( - (x: unknown): x is ParametersOf => { - if ( - !isArray(x) || x.length < requiresLength || x.length > predTup.length - ) { - return false; - } - return predTup.every((pred, i) => pred(x[i])); - }, - { name: "isParametersOf", args: [predTup] }, - ); - } else { - return setPredicateFactoryMetadata( - (x: unknown): x is [...ParametersOf, ...PredicateType] => { - if (!isArray(x) || x.length < requiresLength) { - 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: "isParametersOf", args: [predTup, predElse] }, - ); - } -} - -type ParametersOf = T extends readonly [] ? [] - : T extends readonly [...infer P, infer R] - // Tuple of predicates - ? P extends Predicate[] - ? R extends Predicate & WithMetadata - // Last parameter is optional - ? [...ParametersOf

, PredicateType?] - // Last parameter is NOT optional - : [...ParametersOf

, PredicateType] - : never - // Array of predicates - : TupleOf; - -type IsParametersOfMetadata = { - name: "isParametersOf"; - 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 an Object instance that satisfies `Record`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * Note that this function check if the `x` is an instance of `Object`. - * Use `isRecordOf` instead if you want to check if the `x` satisfies the `Record` type. - * - * ```ts - * import { is } from "@core/unknownutil"; - * - * const isMyType = is.RecordObjectOf(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 "@core/unknownutil"; - * - * const isMyType = is.RecordObjectOf(is.Number, is.String); - * const a: unknown = {"a": 0, "b": 1}; - * if (isMyType(a)) { - * // a is narrowed to Record - * const _: Record = a; - * } - * ``` - */ -export function isRecordObjectOf( - pred: Predicate, - predKey?: Predicate, -): Predicate> & WithMetadata { - return setPredicateFactoryMetadata( - (x: unknown): x is Record => { - if (!isRecordObject(x)) return false; - for (const k in x) { - if (!pred(x[k])) return false; - if (predKey && !predKey(k)) return false; - } - return true; - }, - { name: "isRecordObjectOf", args: [pred, predKey] }, - ); -} - -type IsRecordObjectOfMetadata = { - name: "isRecordObjectOf"; - args: Parameters; -}; - -/** - * Return a type predicate function that returns `true` if the type of `x` satisfies `Record`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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` satisfies `Record`. - * - * @deprecated Use `is.RecordOf()` instead - */ -export function isRecordLikeOf( - pred: Predicate, - predKey?: Predicate, -): Predicate> & WithMetadata { - return setPredicateFactoryMetadata(isRecordOf(pred, predKey), { - name: "isRecordLikeOf", - args: [pred, predKey], - }); -} - -type IsRecordLikeOfMetadata = { - name: "isRecordLikeOf"; - 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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; - * } - * ``` - */ -export function isObjectOf< - T extends Record>, ->( - predObj: T, -): Predicate> & WithMetadata; -/** - * @deprecated The `option.strict` is deprecated. Use `isStrictOf()` instead. - */ -export function isObjectOf< - T extends Record>, ->( - predObj: T, - options: { strict?: boolean }, -): Predicate> & WithMetadata; -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; - } - return setPredicateFactoryMetadata( - (x: unknown): x is ObjectOf => { - if ( - x == null || - typeof x !== "object" && typeof x !== "function" || - Array.isArray(x) - ) return false; - // Check each values - for (const k in predObj) { - if (!predObj[k]((x as T)[k])) return false; - } - return true; - }, - { name: "isObjectOf", args: [predObj] }, - ); -} - -type WithOptional = - | WithMetadata>> - | { optional: true }; // For backward compatibility - -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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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; -}; - -/** - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 `IntersectionOf`. - * - * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. - * - * ```ts - * import { is } from "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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; -export function isIntersectionOf< - T extends readonly [Predicate], ->( - preds: T, -): T[0]; -export function isIntersectionOf< - T extends readonly [Predicate, ...Predicate[]], ->( - preds: T, -): Predicate> & WithMetadata; -export function isIntersectionOf< - T extends readonly [Predicate, ...Predicate[]], ->( - preds: T, -): - | Predicate - | Predicate> - & WithMetadata { - const predObj = {}; - const restPreds = preds.filter((pred) => { - const meta = getMetadata(pred); - if ((meta as IsObjectOfMetadata)?.name !== "isObjectOf") { - return true; - } - Object.assign(predObj, (meta as IsObjectOfMetadata).args[0]); - }); - if (restPreds.length < preds.length) { - restPreds.push(isObjectOf(predObj)); - } - if (restPreds.length === 1) { - return restPreds[0]; - } - return setPredicateFactoryMetadata( - (x: unknown): x is IntersectionOf => restPreds.every((pred) => pred(x)), - { name: "isIntersectionOf", args: [preds] }, - ); -} - -type IntersectionOf = TupleToIntersection>; - -type IsIntersectionOfMetadata = { - name: "isIntersectionOf"; - args: Parameters; -}; - -/** - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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 "@core/unknownutil"; - * - * 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; -} - -/** - * 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> { - return isUnionOf(preds); -} - -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 `IntersectionOf`. - * - * @deprecated Use `isIntersectionOf` instead. - */ -export function isAllOf< - T extends readonly [ - Predicate & WithMetadata, - ...(Predicate & WithMetadata)[], - ], ->( - preds: T, -): Predicate> { - return isIntersectionOf(preds); -} - -type AllOf = IntersectionOf; - -export const is = { - AllOf: isAllOf, - Any: isAny, - Array: isArray, - ArrayOf: isArrayOf, - AsyncFunction: isAsyncFunction, - BigInt: isBigInt, - Boolean: isBoolean, - Function: isFunction, - InstanceOf: isInstanceOf, - IntersectionOf: isIntersectionOf, - LiteralOf: isLiteralOf, - LiteralOneOf: isLiteralOneOf, - Map: isMap, - MapOf: isMapOf, - Null: isNull, - Nullish: isNullish, - Number: isNumber, - ObjectOf: isObjectOf, - OmitOf: isOmitOf, - OneOf: isOneOf, - Optional: isOptional, - OptionalOf: isOptionalOf, - ParametersOf: isParametersOf, - PartialOf: isPartialOf, - PickOf: isPickOf, - Primitive: isPrimitive, - Readonly: isReadonly, - ReadonlyOf: isReadonlyOf, - ReadonlyTupleOf: isReadonlyTupleOf, - ReadonlyUniformTupleOf: isReadonlyUniformTupleOf, - Record: isRecord, - RecordLike: isRecordLike, - RecordLikeOf: isRecordLikeOf, - RecordObject: isRecordObject, - RecordObjectOf: isRecordObjectOf, - RecordOf: isRecordOf, - RequiredOf: isRequiredOf, - Set: isSet, - SetOf: isSetOf, - StrictOf: isStrictOf, - String: isString, - Symbol: isSymbol, - SyncFunction: isSyncFunction, - TupleOf: isTupleOf, - Undefined: isUndefined, - UniformTupleOf: isUniformTupleOf, - UnionOf: isUnionOf, - Unknown: isUnknown, - UnwrapOptionalOf: isUnwrapOptionalOf, - UnwrapReadonlyOf: isUnwrapReadonlyOf, -}; diff --git a/is/__snapshots__/intersection_of_test.ts.snap b/is/__snapshots__/intersection_of_test.ts.snap new file mode 100644 index 0000000..56278e4 --- /dev/null +++ b/is/__snapshots__/intersection_of_test.ts.snap @@ -0,0 +1,17 @@ +export const snapshot = {}; + +snapshot[`isIntersectionOf > returns properly named predicate function 1`] = `"isString"`; + +snapshot[`isIntersectionOf > returns properly named predicate function 2`] = ` +"isObjectOf({ + a: isNumber, + b: isString +})" +`; + +snapshot[`isIntersectionOf > returns properly named predicate function 3`] = ` +"isIntersectionOf([ + isFunction, + isObjectOf({b: isString}) +])" +`; diff --git a/is/__snapshots__/object_of_test.ts.snap b/is/__snapshots__/object_of_test.ts.snap new file mode 100644 index 0000000..7524ffc --- /dev/null +++ b/is/__snapshots__/object_of_test.ts.snap @@ -0,0 +1,19 @@ +export const snapshot = {}; + +snapshot[`isObjectOf > returns properly named predicate function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +})" +`; + +snapshot[`isObjectOf > returns properly named predicate function 2`] = `"isObjectOf({a: a})"`; + +snapshot[`isObjectOf > returns properly named predicate function 3`] = ` +"isObjectOf({ + a: isObjectOf({ + b: isObjectOf({c: isBoolean}) + }) +})" +`; diff --git a/is/__snapshots__/omit_of_test.ts.snap b/is/__snapshots__/omit_of_test.ts.snap new file mode 100644 index 0000000..19c8629 --- /dev/null +++ b/is/__snapshots__/omit_of_test.ts.snap @@ -0,0 +1,10 @@ +export const snapshot = {}; + +snapshot[`isOmitOf > returns properly named predicate function 1`] = ` +"isObjectOf({ + a: isNumber, + c: isBoolean +})" +`; + +snapshot[`isOmitOf > returns properly named predicate function 2`] = `"isObjectOf({a: isNumber})"`; diff --git a/is/__snapshots__/parameters_of_test.ts.snap b/is/__snapshots__/parameters_of_test.ts.snap new file mode 100644 index 0000000..d1de1ce --- /dev/null +++ b/is/__snapshots__/parameters_of_test.ts.snap @@ -0,0 +1,49 @@ +export const snapshot = {}; + +snapshot[`isParametersOf > returns properly named predicate function 1`] = ` +"isParametersOf([ + isNumber, + isString, + asOptional(isBoolean) +])" +`; + +snapshot[`isParametersOf > returns properly named predicate function 2`] = `"isParametersOf([(anonymous)])"`; + +snapshot[`isParametersOf > returns properly named predicate function 3`] = `"isParametersOf([])"`; + +snapshot[`isParametersOf > returns properly named predicate function 4`] = ` +"isParametersOf([ + isParametersOf([ + isParametersOf([ + isNumber, + isString, + asOptional(isBoolean) + ]) + ]) +])" +`; + +snapshot[`isParametersOf > returns properly named predicate function 1`] = ` +"isParametersOf([ + isNumber, + isString, + asOptional(isBoolean) +], isArray)" +`; + +snapshot[`isParametersOf > returns properly named predicate function 2`] = `"isParametersOf([(anonymous)], isArrayOf(isString))"`; + +snapshot[`isParametersOf > returns properly named predicate function 3`] = `"isParametersOf([], isArrayOf(isString))"`; + +snapshot[`isParametersOf > returns properly named predicate function 4`] = ` +"isParametersOf([ + isParametersOf([ + isParametersOf([ + isNumber, + isString, + asOptional(isBoolean) + ], isArray) + ], isArray) +])" +`; diff --git a/is/__snapshots__/partial_of_test.ts.snap b/is/__snapshots__/partial_of_test.ts.snap new file mode 100644 index 0000000..6f96fc2 --- /dev/null +++ b/is/__snapshots__/partial_of_test.ts.snap @@ -0,0 +1,25 @@ +export const snapshot = {}; + +snapshot[`isPartialOf > returns properly named predicate function 1`] = ` +"isObjectOf({ + a: asOptional(isNumber), + b: asOptional(isUnionOf([ + isString, + isUndefined + ])), + c: asOptional(isBoolean), + d: asOptional(asReadonly(isString)) +})" +`; + +snapshot[`isPartialOf > returns properly named predicate function 2`] = ` +"isObjectOf({ + a: asOptional(isNumber), + b: asOptional(isUnionOf([ + isString, + isUndefined + ])), + c: asOptional(isBoolean), + d: asOptional(asReadonly(isString)) +})" +`; diff --git a/is/__snapshots__/pick_of_test.ts.snap b/is/__snapshots__/pick_of_test.ts.snap new file mode 100644 index 0000000..911dc0e --- /dev/null +++ b/is/__snapshots__/pick_of_test.ts.snap @@ -0,0 +1,10 @@ +export const snapshot = {}; + +snapshot[`isPickOf > returns properly named predicate function 1`] = ` +"isObjectOf({ + a: isNumber, + c: isBoolean +})" +`; + +snapshot[`isPickOf > returns properly named predicate function 2`] = `"isObjectOf({a: isNumber})"`; diff --git a/is/__snapshots__/readonly_of_test.ts.snap b/is/__snapshots__/readonly_of_test.ts.snap new file mode 100644 index 0000000..308a72d --- /dev/null +++ b/is/__snapshots__/readonly_of_test.ts.snap @@ -0,0 +1,47 @@ +export const snapshot = {}; + +snapshot[`isReadonlyOf > with isRecord > returns properly named predicate function 1`] = `"isReadonlyOf(isRecord)"`; + +snapshot[`isReadonlyOf > with isRecord > returns properly named predicate function 2`] = `"isReadonlyOf(isRecord)"`; + +snapshot[`isReadonlyOf > with isObjectOf > returns properly named predicate function 1`] = ` +"isReadonlyOf(isObjectOf({ + a: isNumber, + b: isUnionOf([ + isString, + isUndefined + ]), + c: asReadonly(isBoolean) +}))" +`; + +snapshot[`isReadonlyOf > with isObjectOf > returns properly named predicate function 2`] = ` +"isReadonlyOf(isObjectOf({ + a: isNumber, + b: isUnionOf([ + isString, + isUndefined + ]), + c: asReadonly(isBoolean) +}))" +`; + +snapshot[`isReadonlyOf > with isTupleOf > returns properly named predicate function 1`] = ` +"isReadonlyOf(isTupleOf([ + isNumber, + isString, + asReadonly(isBoolean) +]))" +`; + +snapshot[`isReadonlyOf > with isTupleOf > returns properly named predicate function 2`] = ` +"isReadonlyOf(isTupleOf([ + isNumber, + isString, + asReadonly(isBoolean) +]))" +`; + +snapshot[`isReadonlyOf > with isUniformTupleOf > returns properly named predicate function 1`] = `"isReadonlyOf(isUniformTupleOf(3, isNumber))"`; + +snapshot[`isReadonlyOf > with isUniformTupleOf > returns properly named predicate function 2`] = `"isReadonlyOf(isUniformTupleOf(3, isNumber))"`; diff --git a/is/__snapshots__/record_object_of_test.ts.snap b/is/__snapshots__/record_object_of_test.ts.snap new file mode 100644 index 0000000..1254e9f --- /dev/null +++ b/is/__snapshots__/record_object_of_test.ts.snap @@ -0,0 +1,9 @@ +export const snapshot = {}; + +snapshot[`isRecordObjectOf > returns properly named predicate function 1`] = `"isRecordObjectOf(isNumber, undefined)"`; + +snapshot[`isRecordObjectOf > returns properly named predicate function 2`] = `"isRecordObjectOf((anonymous), undefined)"`; + +snapshot[`isRecordObjectOf > returns properly named predicate function 1`] = `"isRecordObjectOf(isNumber, isString)"`; + +snapshot[`isRecordObjectOf > returns properly named predicate function 2`] = `"isRecordObjectOf((anonymous), isString)"`; diff --git a/is/__snapshots__/record_of_test.ts.snap b/is/__snapshots__/record_of_test.ts.snap new file mode 100644 index 0000000..aa1da92 --- /dev/null +++ b/is/__snapshots__/record_of_test.ts.snap @@ -0,0 +1,9 @@ +export const snapshot = {}; + +snapshot[`isRecordOf > returns properly named predicate function 1`] = `"isRecordOf(isNumber, undefined)"`; + +snapshot[`isRecordOf > returns properly named predicate function 2`] = `"isRecordOf((anonymous), undefined)"`; + +snapshot[`isRecordOf > returns properly named predicate function 1`] = `"isRecordOf(isNumber, isString)"`; + +snapshot[`isRecordOf > returns properly named predicate function 2`] = `"isRecordOf((anonymous), isString)"`; diff --git a/is/__snapshots__/required_of_test.ts.snap b/is/__snapshots__/required_of_test.ts.snap new file mode 100644 index 0000000..7c30fac --- /dev/null +++ b/is/__snapshots__/required_of_test.ts.snap @@ -0,0 +1,25 @@ +export const snapshot = {}; + +snapshot[`isRequiredOf > returns properly named predicate function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isUnionOf([ + isString, + isUndefined + ]), + c: isBoolean, + d: asReadonly(isString) +})" +`; + +snapshot[`isRequiredOf > returns properly named predicate function 2`] = ` +"isObjectOf({ + a: isNumber, + b: isUnionOf([ + isString, + isUndefined + ]), + c: isBoolean, + d: asReadonly(isString) +})" +`; diff --git a/is/__snapshots__/set_of_test.ts.snap b/is/__snapshots__/set_of_test.ts.snap new file mode 100644 index 0000000..b1dc926 --- /dev/null +++ b/is/__snapshots__/set_of_test.ts.snap @@ -0,0 +1,5 @@ +export const snapshot = {}; + +snapshot[`isSetOf > returns properly named predicate function 1`] = `"isSetOf(isNumber)"`; + +snapshot[`isSetOf > returns properly named predicate function 2`] = `"isSetOf((anonymous))"`; diff --git a/is/__snapshots__/strict_of_test.ts.snap b/is/__snapshots__/strict_of_test.ts.snap new file mode 100644 index 0000000..add9027 --- /dev/null +++ b/is/__snapshots__/strict_of_test.ts.snap @@ -0,0 +1,19 @@ +export const snapshot = {}; + +snapshot[`isStrictOf > returns properly named predicate function 1`] = ` +"isStrictOf(isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +}))" +`; + +snapshot[`isStrictOf > returns properly named predicate function 2`] = `"isStrictOf(isObjectOf({a: a}))"`; + +snapshot[`isStrictOf > returns properly named predicate function 3`] = ` +"isStrictOf(isObjectOf({ + a: isStrictOf(isObjectOf({ + b: isStrictOf(isObjectOf({c: isBoolean})) + })) +}))" +`; diff --git a/is/__snapshots__/tuple_of_test.ts.snap b/is/__snapshots__/tuple_of_test.ts.snap new file mode 100644 index 0000000..2098ae2 --- /dev/null +++ b/is/__snapshots__/tuple_of_test.ts.snap @@ -0,0 +1,45 @@ +export const snapshot = {}; + +snapshot[`isTupleOf > returns properly named predicate function 1`] = ` +"isTupleOf([ + isNumber, + isString, + isBoolean +])" +`; + +snapshot[`isTupleOf > returns properly named predicate function 2`] = `"isTupleOf([(anonymous)])"`; + +snapshot[`isTupleOf > returns properly named predicate function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ + isNumber, + isString, + isBoolean + ]) + ]) +])" +`; + +snapshot[`isTupleOf > returns properly named predicate function 1`] = ` +"isTupleOf([ + isNumber, + isString, + isBoolean +], isArray)" +`; + +snapshot[`isTupleOf > returns properly named predicate function 2`] = `"isTupleOf([(anonymous)], isArrayOf(isString))"`; + +snapshot[`isTupleOf > returns properly named predicate function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ + isNumber, + isString, + isBoolean + ], isArray) + ], isArray) +])" +`; diff --git a/is/__snapshots__/uniform_tuple_of_test.ts.snap b/is/__snapshots__/uniform_tuple_of_test.ts.snap new file mode 100644 index 0000000..5f32b7c --- /dev/null +++ b/is/__snapshots__/uniform_tuple_of_test.ts.snap @@ -0,0 +1,7 @@ +export const snapshot = {}; + +snapshot[`isUniformTupleOf > returns properly named predicate function 1`] = `"isUniformTupleOf(3, undefined)"`; + +snapshot[`isUniformTupleOf > returns properly named predicate function 2`] = `"isUniformTupleOf(3, isNumber)"`; + +snapshot[`isUniformTupleOf > returns properly named predicate function 3`] = `"isUniformTupleOf(3, (anonymous))"`; diff --git a/is/__snapshots__/union_of_test.ts.snap b/is/__snapshots__/union_of_test.ts.snap new file mode 100644 index 0000000..db9ae3f --- /dev/null +++ b/is/__snapshots__/union_of_test.ts.snap @@ -0,0 +1,9 @@ +export const snapshot = {}; + +snapshot[`isUnionOf > returns properly named predicate function 1`] = ` +"isUnionOf([ + isNumber, + isString, + isBoolean +])" +`; diff --git a/is/any.ts b/is/any.ts new file mode 100644 index 0000000..e3a177a --- /dev/null +++ b/is/any.ts @@ -0,0 +1,18 @@ +/** + * Assume `x is `any` and always return `true` regardless of the type of `x`. + * + * Use {@linkcode isUnknown} to assume that a value is `unknown`. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a = "a"; + * if (is.Any(a)) { + * const _: any = a; + * } + * ``` + */ +// deno-lint-ignore no-explicit-any +export function isAny(_x: unknown): _x is any { + return true; +} diff --git a/is/any_test.ts b/is/any_test.ts new file mode 100644 index 0000000..bc7746f --- /dev/null +++ b/is/any_test.ts @@ -0,0 +1,24 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isAny } from "./any.ts"; + +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", + ], + }); +}); diff --git a/is/array.ts b/is/array.ts new file mode 100644 index 0000000..9921c1d --- /dev/null +++ b/is/array.ts @@ -0,0 +1,19 @@ +/** + * Return `true` if the type of `x` is `unknown[]`. + * + * Use {@linkcode isArrayOf} to check if the type of `x` is an array of `T`. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = [0, 1, 2]; + * if (is.Array(a)) { + * const _: unknown[] = a; + * } + * ``` + */ +export function isArray( + x: unknown, +): x is unknown[] { + return Array.isArray(x); +} diff --git a/is/array_of.ts b/is/array_of.ts new file mode 100644 index 0000000..e9b91a7 --- /dev/null +++ b/is/array_of.ts @@ -0,0 +1,30 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { Predicate } from "../type.ts"; +import { isArray } from "./array.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `T[]`. + * + * Use {@linkcode isArray} to check if the type of `x` is an array of `unknown`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.ArrayOf(is.String); + * const a: unknown = ["a", "b", "c"]; + * if (isMyType(a)) { + * const _: string[] = a; + * } + * ``` + */ +export function isArrayOf( + pred: Predicate, +): Predicate { + return rewriteName( + (x: unknown): x is T[] => isArray(x) && x.every(pred), + "isArrayOf", + pred, + ); +} diff --git a/is/array_of_test.ts b/is/array_of_test.ts new file mode 100644 index 0000000..2944a77 --- /dev/null +++ b/is/array_of_test.ts @@ -0,0 +1,36 @@ +import { assertEquals } from "@std/assert"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { is } from "./mod.ts"; +import { isArrayOf } from "./array_of.ts"; + +Deno.test("isArrayOf", async (t) => { + await t.step("returns properly named predicate function", () => { + assertEquals(typeof isArrayOf(is.Number), "function"); + assertEquals(isArrayOf(is.Number).name, "isArrayOf(isNumber)"); + assertEquals( + isArrayOf((_x): _x is unknown => true).name, + "isArrayOf((anonymous))", + ); + }); + + await t.step("returns true on T array", () => { + assertEquals(isArrayOf(is.Number)([0, 1, 2]), true); + assertEquals(isArrayOf(is.String)(["a", "b", "c"]), true); + assertEquals(isArrayOf(is.Boolean)([true, false, true]), true); + }); + + await t.step("returns false on non T array", () => { + assertEquals(isArrayOf(is.String)([0, 1, 2]), false); + assertEquals(isArrayOf(is.Number)(["a", "b", "c"]), false); + assertEquals(isArrayOf(is.String)([true, false, true]), false); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = undefined; + + if (isArrayOf(is.Number)(a)) { + assertType>(true); + } + }); +}); diff --git a/is/array_test.ts b/is/array_test.ts new file mode 100644 index 0000000..1994ab3 --- /dev/null +++ b/is/array_test.ts @@ -0,0 +1,6 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isArray } from "./array.ts"; + +Deno.test("isArray", async (t) => { + await testWithExamples(t, isArray, { validExamples: ["array"] }); +}); diff --git a/is/async_function.ts b/is/async_function.ts new file mode 100644 index 0000000..78db3eb --- /dev/null +++ b/is/async_function.ts @@ -0,0 +1,22 @@ +const objectToString = Object.prototype.toString; + +/** + * Return `true` if the type of `x` is `function` (async function). + * + * Use {@linkcode isFunction} to check if the type of `x` is a function. + * Use {@linkcode isSyncFunction} to check if the type of `x` is a synchronous function. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = async () => {}; + * if (is.AsyncFunction(a)) { + * const _: ((...args: unknown[]) => Promise) = a; + * } + * ``` + */ +export function isAsyncFunction( + x: unknown, +): x is (...args: unknown[]) => Promise { + return objectToString.call(x) === "[object AsyncFunction]"; +} diff --git a/is/async_function_test.ts b/is/async_function_test.ts new file mode 100644 index 0000000..7fbe8e7 --- /dev/null +++ b/is/async_function_test.ts @@ -0,0 +1,8 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isAsyncFunction } from "./async_function.ts"; + +Deno.test("isAsyncFunction", async (t) => { + await testWithExamples(t, isAsyncFunction, { + validExamples: ["asyncFunction"], + }); +}); diff --git a/is/bigint.ts b/is/bigint.ts new file mode 100644 index 0000000..60b68e0 --- /dev/null +++ b/is/bigint.ts @@ -0,0 +1,15 @@ +/** + * Return `true` if the type of `x` is `bigint`. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = 0n; + * if (is.Bigint(a)) { + * const _: bigint = a; + * } + * ``` + */ +export function isBigint(x: unknown): x is bigint { + return typeof x === "bigint"; +} diff --git a/is/bigint_test.ts b/is/bigint_test.ts new file mode 100644 index 0000000..8d1fa2e --- /dev/null +++ b/is/bigint_test.ts @@ -0,0 +1,6 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isBigint } from "./bigint.ts"; + +Deno.test("isBigint", async (t) => { + await testWithExamples(t, isBigint, { validExamples: ["bigint"] }); +}); diff --git a/is/boolean.ts b/is/boolean.ts new file mode 100644 index 0000000..4c36b54 --- /dev/null +++ b/is/boolean.ts @@ -0,0 +1,15 @@ +/** + * Return `true` if the type of `x` is `boolean`. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = true; + * if (is.Boolean(a)) { + * const _: boolean = a; + * } + * ``` + */ +export function isBoolean(x: unknown): x is boolean { + return typeof x === "boolean"; +} diff --git a/is/boolean_test.ts b/is/boolean_test.ts new file mode 100644 index 0000000..8f9a376 --- /dev/null +++ b/is/boolean_test.ts @@ -0,0 +1,6 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isBoolean } from "./boolean.ts"; + +Deno.test("isBoolean", async (t) => { + await testWithExamples(t, isBoolean, { validExamples: ["boolean"] }); +}); diff --git a/is/function.ts b/is/function.ts new file mode 100644 index 0000000..9b6cc71 --- /dev/null +++ b/is/function.ts @@ -0,0 +1,18 @@ +/** + * Return `true` if the type of `x` is `function`. + * + * Use {@linkcode isSyncFunction} to check if the type of `x` is a synchronous function. + * Use {@linkcode isAsyncFunction} to check if the type of `x` is an asynchronous function. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = () => {}; + * if (is.Function(a)) { + * const _: ((...args: unknown[]) => unknown) = a; + * } + * ``` + */ +export function isFunction(x: unknown): x is (...args: unknown[]) => unknown { + return x instanceof Function; +} diff --git a/is/function_test.ts b/is/function_test.ts new file mode 100644 index 0000000..cca305e --- /dev/null +++ b/is/function_test.ts @@ -0,0 +1,8 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isFunction } from "./function.ts"; + +Deno.test("isFunction", async (t) => { + await testWithExamples(t, isFunction, { + validExamples: ["syncFunction", "asyncFunction"], + }); +}); diff --git a/is/instance_of.ts b/is/instance_of.ts new file mode 100644 index 0000000..0af259e --- /dev/null +++ b/is/instance_of.ts @@ -0,0 +1,28 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { Predicate } from "../type.ts"; + +/** + * 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 "@core/unknownutil"; + * + * const isMyType = is.InstanceOf(Date); + * const a: unknown = new Date(); + * if (isMyType(a)) { + * const _: Date = a; + * } + * ``` + */ +// deno-lint-ignore no-explicit-any +export function isInstanceOf unknown>( + ctor: T, +): Predicate> { + return rewriteName( + (x: unknown): x is InstanceType => x instanceof ctor, + "isInstanceOf", + ctor, + ); +} diff --git a/is/instance_of_test.ts b/is/instance_of_test.ts new file mode 100644 index 0000000..a8fa635 --- /dev/null +++ b/is/instance_of_test.ts @@ -0,0 +1,43 @@ +import { assertEquals } from "@std/assert"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { isInstanceOf } from "./instance_of.ts"; + +Deno.test("isInstanceOf", async (t) => { + await t.step("returns properly named predicate function", () => { + assertEquals(typeof isInstanceOf(Date), "function"); + assertEquals(isInstanceOf(Date).name, "isInstanceOf(Date)"); + assertEquals(isInstanceOf(class {}).name, "isInstanceOf((anonymous))"); + }); + + await t.step("returns true on T instance", () => { + class Cls {} + assertEquals(isInstanceOf(Cls)(new Cls()), true); + assertEquals(isInstanceOf(Date)(new Date()), true); + assertEquals(isInstanceOf(Promise)(new Promise(() => {})), true); + }); + + await t.step("returns false on non T instance", () => { + class Cls {} + assertEquals(isInstanceOf(Date)(new Cls()), false); + assertEquals(isInstanceOf(Promise)(new Date()), false); + assertEquals(isInstanceOf(Cls)(new Promise(() => {})), false); + }); + + await t.step("predicated type is correct", () => { + class Cls {} + const a: unknown = undefined; + + if (isInstanceOf(Cls)(a)) { + assertType>(true); + } + + if (isInstanceOf(Date)(a)) { + assertType>(true); + } + + if (isInstanceOf(Promise)(a)) { + assertType>>(true); + } + }); +}); diff --git a/is/intersection_of.ts b/is/intersection_of.ts new file mode 100644 index 0000000..0b748d8 --- /dev/null +++ b/is/intersection_of.ts @@ -0,0 +1,102 @@ +import { rewriteName } from "../_funcutil.ts"; +import { hasAnnotation, type IsPredObj } from "../_annotation.ts"; +import type { Predicate } from "../type.ts"; +import { isObjectOf } from "./object_of.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `IntersectionOf`. + * + * Use {@linkcode isUnionOf} to check if the type of `x` is a union of `T`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.IntersectionOf([ + * is.ObjectOf({ a: is.Number }), + * is.ObjectOf({ b: is.String }), + * ]); + * const a: unknown = { a: 0, b: "a" }; + * if (isMyType(a)) { + * 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 "@core/unknownutil"; + * + * 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)) { + * const _: { a: number } & { b: string } = a; + * } + * ``` + */ +export function isIntersectionOf< + T extends readonly [ + Predicate & IsPredObj, + ...(Predicate & IsPredObj)[], + ], +>( + preds: T, +): + & Predicate> + & IsPredObj; +export function isIntersectionOf< + T extends readonly [Predicate], +>( + preds: T, +): T[0]; +export function isIntersectionOf< + T extends readonly [Predicate, ...Predicate[]], +>( + preds: T, +): + & Predicate> + & IsPredObj; +export function isIntersectionOf< + T extends readonly [Predicate, ...Predicate[]], +>( + preds: T, +): + | Predicate + | Predicate> + & IsPredObj { + const predObj = {}; + const restPreds = preds.filter((pred) => { + if (!hasAnnotation(pred, "predObj")) { + return true; + } + Object.assign(predObj, pred.predObj); + }); + if (restPreds.length < preds.length) { + restPreds.push(isObjectOf(predObj)); + } + if (restPreds.length === 1) { + return restPreds[0]; + } + return rewriteName( + (x: unknown): x is IntersectionOf => restPreds.every((pred) => pred(x)), + "isIntersectionOf", + preds, + ); +} + +type TupleToIntersection = T extends readonly [] ? never + : T extends readonly [infer U] ? U + : T extends readonly [infer U, ...infer R] ? U & TupleToIntersection + : never; + +type IntersectionOf = TupleToIntersection< + { + -readonly [P in keyof T]: T[P] extends Predicate ? U : never; + } +>; diff --git a/is/intersection_of_test.ts b/is/intersection_of_test.ts new file mode 100644 index 0000000..a22c4f7 --- /dev/null +++ b/is/intersection_of_test.ts @@ -0,0 +1,80 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { is } from "./mod.ts"; +import { isIntersectionOf } from "./intersection_of.ts"; + +Deno.test("isIntersectionOf", async (t) => { + const objPreds = [ + is.ObjectOf({ a: is.Number }), + is.ObjectOf({ b: is.String }), + ] as const; + const mixPreds = [ + is.Function, + is.ObjectOf({ b: is.String }), + ] as const; + + await t.step("returns properly named predicate function", async (t) => { + assertEquals(typeof isIntersectionOf([is.String]), "function"); + await assertSnapshot(t, isIntersectionOf([is.String]).name); + await assertSnapshot(t, isIntersectionOf(objPreds).name); + await assertSnapshot(t, isIntersectionOf(mixPreds).name); + }); + + await t.step("returns true on all of T", () => { + const f = Object.assign(() => void 0, { b: "a" }); + assertEquals(isIntersectionOf([is.String])("a"), true); + assertEquals(isIntersectionOf(objPreds)({ a: 0, b: "a" }), true); + assertEquals(isIntersectionOf(mixPreds)(f), true); + }); + + await t.step("returns false on non of T", () => { + const f = Object.assign(() => void 0, { b: "a" }); + assertEquals(isIntersectionOf(objPreds)("a"), false); + assertEquals(isIntersectionOf(mixPreds)({ a: 0, b: "a" }), false); + assertEquals(isIntersectionOf([is.String])(f), false); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = undefined; + + if (isIntersectionOf([is.String])(a)) { + assertType>(true); + } + + if (isIntersectionOf(objPreds)(a)) { + assertType>(true); + } + + if (isIntersectionOf(mixPreds)(a)) { + assertType< + Equal< + typeof a, + & ((...args: unknown[]) => unknown) + & { b: string } + > + >(true); + } + }); + + await t.step("predicated type is correct (#68)", () => { + const a: unknown = undefined; + const pred = isIntersectionOf([ + is.ObjectOf({ id: is.String }), + is.UnionOf([ + is.ObjectOf({ result: is.String }), + is.ObjectOf({ error: is.String }), + ]), + ]); + + if (pred(a)) { + assertType< + Equal< + typeof a, + { id: string } & ({ result: string } | { error: string }) + > + >(true); + } + }); +}); diff --git a/is/literal_of.ts b/is/literal_of.ts new file mode 100644 index 0000000..1388675 --- /dev/null +++ b/is/literal_of.ts @@ -0,0 +1,30 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { Predicate, Primitive } from "../type.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is a literal type of `pred`. + * + * Use {@linkcode isLiteral} to check if the type of `x` is a literal type. + * Use {@linkcode isLiteralOneOf} to check if the type of `x` is one of the literal type of `Primitive[]`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.LiteralOf("hello"); + * const a: unknown = "hello"; + * if (isMyType(a)) { + * const _: "hello" = a; + * } + * ``` + */ +export function isLiteralOf( + literal: T, +): Predicate { + return rewriteName( + (x: unknown): x is T => x === literal, + "isLiteralOf", + literal, + ); +} diff --git a/is/literal_of_test.ts b/is/literal_of_test.ts new file mode 100644 index 0000000..f8a9b75 --- /dev/null +++ b/is/literal_of_test.ts @@ -0,0 +1,79 @@ +import { assertEquals } from "@std/assert"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { isLiteralOf } from "./literal_of.ts"; + +Deno.test("isLiteralOf", async (t) => { + await t.step("returns properly named predicate function", () => { + assertEquals(typeof isLiteralOf("hello"), "function"); + assertEquals(isLiteralOf("hello").name, `isLiteralOf("hello")`); + assertEquals(isLiteralOf(100).name, `isLiteralOf(100)`); + assertEquals(isLiteralOf(100n).name, `isLiteralOf(100n)`); + assertEquals(isLiteralOf(true).name, `isLiteralOf(true)`); + assertEquals(isLiteralOf(false).name, `isLiteralOf(false)`); + assertEquals(isLiteralOf(null).name, `isLiteralOf(null)`); + assertEquals(isLiteralOf(undefined).name, `isLiteralOf(undefined)`); + assertEquals(isLiteralOf(Symbol("asdf")).name, `isLiteralOf(Symbol(asdf))`); + }); + + await t.step("returns true on literal T", () => { + const s = Symbol("asdf"); + assertEquals(isLiteralOf("hello")("hello"), true); + assertEquals(isLiteralOf(100)(100), true); + assertEquals(isLiteralOf(100n)(100n), true); + assertEquals(isLiteralOf(true)(true), true); + assertEquals(isLiteralOf(false)(false), true); + assertEquals(isLiteralOf(null)(null), true); + assertEquals(isLiteralOf(undefined)(undefined), true); + assertEquals(isLiteralOf(s)(s), true); + }); + + await t.step("returns false on non literal T", () => { + const s = Symbol("asdf"); + assertEquals(isLiteralOf(100)("hello"), false); + assertEquals(isLiteralOf(100n)(100), false); + assertEquals(isLiteralOf(true)(100n), false); + assertEquals(isLiteralOf(false)(true), false); + assertEquals(isLiteralOf(null)(false), false); + assertEquals(isLiteralOf(undefined)(null), false); + assertEquals(isLiteralOf(s)(undefined), false); + assertEquals(isLiteralOf("hello")(s), false); + }); + + await t.step("predicated type is correct", () => { + const s = Symbol("asdf"); + const a: unknown = undefined; + + if (isLiteralOf("hello")(a)) { + assertType>(true); + } + + if (isLiteralOf(100)(a)) { + assertType>(true); + } + + if (isLiteralOf(100n)(a)) { + assertType>(true); + } + + if (isLiteralOf(true)(a)) { + assertType>(true); + } + + if (isLiteralOf(false)(a)) { + assertType>(true); + } + + if (isLiteralOf(null)(a)) { + assertType>(true); + } + + if (isLiteralOf(undefined)(a)) { + assertType>(true); + } + + if (isLiteralOf(s)(a)) { + assertType>(true); + } + }); +}); diff --git a/is/literal_one_of.ts b/is/literal_one_of.ts new file mode 100644 index 0000000..b60547b --- /dev/null +++ b/is/literal_one_of.ts @@ -0,0 +1,31 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { Predicate, Primitive } from "../type.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is one of literal type in `preds`. + * + * Use {@linkcode isLiteral} to check if the type of `x` is a literal type. + * Use {@linkcode isLiteralOf} to check if the type of `x` is a literal type of `Primitive`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.LiteralOneOf(["hello", "world"] as const); + * const a: unknown = "hello"; + * if (isMyType(a)) { + * const _: "hello" | "world" = a; + * } + * ``` + */ +export function isLiteralOneOf( + literals: T, +): Predicate { + const s = new Set(literals); + return rewriteName( + (x: unknown): x is T[number] => s.has(x as T[number]), + "isLiteralOneOf", + literals, + ); +} diff --git a/is/literal_one_of_test.ts b/is/literal_one_of_test.ts new file mode 100644 index 0000000..ecea097 --- /dev/null +++ b/is/literal_one_of_test.ts @@ -0,0 +1,32 @@ +import { assertEquals } from "@std/assert"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { isLiteralOneOf } from "./literal_one_of.ts"; + +Deno.test("isLiteralOneOf", async (t) => { + const literals = ["hello", "world"] as const; + await t.step("returns properly named predicate function", () => { + assertEquals(typeof isLiteralOneOf(literals), "function"); + assertEquals( + isLiteralOneOf(literals).name, + `isLiteralOneOf(["hello", "world"])`, + ); + }); + + await t.step("returns true on literal T", () => { + assertEquals(isLiteralOneOf(literals)("hello"), true); + assertEquals(isLiteralOneOf(literals)("world"), true); + }); + + await t.step("returns false on non literal T", () => { + assertEquals(isLiteralOneOf(literals)(""), false); + assertEquals(isLiteralOneOf(literals)(100), false); + }); + + await t.step("returns proper type predicate", () => { + const a: unknown = "hello"; + if (isLiteralOneOf(literals)(a)) { + assertType>(true); + } + }); +}); diff --git a/is/map.ts b/is/map.ts new file mode 100644 index 0000000..545a993 --- /dev/null +++ b/is/map.ts @@ -0,0 +1,17 @@ +/** + * Return `true` if the type of `x` is `Map`. + * + * Use {@linkcode isMapOf} to check if the type of `x` is a map of `T`. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = new Map([["a", 0], ["b", 1]]); + * if (is.Map(a)) { + * const _: Map = a; + * } + * ``` + */ +export function isMap(x: unknown): x is Map { + return x instanceof Map; +} diff --git a/is/map_of.ts b/is/map_of.ts new file mode 100644 index 0000000..cf4bc68 --- /dev/null +++ b/is/map_of.ts @@ -0,0 +1,51 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { Predicate } from "../type.ts"; +import { isMap } from "./map.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `Map`. + * + * Use {@linkcode isMap} to check if the type of `x` is a map of `unknown`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.MapOf(is.Number); + * const a: unknown = new Map([["a", 0], ["b", 1]]); + * if (isMyType(a)) { + * const _: Map = a; + * } + * ``` + * + * With predicate function for keys: + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.MapOf(is.Number, is.String); + * const a: unknown = new Map([["a", 0], ["b", 1]]); + * if (isMyType(a)) { + * const _: Map = a; + * } + * ``` + */ +export function isMapOf( + pred: Predicate, + predKey?: Predicate, +): Predicate> { + return rewriteName( + (x: unknown): x is Map => { + if (!isMap(x)) return false; + for (const [k, v] of x.entries()) { + if (!pred(v)) return false; + if (predKey && !predKey(k)) return false; + } + return true; + }, + "isMapOf", + pred, + predKey, + ); +} diff --git a/is/map_of_test.ts b/is/map_of_test.ts new file mode 100644 index 0000000..2ebb16f --- /dev/null +++ b/is/map_of_test.ts @@ -0,0 +1,74 @@ +import { assertEquals } from "@std/assert"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { is } from "./mod.ts"; +import { isMapOf } from "./map_of.ts"; + +Deno.test("isMapOf", async (t) => { + await t.step("returns properly named predicate function", () => { + assertEquals(typeof isMapOf(is.Number), "function"); + assertEquals(isMapOf(is.Number).name, "isMapOf(isNumber, undefined)"); + assertEquals( + isMapOf((_x): _x is unknown => true).name, + "isMapOf((anonymous), undefined)", + ); + }); + + await t.step("returns true on T map", () => { + assertEquals(isMapOf(is.Number)(new Map([["a", 0]])), true); + assertEquals(isMapOf(is.String)(new Map([["a", "a"]])), true); + assertEquals(isMapOf(is.Boolean)(new Map([["a", true]])), true); + }); + + await t.step("returns false on non T map", () => { + assertEquals(isMapOf(is.String)(new Map([["a", 0]])), false); + assertEquals(isMapOf(is.Number)(new Map([["a", "a"]])), false); + assertEquals(isMapOf(is.String)(new Map([["a", true]])), false); + }); + + await t.step("returns proper type predicate", () => { + const a: unknown = undefined; + if (isMapOf(is.Number)(a)) { + assertType>>(true); + } + }); +}); + +Deno.test("isMapOf", async (t) => { + await t.step("returns properly named predicate function", () => { + assertEquals(typeof isMapOf(is.Number, is.String), "function"); + assertEquals( + isMapOf(is.Number, is.String).name, + "isMapOf(isNumber, isString)", + ); + assertEquals( + isMapOf((_x): _x is unknown => true, is.String).name, + "isMapOf((anonymous), isString)", + ); + }); + + await t.step("returns true on T map", () => { + assertEquals(isMapOf(is.Number, is.String)(new Map([["a", 0]])), true); + assertEquals(isMapOf(is.String, is.String)(new Map([["a", "a"]])), true); + assertEquals(isMapOf(is.Boolean, is.String)(new Map([["a", true]])), true); + }); + + await t.step("returns false on non T map", () => { + assertEquals(isMapOf(is.String, is.String)(new Map([["a", 0]])), false); + assertEquals(isMapOf(is.Number, is.String)(new Map([["a", "a"]])), false); + assertEquals(isMapOf(is.String, is.String)(new Map([["a", true]])), false); + }); + + await t.step("returns false on non K map", () => { + assertEquals(isMapOf(is.Number, is.Number)(new Map([["a", 0]])), false); + assertEquals(isMapOf(is.String, is.Number)(new Map([["a", "a"]])), false); + assertEquals(isMapOf(is.Boolean, is.Number)(new Map([["a", true]])), false); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = new Map([["a", 0]]); + if (isMapOf(is.Number, is.String)(a)) { + assertType>>(true); + } + }); +}); diff --git a/is/map_test.ts b/is/map_test.ts new file mode 100644 index 0000000..b7e8af6 --- /dev/null +++ b/is/map_test.ts @@ -0,0 +1,8 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isMap } from "./map.ts"; + +Deno.test("isMap", async (t) => { + await testWithExamples(t, isMap, { + validExamples: ["map"], + }); +}); diff --git a/is/mod.ts b/is/mod.ts new file mode 100644 index 0000000..f99ba41 --- /dev/null +++ b/is/mod.ts @@ -0,0 +1,84 @@ +import { isAny } from "./any.ts"; +import { isArray } from "./array.ts"; +import { isArrayOf } from "./array_of.ts"; +import { isAsyncFunction } from "./async_function.ts"; +import { isBigint } from "./bigint.ts"; +import { isBoolean } from "./boolean.ts"; +import { isFunction } from "./function.ts"; +import { isInstanceOf } from "./instance_of.ts"; +import { isIntersectionOf } from "./intersection_of.ts"; +import { isLiteralOf } from "./literal_of.ts"; +import { isLiteralOneOf } from "./literal_one_of.ts"; +import { isMap } from "./map.ts"; +import { isMapOf } from "./map_of.ts"; +import { isNull } from "./null.ts"; +import { isNullish } from "./nullish.ts"; +import { isNumber } from "./number.ts"; +import { isObjectOf } from "./object_of.ts"; +import { isOmitOf } from "./omit_of.ts"; +import { isParametersOf } from "./parameters_of.ts"; +import { isPartialOf } from "./partial_of.ts"; +import { isPickOf } from "./pick_of.ts"; +import { isPrimitive } from "./primitive.ts"; +import { isReadonlyOf } from "./readonly_of.ts"; +import { isRecord } from "./record.ts"; +import { isRecordObject } from "./record_object.ts"; +import { isRecordObjectOf } from "./record_object_of.ts"; +import { isRecordOf } from "./record_of.ts"; +import { isRequiredOf } from "./required_of.ts"; +import { isSet } from "./set.ts"; +import { isSetOf } from "./set_of.ts"; +import { isStrictOf } from "./strict_of.ts"; +import { isString } from "./string.ts"; +import { isSymbol } from "./symbol.ts"; +import { isSyncFunction } from "./sync_function.ts"; +import { isTupleOf } from "./tuple_of.ts"; +import { isUndefined } from "./undefined.ts"; +import { isUniformTupleOf } from "./uniform_tuple_of.ts"; +import { isUnionOf } from "./union_of.ts"; +import { isUnknown } from "./unknown.ts"; + +/** + * Type predicate function collection. + */ +export const is = { + Any: isAny, + Array: isArray, + ArrayOf: isArrayOf, + AsyncFunction: isAsyncFunction, + Bigint: isBigint, + Boolean: isBoolean, + Function: isFunction, + InstanceOf: isInstanceOf, + IntersectionOf: isIntersectionOf, + LiteralOf: isLiteralOf, + LiteralOneOf: isLiteralOneOf, + Map: isMap, + MapOf: isMapOf, + Null: isNull, + Nullish: isNullish, + Number: isNumber, + ObjectOf: isObjectOf, + OmitOf: isOmitOf, + ParametersOf: isParametersOf, + PartialOf: isPartialOf, + PickOf: isPickOf, + Primitive: isPrimitive, + ReadonlyOf: isReadonlyOf, + Record: isRecord, + RecordObject: isRecordObject, + RecordObjectOf: isRecordObjectOf, + RecordOf: isRecordOf, + RequiredOf: isRequiredOf, + Set: isSet, + SetOf: isSetOf, + StrictOf: isStrictOf, + String: isString, + Symbol: isSymbol, + SyncFunction: isSyncFunction, + TupleOf: isTupleOf, + Undefined: isUndefined, + UniformTupleOf: isUniformTupleOf, + UnionOf: isUnionOf, + Unknown: isUnknown, +} as const; diff --git a/is/mod_test.ts b/is/mod_test.ts new file mode 100644 index 0000000..7276c9e --- /dev/null +++ b/is/mod_test.ts @@ -0,0 +1,33 @@ +import { assertEquals } from "@std/assert"; +import { globToRegExp } from "@std/path"; +import { is } from "./mod.ts"; + +const excludes = [ + "mod.ts", + "*_test.ts", +]; + +Deno.test("is", async (t) => { + const names = await listIsFunctions(); + await t.step( + "must have all `is*` function aliases as entries", + () => { + assertEquals(Object.keys(is).sort(), names); + }, + ); +}); + +async function listIsFunctions(): Promise { + const patterns = excludes.map((p) => globToRegExp(p)); + const names: string[] = []; + for await (const entry of Deno.readDir(import.meta.dirname!)) { + if (!entry.isFile || !entry.name.endsWith(".ts")) continue; + if (patterns.some((p) => p.test(entry.name))) continue; + const mod = await import(import.meta.resolve(`./${entry.name}`)); + const isFunctionNames = Object.entries(mod) + .filter(([k, _]) => k.startsWith("is")) + .map(([k, _]) => k.slice(2)); + names.push(...isFunctionNames); + } + return names.toSorted(); +} diff --git a/is/null.ts b/is/null.ts new file mode 100644 index 0000000..a4d8a15 --- /dev/null +++ b/is/null.ts @@ -0,0 +1,18 @@ +/** + * Return `true` if the type of `x` is `null`. + * + * Use {@linkcode isUndefined} to check if the type of `x` is `undefined`. + * Use {@linkcode isNullish} to check if the type of `x` is `null` or `undefined`. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = null; + * if (is.Null(a)) { + * const _: null = a; + * } + * ``` + */ +export function isNull(x: unknown): x is null { + return x === null; +} diff --git a/is/null_test.ts b/is/null_test.ts new file mode 100644 index 0000000..aa6ddb5 --- /dev/null +++ b/is/null_test.ts @@ -0,0 +1,6 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isNull } from "./null.ts"; + +Deno.test("isNull", async (t) => { + await testWithExamples(t, isNull, { validExamples: ["null"] }); +}); diff --git a/is/nullish.ts b/is/nullish.ts new file mode 100644 index 0000000..d462fba --- /dev/null +++ b/is/nullish.ts @@ -0,0 +1,18 @@ +/** + * Return `true` if the type of `x` is `null` or `undefined`. + * + * Use {@linkcode isNull} to check if the type of `x` is `null`. + * Use {@linkcode isUndefined} to check if the type of `x` is `undefined`. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = null; + * if (is.Nullish(a)) { + * const _: (null | undefined) = a; + * } + * ``` + */ +export function isNullish(x: unknown): x is null | undefined { + return x == null; +} diff --git a/is/nullish_test.ts b/is/nullish_test.ts new file mode 100644 index 0000000..a383654 --- /dev/null +++ b/is/nullish_test.ts @@ -0,0 +1,8 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isNullish } from "./nullish.ts"; + +Deno.test("isNullish", async (t) => { + await testWithExamples(t, isNullish, { + validExamples: ["null", "undefined"], + }); +}); diff --git a/is/number.ts b/is/number.ts new file mode 100644 index 0000000..30a2443 --- /dev/null +++ b/is/number.ts @@ -0,0 +1,15 @@ +/** + * Return `true` if the type of `x` is `number`. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = 0; + * if (is.Number(a)) { + * const _: number = a; + * } + * ``` + */ +export function isNumber(x: unknown): x is number { + return typeof x === "number"; +} diff --git a/is/number_test.ts b/is/number_test.ts new file mode 100644 index 0000000..a81c129 --- /dev/null +++ b/is/number_test.ts @@ -0,0 +1,6 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isNumber } from "./number.ts"; + +Deno.test("isNumber", async (t) => { + await testWithExamples(t, isNumber, { validExamples: ["number"] }); +}); diff --git a/is/object_of.ts b/is/object_of.ts new file mode 100644 index 0000000..6a7633c --- /dev/null +++ b/is/object_of.ts @@ -0,0 +1,97 @@ +import type { FlatType } from "../_typeutil.ts"; +import { rewriteName } from "../_funcutil.ts"; +import { + annotate, + type AsOptional, + type AsReadonly, + type IsPredObj, +} from "../_annotation.ts"; +import type { Predicate } from "../type.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `ObjectOf`. + * + * Use {@linkcode isRecordOf} if you want to check if the type of `x` is a record of `T`. + * + * If {@linkcode asOptional} is specified in the predicate function in `predObj`, the property becomes optional. + * If {@linkcode asReadonly} is specified in the predicate function in `predObj`, the property becomes readonly. + * + * The number of keys of `x` must be greater than or equal to the number of keys of `predObj`. + * Use {@linkcode isStrictOf} if you want to check the exact number of keys. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { as, is } from "@core/unknownutil"; + * + * const isMyType = is.ObjectOf({ + * a: is.Number, + * b: is.String, + * c: as.Optional(is.Boolean), + * d: as.Readonly(is.String), + * }); + * const a: unknown = { a: 0, b: "a", d: "d" }; + * if (isMyType(a)) { + * const _: { a: number; b: string; c?: boolean | undefined, readonly d: string } = a; + * } + * ``` + */ +export function isObjectOf< + T extends Record>, +>(predObj: T): Predicate> & IsPredObj { + return annotate( + rewriteName( + (x: unknown): x is ObjectOf => { + if ( + x == null || + typeof x !== "object" && typeof x !== "function" || + Array.isArray(x) + ) return false; + // Check each values + for (const k in predObj) { + if (!predObj[k]((x as T)[k])) return false; + } + return true; + }, + "isObjectOf", + predObj, + ), + "predObj", + predObj, + ); +} + +type ObjectOf>> = FlatType< + // Readonly/Optional + & { + readonly [ + K in keyof T as T[K] extends AsReadonly + ? T[K] extends AsOptional ? K : never + : never + ]?: T[K] extends Predicate ? U : never; + } + // Readonly/Non optional + & { + readonly [ + K in keyof T as T[K] extends AsReadonly + ? T[K] extends AsOptional ? never : K + : never + ]: T[K] extends Predicate ? U : never; + } + // Non readonly/Optional + & { + [ + K in keyof T as T[K] extends AsReadonly ? never + : T[K] extends AsOptional ? K + : never + ]?: T[K] extends Predicate ? U : never; + } + // Non readonly/Non optional + & { + [ + K in keyof T as T[K] extends AsReadonly ? never + : T[K] extends AsOptional ? never + : K + ]: T[K] extends Predicate ? U : never; + } +>; diff --git a/is/object_of_test.ts b/is/object_of_test.ts new file mode 100644 index 0000000..7379377 --- /dev/null +++ b/is/object_of_test.ts @@ -0,0 +1,101 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { is } from "./mod.ts"; +import { as } from "../as/mod.ts"; +import { isObjectOf } from "./object_of.ts"; + +Deno.test("isObjectOf", async (t) => { + const predObj = { + a: is.Number, + b: is.String, + c: is.Boolean, + }; + + await t.step("returns properly named predicate function", async (t) => { + assertEquals(typeof isObjectOf({}), "function"); + await assertSnapshot(t, isObjectOf(predObj).name); + await assertSnapshot( + t, + isObjectOf({ a: (_x): _x is string => false }).name, + ); + await assertSnapshot( + t, + isObjectOf({ a: isObjectOf({ b: isObjectOf({ c: is.Boolean }) }) }).name, + ); + }); + + await t.step("returns true on T object", () => { + assertEquals(isObjectOf(predObj)({ a: 0, b: "a", c: true }), true); + assertEquals( + isObjectOf(predObj)({ a: 0, b: "a", c: true, d: "ignored" }), + true, + ); + assertEquals( + isObjectOf(predObj)( + Object.assign(() => void 0, { a: 0, b: "a", c: true }), + ), + true, + ); + }); + + await t.step("returns false on non T object", () => { + assertEquals(isObjectOf(predObj)("a"), false, "Value is not an object"); + assertEquals( + isObjectOf(predObj)({ a: 0, b: "a", c: "" }), + false, + "Object have a different type property", + ); + assertEquals( + isObjectOf(predObj)({ a: 0, b: "a" }), + false, + "Object does not have one property", + ); + assertEquals( + isObjectOf({ 0: is.String })(["a"]), + false, + "Value is not an object", + ); + }); + + await t.step("returns true on T instance", () => { + const date = new Date(); + const predObj = { + getFullYear: is.Function, + }; + assertEquals(isObjectOf(predObj)(date), true, "Value is not an object"); + }); + + await t.step("predicated type is correct", () => { + const predObj2 = { + a: as.Readonly(as.Optional(is.String)), + b: as.Optional(as.Readonly(is.String)), + c: as.Readonly(is.String), + d: as.Optional(is.String), + e: as.Unreadonly(as.Unoptional(as.Readonly(as.Optional(is.String)))), + f: as.Unoptional(as.Unreadonly(as.Optional(as.Readonly(is.String)))), + }; + const a: unknown = { a: 0, b: "a", c: true }; + + if (isObjectOf(predObj)(a)) { + assertType>(true); + } + + if (isObjectOf(predObj2)(a)) { + assertType< + Equal< + typeof a, + { + readonly a?: string; + readonly b?: string; + readonly c: string; + d?: string; + e: string; + f: string; + } + > + >(true); + } + }); +}); diff --git a/is/omit_of.ts b/is/omit_of.ts new file mode 100644 index 0000000..6ef562d --- /dev/null +++ b/is/omit_of.ts @@ -0,0 +1,53 @@ +import type { FlatType } from "../_typeutil.ts"; +import type { IsPredObj } from "../_annotation.ts"; +import type { Predicate } from "../type.ts"; +import { isObjectOf } from "./object_of.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `Omit, K>`. + * + * It only supports modifing a predicate function annotated with `IsPredObj`, usually returned by the followings + * + * - {@linkcode isIntersectionOf} + * - {@linkcode isObjectOf} + * - {@linkcode isOmitOf} + * - {@linkcode isPartialOf} + * - {@linkcode isPickOf} + * - {@linkcode isReadonlyOf} + * - {@linkcode isRequiredOf} + * - {@linkcode isStrictOf} + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```typescript + * import { as, is } from "@core/unknownutil"; + * + * const isMyType = is.OmitOf(is.ObjectOf({ + * a: is.Number, + * b: is.String, + * c: as.Optional(is.Boolean), + * }), ["a", "c"]); + * const a: unknown = { a: 0, b: "a" }; + * if (isMyType(a)) { + * const _: { b: string } = a; + * } + * ``` + */ +export function isOmitOf< + T extends Record, + P extends Record>, + K extends keyof T, +>( + pred: Predicate & IsPredObj

, + keys: K[], +): + & Predicate>> + & IsPredObj

{ + const s = new Set(keys); + const predObj = Object.fromEntries( + Object.entries(pred.predObj).filter(([k]) => !s.has(k as K)), + ); + return isObjectOf(predObj) as + & Predicate>> + & IsPredObj

; +} diff --git a/is/omit_of_test.ts b/is/omit_of_test.ts new file mode 100644 index 0000000..107d4f2 --- /dev/null +++ b/is/omit_of_test.ts @@ -0,0 +1,57 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { is } from "./mod.ts"; +import { isOmitOf } from "./omit_of.ts"; + +Deno.test("isOmitOf", async (t) => { + const pred = is.ObjectOf({ + a: is.Number, + b: is.String, + c: is.Boolean, + }); + + await t.step("returns properly named predicate function", async (t) => { + assertEquals(typeof isOmitOf(pred, ["b"]), "function"); + await assertSnapshot(t, isOmitOf(pred, ["b"]).name); + await assertSnapshot(t, isOmitOf(isOmitOf(pred, ["b"]), ["c"]).name); + }); + + 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", + ); + }); + + await t.step("predicated type is correct", () => { + 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); + } + }); +}); diff --git a/is/parameters_of.ts b/is/parameters_of.ts new file mode 100644 index 0000000..819d95c --- /dev/null +++ b/is/parameters_of.ts @@ -0,0 +1,128 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { AsOptional } from "../_annotation.ts"; +import { hasOptional } from "../as/optional.ts"; +import type { Predicate, PredicateType } from "../type.ts"; +import { isArray } from "./array.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `ParametersOf` or `ParametersOf`. + * + * This is similar to {@linkcode isTupleOf}, but if {@linkcode asOptional} is specified at the trailing, the trailing elements becomes optional and makes variable-length tuple. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { as, is } from "@core/unknownutil"; + * + * const isMyType = is.ParametersOf([ + * is.Number, + * as.Optional(is.String), + * is.Boolean, + * as.Optional(is.Number), + * as.Optional(is.String), + * as.Optional(is.Boolean), + * ] as const); + * const a: unknown = [0, undefined, "a"]; + * if (isMyType(a)) { + * const _: [number, string | undefined, boolean, number?, string?, boolean?] = a; + * } + * ``` + * + * With `predElse`: + * + * ```ts + * import { as, is } from "@core/unknownutil"; + * + * const isMyType = is.ParametersOf( + * [ + * is.Number, + * as.Optional(is.String), + * as.Optional(is.Boolean), + * ] as const, + * is.ArrayOf(is.Number), + * ); + * const a: unknown = [0, "a", true, 0, 1, 2]; + * if (isMyType(a)) { + * 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 { as, is } from "@core/unknownutil"; + * + * const predTup = [is.Number, is.String, as.Optional(is.Boolean)] as const; + * const isMyType = is.ParametersOf(predTup); + * const a: unknown = [0, "a"]; + * if (isMyType(a)) { + * const _: [number, string, boolean?] = a; + * } + * ``` + */ +export function isParametersOf< + T extends readonly [...Predicate[]], +>( + predTup: T, +): Predicate>; +export function isParametersOf< + T extends readonly [...Predicate[]], + E extends Predicate, +>( + predTup: T, + predElse: E, +): Predicate<[...ParametersOf, ...PredicateType]>; +export function isParametersOf< + T extends readonly [...Predicate[]], + E extends Predicate, +>( + predTup: T, + predElse?: E, +): Predicate | [...ParametersOf, ...PredicateType]> { + const requiresLength = 1 + + predTup.findLastIndex((pred) => !hasOptional(pred)); + if (!predElse) { + return rewriteName( + (x: unknown): x is ParametersOf => { + if ( + !isArray(x) || x.length < requiresLength || x.length > predTup.length + ) { + return false; + } + return predTup.every((pred, i) => pred(x[i])); + }, + "isParametersOf", + predTup, + ); + } else { + return rewriteName( + (x: unknown): x is [...ParametersOf, ...PredicateType] => { + if (!isArray(x) || x.length < requiresLength) { + 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); + }, + "isParametersOf", + predTup, + predElse, + ); + } +} + +type ParametersOf = T extends readonly [] ? [] + : T extends readonly [...infer P, infer R] + // Tuple of predicates + ? P extends Predicate[] + ? R extends Predicate & AsOptional + // Last parameter is optional + ? [...ParametersOf

, PredicateType?] + // Last parameter is NOT optional + : [...ParametersOf

, PredicateType] + : never + // Array of predicates + : { + -readonly [P in keyof T]: T[P] extends Predicate ? U : never; + }; diff --git a/is/parameters_of_test.ts b/is/parameters_of_test.ts new file mode 100644 index 0000000..6bae4b2 --- /dev/null +++ b/is/parameters_of_test.ts @@ -0,0 +1,143 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { as } from "../as/mod.ts"; +import { is } from "./mod.ts"; +import { isParametersOf } from "./parameters_of.ts"; + +Deno.test("isParametersOf", async (t) => { + await t.step("returns properly named predicate function", async (t) => { + assertEquals(typeof isParametersOf([]), "function"); + await assertSnapshot( + t, + isParametersOf([is.Number, is.String, as.Optional(is.Boolean)]).name, + ); + await assertSnapshot( + t, + isParametersOf([(_x): _x is string => false]).name, + ); + await assertSnapshot( + t, + isParametersOf([]).name, + ); + await assertSnapshot( + t, + isParametersOf([ + isParametersOf([ + isParametersOf([is.Number, is.String, as.Optional(is.Boolean)]), + ]), + ]).name, + ); + }); + + await t.step("returns true on T tuple", () => { + const predTup = [is.Number, is.String, as.Optional(is.Boolean)] as const; + assertEquals(isParametersOf(predTup)([0, "a", true]), true); + assertEquals(isParametersOf(predTup)([0, "a"]), true); + }); + + await t.step("returns false on non T tuple", () => { + const predTup = [is.Number, is.String, as.Optional(is.Boolean)] as const; + assertEquals(isParametersOf(predTup)([0, 1, 2]), false); + assertEquals(isParametersOf(predTup)([0, "a", true, 0]), false); + }); + + await t.step("predicated type is correct", () => { + const predTup = [ + as.Optional(is.Number), + is.String, + as.Optional(is.String), + as.Optional(is.Boolean), + ] as const; + const a: unknown = [0, "a"]; + if (isParametersOf(predTup)(a)) { + assertType< + Equal + >(true); + } + }); +}); + +Deno.test("isParametersOf", async (t) => { + await t.step("returns properly named predicate function", async (t) => { + assertEquals(typeof isParametersOf([], is.Array), "function"); + await assertSnapshot( + t, + isParametersOf([is.Number, is.String, as.Optional(is.Boolean)], is.Array) + .name, + ); + await assertSnapshot( + t, + isParametersOf([(_x): _x is string => false], is.ArrayOf(is.String)) + .name, + ); + await assertSnapshot( + t, + isParametersOf([], is.ArrayOf(is.String)).name, + ); + await assertSnapshot( + t, + isParametersOf([ + isParametersOf( + [isParametersOf( + [is.Number, is.String, as.Optional(is.Boolean)], + is.Array, + )], + is.Array, + ), + ]).name, + ); + }); + + await t.step("returns true on T tuple", () => { + const predTup = [is.Number, is.String, as.Optional(is.Boolean)] as const; + const predElse = is.ArrayOf(is.Number); + assertEquals( + isParametersOf(predTup, predElse)([0, "a", true, 0, 1, 2]), + true, + ); + assertEquals( + isParametersOf(predTup, predElse)([0, "a", undefined, 0, 1, 2]), + true, + ); + assertEquals(isParametersOf(predTup, predElse)([0, "a"]), true); + }); + + await t.step("returns false on non T tuple", () => { + const predTup = [is.Number, is.String, as.Optional(is.Boolean)] as const; + const predElse = is.ArrayOf(is.String); + assertEquals(isParametersOf(predTup, predElse)([0, 1, 2, 0, 1, 2]), false); + assertEquals(isParametersOf(predTup, predElse)([0, "a", 0, 1, 2]), false); + assertEquals( + isParametersOf(predTup, predElse)([0, "a", true, 0, 1, 2]), + false, + ); + assertEquals( + isParametersOf(predTup, predElse)([0, "a", undefined, 0, 1, 2]), + false, + ); + assertEquals(isParametersOf(predTup, predElse)([0, "a", "b"]), false); + }); + + await t.step("predicated type is correct", () => { + const predTup = [ + as.Optional(is.Number), + is.String, + as.Optional(is.String), + as.Optional(is.Boolean), + ] as const; + const predElse = is.ArrayOf(is.Number); + const a: unknown = [0, "a"]; + if (isParametersOf(predTup, predElse)(a)) { + assertType< + Equal< + typeof a, + [number | undefined, string, string?, boolean?, ...number[]] + > + >( + true, + ); + } + }); +}); diff --git a/is/partial_of.ts b/is/partial_of.ts new file mode 100644 index 0000000..440db32 --- /dev/null +++ b/is/partial_of.ts @@ -0,0 +1,51 @@ +import type { FlatType } from "../_typeutil.ts"; +import type { IsPredObj } from "../_annotation.ts"; +import { asOptional } from "../as/optional.ts"; +import type { Predicate } from "../type.ts"; +import { isObjectOf } from "./object_of.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `Partial>`. + * + * It only supports modifing a predicate function annotated with `IsPredObj`, usually returned by the followings + * + * - {@linkcode isIntersectionOf} + * - {@linkcode isObjectOf} + * - {@linkcode isOmitOf} + * - {@linkcode isPartialOf} + * - {@linkcode isPickOf} + * - {@linkcode isReadonlyOf} + * - {@linkcode isRequiredOf} + * - {@linkcode isStrictOf} + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```typescript + * import { as, is } from "@core/unknownutil"; + * + * const isMyType = is.PartialOf(is.ObjectOf({ + * a: is.Number, + * b: is.UnionOf([is.String, is.Undefined]), + * c: as.Optional(is.Boolean), + * })); + * const a: unknown = { a: undefined, other: "other" }; + * if (isMyType(a)) { + * const _: { a?: number | undefined; b?: string | undefined; c?: boolean | undefined } = a; + * } + * ``` + */ +export function isPartialOf< + T extends Record, + P extends Record>, +>( + pred: Predicate & IsPredObj

, +): + & Predicate>> + & IsPredObj

{ + const predObj = Object.fromEntries( + Object.entries(pred.predObj).map(([k, v]) => [k, asOptional(v)]), + ) as Record>; + return isObjectOf(predObj) as + & Predicate>> + & IsPredObj

; +} diff --git a/is/partial_of_test.ts b/is/partial_of_test.ts new file mode 100644 index 0000000..de0a584 --- /dev/null +++ b/is/partial_of_test.ts @@ -0,0 +1,50 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { as } from "../as/mod.ts"; +import { is } from "./mod.ts"; +import { isPartialOf } from "./partial_of.ts"; + +Deno.test("isPartialOf", async (t) => { + const pred = is.ObjectOf({ + a: is.Number, + b: is.UnionOf([is.String, is.Undefined]), + c: as.Optional(is.Boolean), + d: as.Readonly(is.String), + }); + + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isPartialOf(pred).name); + await assertSnapshot(t, isPartialOf(isPartialOf(pred)).name); + }); + + 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", + ); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0, b: "a", c: true }; + if (isPartialOf(pred)(a)) { + assertType< + Equal< + typeof a, + Partial<{ a: number; b: string; c: boolean; readonly d: string }> + > + >(true); + } + }); +}); diff --git a/is/pick_of.ts b/is/pick_of.ts new file mode 100644 index 0000000..e605ceb --- /dev/null +++ b/is/pick_of.ts @@ -0,0 +1,53 @@ +import type { FlatType } from "../_typeutil.ts"; +import type { IsPredObj } from "../_annotation.ts"; +import type { Predicate } from "../type.ts"; +import { isObjectOf } from "./object_of.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `Pick, K>`. + * + * It only supports modifing a predicate function annotated with `IsPredObj`, usually returned by the followings + * + * - {@linkcode isIntersectionOf} + * - {@linkcode isObjectOf} + * - {@linkcode isOmitOf} + * - {@linkcode isPartialOf} + * - {@linkcode isPickOf} + * - {@linkcode isReadonlyOf} + * - {@linkcode isRequiredOf} + * - {@linkcode isStrictOf} + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```typescript + * import { as, is } from "@core/unknownutil"; + * + * const isMyType = is.PickOf(is.ObjectOf({ + * a: is.Number, + * b: is.String, + * c: as.Optional(is.Boolean), + * }), ["a", "c"]); + * const a: unknown = { a: 0, b: "a" }; + * if (isMyType(a)) { + * const _: { a: number; c?: boolean | undefined } = a; + * } + * ``` + */ +export function isPickOf< + T extends Record, + P extends Record>, + K extends keyof T, +>( + pred: Predicate & IsPredObj

, + keys: K[], +): + & Predicate>> + & IsPredObj

{ + const s = new Set(keys); + const predObj = Object.fromEntries( + Object.entries(pred.predObj).filter(([k]) => s.has(k as K)), + ); + return isObjectOf(predObj) as + & Predicate>> + & IsPredObj

; +} diff --git a/is/pick_of_test.ts b/is/pick_of_test.ts new file mode 100644 index 0000000..9f79564 --- /dev/null +++ b/is/pick_of_test.ts @@ -0,0 +1,54 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { is } from "./mod.ts"; +import { isPickOf } from "./pick_of.ts"; + +Deno.test("isPickOf", async (t) => { + const pred = is.ObjectOf({ + a: is.Number, + b: is.String, + c: is.Boolean, + }); + + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isPickOf(pred, ["a", "c"]).name); + await assertSnapshot(t, isPickOf(isPickOf(pred, ["a", "c"]), ["a"]).name); + }); + + 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", + ); + }); + + await t.step("predicated type is correct", () => { + 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); + } + }); +}); diff --git a/is/primitive.ts b/is/primitive.ts new file mode 100644 index 0000000..4714ba1 --- /dev/null +++ b/is/primitive.ts @@ -0,0 +1,25 @@ +import type { Primitive } from "../type.ts"; + +const primitiveSet: Set = new Set([ + "string", + "number", + "bigint", + "boolean", + "symbol", +]); + +/** + * Return `true` if the type of `x` is `Primitive`. + * + * ```ts + * import { is, type Primitive } from "@core/unknownutil"; + * + * const a: unknown = 0; + * if (is.Primitive(a)) { + * const _: Primitive = a; + * } + * ``` + */ +export function isPrimitive(x: unknown): x is Primitive { + return x == null || primitiveSet.has(typeof x); +} diff --git a/is/primitive_test.ts b/is/primitive_test.ts new file mode 100644 index 0000000..29b806b --- /dev/null +++ b/is/primitive_test.ts @@ -0,0 +1,16 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isPrimitive } from "./primitive.ts"; + +Deno.test("isPrimitive", async (t) => { + await testWithExamples(t, isPrimitive, { + validExamples: [ + "string", + "number", + "bigint", + "boolean", + "null", + "undefined", + "symbol", + ], + }); +}); diff --git a/is/readonly_of.ts b/is/readonly_of.ts new file mode 100644 index 0000000..c9e2671 --- /dev/null +++ b/is/readonly_of.ts @@ -0,0 +1,47 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { IsPredObj } from "../_annotation.ts"; +import type { Predicate } from "../type.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `Readonly>`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```typescript + * import { as, is } from "@core/unknownutil"; + * + * const isMyType = is.ReadonlyOf(is.ObjectOf({ + * a: is.Number, + * b: is.UnionOf([is.String, is.Undefined]), + * c: as.Readonly(is.Boolean), + * })); + * const a: unknown = { a: 0, b: "b", c: true }; + * if (isMyType(a)) { + * const _: { readonly a: number; readonly b: string | undefined; readonly c: boolean } = a; + * } + * ``` + */ +export function isReadonlyOf< + T extends Record, + P extends Record>, +>( + pred: Predicate & IsPredObj

, +): + & Predicate> + & IsPredObj

; +export function isReadonlyOf< + T extends Record, +>( + pred: Predicate, +): Predicate>; +export function isReadonlyOf< + T extends readonly [unknown, ...unknown[]], +>( + pred: Predicate, +): Predicate>; +export function isReadonlyOf( + pred: Predicate, +): Predicate> { + if (pred.name.startsWith("isReadonlyOf(")) return pred; + return rewriteName((x) => pred(x), "isReadonlyOf", pred); +} diff --git a/is/readonly_of_test.ts b/is/readonly_of_test.ts new file mode 100644 index 0000000..0661542 --- /dev/null +++ b/is/readonly_of_test.ts @@ -0,0 +1,173 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { as } from "../as/mod.ts"; +import { is } from "../is/mod.ts"; +import { isReadonlyOf } from "./readonly_of.ts"; + +Deno.test("isReadonlyOf", async (t) => { + await t.step("with isRecord", async (t) => { + const pred = is.Record; + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isReadonlyOf(pred).name); + await assertSnapshot(t, isReadonlyOf(isReadonlyOf(pred)).name); + }); + + await t.step("returns true on Readonly object", () => { + assertEquals( + isReadonlyOf(pred)({ a: 0, b: "b", c: true } as const), + true, + ); + assertEquals( + isReadonlyOf(pred)({ a: 0, b: "b", c: true }), + true, + ); + }); + + await t.step("returns false on non Readonly object", () => { + assertEquals(isReadonlyOf(pred)("a"), false, "Value is not an object"); + assertEquals( + isReadonlyOf(pred)([]), + false, + "Object have a different type property", + ); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0, b: "a", c: true }; + if (isReadonlyOf(pred)(a)) { + assertType< + Equal< + typeof a, + Readonly> + > + >(true); + } + }); + }); + + await t.step("with isObjectOf", async (t) => { + const pred = is.ObjectOf({ + a: is.Number, + b: is.UnionOf([is.String, is.Undefined]), + c: as.Readonly(is.Boolean), + }); + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isReadonlyOf(pred).name); + await assertSnapshot(t, isReadonlyOf(isReadonlyOf(pred)).name); + }); + + await t.step("returns true on Readonly object", () => { + assertEquals( + isReadonlyOf(pred)({ a: 0, b: "b", c: true } as const), + true, + ); + assertEquals( + isReadonlyOf(pred)({ a: 0, b: "b", c: true }), + true, + ); + }); + + await t.step("returns false on non Readonly object", () => { + assertEquals(isReadonlyOf(pred)("a"), false, "Value is not an object"); + assertEquals( + isReadonlyOf(pred)({ a: 0, b: "a", c: "" }), + false, + "Object have a different type property", + ); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0, b: "a", c: true }; + if (isReadonlyOf(pred)(a)) { + assertType< + Equal< + typeof a, + Readonly<{ a: number; b: string | undefined; c: boolean }> + > + >(true); + } + }); + }); + + await t.step("with isTupleOf", async (t) => { + const pred = is.TupleOf([is.Number, is.String, as.Readonly(is.Boolean)]); + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isReadonlyOf(pred).name); + await assertSnapshot(t, isReadonlyOf(isReadonlyOf(pred)).name); + }); + + await t.step("returns true on Readonly object", () => { + assertEquals( + isReadonlyOf(pred)([0, "b", true] as const), + true, + ); + assertEquals( + isReadonlyOf(pred)([0, "b", true]), + true, + ); + }); + + await t.step("returns false on non Readonly object", () => { + assertEquals(isReadonlyOf(pred)("a"), false, "Value is not an object"); + assertEquals( + isReadonlyOf(pred)([0, "a", ""]), + false, + "Object have a different type property", + ); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = []; + if (isReadonlyOf(pred)(a)) { + assertType< + Equal< + typeof a, + Readonly<[number, string, boolean]> + > + >(true); + } + }); + }); + + await t.step("with isUniformTupleOf", async (t) => { + const pred = is.UniformTupleOf(3, is.Number); + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isReadonlyOf(pred).name); + await assertSnapshot(t, isReadonlyOf(isReadonlyOf(pred)).name); + }); + + await t.step("returns true on Readonly object", () => { + assertEquals( + isReadonlyOf(pred)([0, 1, 2] as const), + true, + ); + assertEquals( + isReadonlyOf(pred)([0, 1, 2]), + true, + ); + }); + + await t.step("returns false on non Readonly object", () => { + assertEquals(isReadonlyOf(pred)("a"), false, "Value is not an object"); + assertEquals( + isReadonlyOf(pred)([0, "a", ""]), + false, + "Object have a different type property", + ); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = []; + if (isReadonlyOf(pred)(a)) { + assertType< + Equal< + typeof a, + Readonly<[number, number, number]> + > + >(true); + } + }); + }); +}); diff --git a/is/record.ts b/is/record.ts new file mode 100644 index 0000000..fc691f7 --- /dev/null +++ b/is/record.ts @@ -0,0 +1,25 @@ +/** + * Return `true` if the type of `x` satisfies `Record`. + * + * Note that this function returns `true` for ambiguous instances like `Set`, `Map`, `Date`, `Promise`, etc. + * Use {@linkcode isRecordObject} instead if you want to check if `x` is an instance of `Object`. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = {"a": 0, "b": 1}; + * if (is.Record(a)) { + * const _: Record = a; + * } + * + * const b: unknown = new Set(); + * if (is.Record(b)) { + * const _: Record = b; + * } + * ``` + */ +export function isRecord( + x: unknown, +): x is Record { + return x != null && !Array.isArray(x) && typeof x === "object"; +} diff --git a/is/record_object.ts b/is/record_object.ts new file mode 100644 index 0000000..2281809 --- /dev/null +++ b/is/record_object.ts @@ -0,0 +1,25 @@ +/** + * Return `true` if the type of `x` is an object instance that satisfies `Record`. + * + * Note that this function check if the `x` is an instance of `Object`. + * Use {@linkcode isRecord} instead if you want to check if the `x` satisfies the `Record` type. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = {"a": 0, "b": 1}; + * if (is.RecordObject(a)) { + * const _: Record = a; + * } + * + * const b: unknown = new Set(); + * if (is.RecordObject(b)) { + * // b is not a raw object, so it is not narrowed + * } + * ``` + */ +export function isRecordObject( + x: unknown, +): x is Record { + return x != null && typeof x === "object" && x.constructor === Object; +} diff --git a/is/record_object_of.ts b/is/record_object_of.ts new file mode 100644 index 0000000..dbd05f8 --- /dev/null +++ b/is/record_object_of.ts @@ -0,0 +1,52 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { Predicate } from "../type.ts"; +import { isRecordObject } from "./record_object.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is an Object instance that satisfies `Record`. + * + * Note that this function check if the `x` is an instance of `Object`. + * Use {@linkcode isRecordOf} instead if you want to check if the `x` satisfies the `Record` type. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.RecordObjectOf(is.Number); + * const a: unknown = {"a": 0, "b": 1}; + * if (isMyType(a)) { + * const _: Record = a; + * } + * ``` + * + * With predicate function for keys: + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.RecordObjectOf(is.Number, is.String); + * const a: unknown = {"a": 0, "b": 1}; + * if (isMyType(a)) { + * const _: Record = a; + * } + * ``` + */ +export function isRecordObjectOf( + pred: Predicate, + predKey?: Predicate, +): Predicate> { + return rewriteName( + (x: unknown): x is Record => { + if (!isRecordObject(x)) return false; + for (const k in x) { + if (!pred(x[k])) return false; + if (predKey && !predKey(k)) return false; + } + return true; + }, + "isRecordObjectOf", + pred, + predKey, + ); +} diff --git a/is/record_object_of_test.ts b/is/record_object_of_test.ts new file mode 100644 index 0000000..596ea05 --- /dev/null +++ b/is/record_object_of_test.ts @@ -0,0 +1,67 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { is } from "./mod.ts"; +import { isRecordObjectOf } from "./record_object_of.ts"; + +Deno.test("isRecordObjectOf", async (t) => { + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isRecordObjectOf(is.Number).name); + await assertSnapshot(t, isRecordObjectOf((_x): _x is string => false).name); + }); + + await t.step("returns true on T record", () => { + assertEquals(isRecordObjectOf(is.Number)({ a: 0 }), true); + assertEquals(isRecordObjectOf(is.String)({ a: "a" }), true); + assertEquals(isRecordObjectOf(is.Boolean)({ a: true }), true); + }); + + await t.step("returns false on non T record", () => { + assertEquals(isRecordObjectOf(is.String)({ a: 0 }), false); + assertEquals(isRecordObjectOf(is.Number)({ a: "a" }), false); + assertEquals(isRecordObjectOf(is.String)({ a: true }), false); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0 }; + if (isRecordObjectOf(is.Number)(a)) { + assertType>>(true); + } + }); +}); + +Deno.test("isRecordObjectOf", async (t) => { + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isRecordObjectOf(is.Number, is.String).name); + await assertSnapshot( + t, + isRecordObjectOf((_x): _x is string => false, is.String).name, + ); + }); + + await t.step("returns true on T record", () => { + assertEquals(isRecordObjectOf(is.Number, is.String)({ a: 0 }), true); + assertEquals(isRecordObjectOf(is.String, is.String)({ a: "a" }), true); + assertEquals(isRecordObjectOf(is.Boolean, is.String)({ a: true }), true); + }); + + await t.step("returns false on non T record", () => { + assertEquals(isRecordObjectOf(is.String, is.String)({ a: 0 }), false); + assertEquals(isRecordObjectOf(is.Number, is.String)({ a: "a" }), false); + assertEquals(isRecordObjectOf(is.String, is.String)({ a: true }), false); + }); + + await t.step("returns false on non K record", () => { + assertEquals(isRecordObjectOf(is.Number, is.Number)({ a: 0 }), false); + assertEquals(isRecordObjectOf(is.String, is.Number)({ a: "a" }), false); + assertEquals(isRecordObjectOf(is.Boolean, is.Number)({ a: true }), false); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0 }; + if (isRecordObjectOf(is.Number, is.String)(a)) { + assertType>>(true); + } + }); +}); diff --git a/is/record_object_test.ts b/is/record_object_test.ts new file mode 100644 index 0000000..73c71ee --- /dev/null +++ b/is/record_object_test.ts @@ -0,0 +1,8 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isRecordObject } from "./record_object.ts"; + +Deno.test("isRecordObject", async (t) => { + await testWithExamples(t, isRecordObject, { + validExamples: ["record"], + }); +}); diff --git a/is/record_of.ts b/is/record_of.ts new file mode 100644 index 0000000..4676f71 --- /dev/null +++ b/is/record_of.ts @@ -0,0 +1,52 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { Predicate } from "../type.ts"; +import { isRecord } from "./record.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` satisfies `Record`. + * + * Note that this function only check if the `x` satisfies the `Record` type. + * Use {@linkcode isRecordObjectOf} instead if you want to check if the `x` is an instance of `Object`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.RecordOf(is.Number); + * const a: unknown = {"a": 0, "b": 1}; + * if (isMyType(a)) { + * const _: Record = a; + * } + * ``` + * + * With predicate function for keys: + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.RecordOf(is.Number, is.String); + * const a: unknown = {"a": 0, "b": 1}; + * if (isMyType(a)) { + * const _: Record = a; + * } + * ``` + */ +export function isRecordOf( + pred: Predicate, + predKey?: Predicate, +): Predicate> { + return rewriteName( + (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; + }, + "isRecordOf", + pred, + predKey, + ); +} diff --git a/is/record_of_test.ts b/is/record_of_test.ts new file mode 100644 index 0000000..91cf7d6 --- /dev/null +++ b/is/record_of_test.ts @@ -0,0 +1,67 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { is } from "./mod.ts"; +import { isRecordOf } from "./record_of.ts"; + +Deno.test("isRecordOf", async (t) => { + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isRecordOf(is.Number).name); + await assertSnapshot(t, isRecordOf((_x): _x is string => false).name); + }); + + await t.step("returns true on T record", () => { + assertEquals(isRecordOf(is.Number)({ a: 0 }), true); + assertEquals(isRecordOf(is.String)({ a: "a" }), true); + assertEquals(isRecordOf(is.Boolean)({ a: true }), true); + }); + + await t.step("returns false on non T record", () => { + assertEquals(isRecordOf(is.String)({ a: 0 }), false); + assertEquals(isRecordOf(is.Number)({ a: "a" }), false); + assertEquals(isRecordOf(is.String)({ a: true }), false); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0 }; + if (isRecordOf(is.Number)(a)) { + assertType>>(true); + } + }); +}); + +Deno.test("isRecordOf", async (t) => { + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isRecordOf(is.Number, is.String).name); + await assertSnapshot( + t, + isRecordOf((_x): _x is string => false, is.String).name, + ); + }); + + await t.step("returns true on T record", () => { + assertEquals(isRecordOf(is.Number, is.String)({ a: 0 }), true); + assertEquals(isRecordOf(is.String, is.String)({ a: "a" }), true); + assertEquals(isRecordOf(is.Boolean, is.String)({ a: true }), true); + }); + + await t.step("returns false on non T record", () => { + assertEquals(isRecordOf(is.String, is.String)({ a: 0 }), false); + assertEquals(isRecordOf(is.Number, is.String)({ a: "a" }), false); + assertEquals(isRecordOf(is.String, is.String)({ a: true }), false); + }); + + await t.step("returns false on non K record", () => { + assertEquals(isRecordOf(is.Number, is.Number)({ a: 0 }), false); + assertEquals(isRecordOf(is.String, is.Number)({ a: "a" }), false); + assertEquals(isRecordOf(is.Boolean, is.Number)({ a: true }), false); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0 }; + if (isRecordOf(is.Number, is.String)(a)) { + assertType>>(true); + } + }); +}); diff --git a/is/record_test.ts b/is/record_test.ts new file mode 100644 index 0000000..dfa182e --- /dev/null +++ b/is/record_test.ts @@ -0,0 +1,8 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isRecord } from "./record.ts"; + +Deno.test("isRecord", async (t) => { + await testWithExamples(t, isRecord, { + validExamples: ["record", "date", "promise", "set", "map"], + }); +}); diff --git a/is/required_of.ts b/is/required_of.ts new file mode 100644 index 0000000..3b7fba7 --- /dev/null +++ b/is/required_of.ts @@ -0,0 +1,51 @@ +import type { FlatType } from "../_typeutil.ts"; +import type { IsPredObj } from "../_annotation.ts"; +import { asUnoptional } from "../as/optional.ts"; +import type { Predicate } from "../type.ts"; +import { isObjectOf } from "./object_of.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `Required>`. + * + * It only supports modifing a predicate function annotated with `IsPredObj`, usually returned by the followings + * + * - {@linkcode isIntersectionOf} + * - {@linkcode isObjectOf} + * - {@linkcode isOmitOf} + * - {@linkcode isPartialOf} + * - {@linkcode isPickOf} + * - {@linkcode isReadonlyOf} + * - {@linkcode isRequiredOf} + * - {@linkcode isStrictOf} + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```typescript + * import { as, is } from "@core/unknownutil"; + * + * const isMyType = is.RequiredOf(is.ObjectOf({ + * a: is.Number, + * b: is.UnionOf([is.String, is.Undefined]), + * c: as.Optional(is.Boolean), + * })); + * const a: unknown = { a: 0, b: "b", c: true, other: "other" }; + * if (isMyType(a)) { + * const _: { a: number; b: string | undefined; c: boolean } = a; + * } + * ``` + */ +export function isRequiredOf< + T extends Record, + P extends Record>, +>( + pred: Predicate & IsPredObj

, +): + & Predicate>> + & IsPredObj

{ + const predObj = Object.fromEntries( + Object.entries(pred.predObj).map(([k, v]) => [k, asUnoptional(v)]), + ); + return isObjectOf(predObj) as + & Predicate>> + & IsPredObj

; +} diff --git a/is/required_of_test.ts b/is/required_of_test.ts new file mode 100644 index 0000000..16c00e2 --- /dev/null +++ b/is/required_of_test.ts @@ -0,0 +1,54 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { as } from "../as/mod.ts"; +import { is } from "./mod.ts"; +import { isRequiredOf } from "./required_of.ts"; + +Deno.test("isRequiredOf", async (t) => { + const pred = is.ObjectOf({ + a: is.Number, + b: is.UnionOf([is.String, is.Undefined]), + c: as.Optional(is.Boolean), + d: as.Readonly(is.String), + }); + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isRequiredOf(pred).name); + await assertSnapshot(t, isRequiredOf(isRequiredOf(pred)).name); + }); + + 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", + ); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = { a: 0, b: "a", c: true }; + if (isRequiredOf(pred)(a)) { + assertType< + Equal< + typeof a, + { a: number; b: string | undefined; c: boolean; readonly d: string } + > + >(true); + } + }); +}); diff --git a/is/set.ts b/is/set.ts new file mode 100644 index 0000000..fe5a308 --- /dev/null +++ b/is/set.ts @@ -0,0 +1,17 @@ +/** + * Return `true` if the type of `x` is `Set`. + * + * Use {@linkcode isSetOf} to check if the type of `x` is a set of `T`. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = new Set([0, 1, 2]); + * if (is.Set(a)) { + * const _: Set = a; + * } + * ``` + */ +export function isSet(x: unknown): x is Set { + return x instanceof Set; +} diff --git a/is/set_of.ts b/is/set_of.ts new file mode 100644 index 0000000..0f45ffb --- /dev/null +++ b/is/set_of.ts @@ -0,0 +1,36 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { Predicate } from "../type.ts"; +import { isSet } from "./set.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `Set`. + * + * Use {@linkcode isSet} to check if the type of `x` is a set of `unknown`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.SetOf(is.String); + * const a: unknown = new Set(["a", "b", "c"]); + * if (isMyType(a)) { + * const _: Set = a; + * } + * ``` + */ +export function isSetOf( + pred: Predicate, +): Predicate> { + return rewriteName( + (x: unknown): x is Set => { + if (!isSet(x)) return false; + for (const v of x.values()) { + if (!pred(v)) return false; + } + return true; + }, + "isSetOf", + pred, + ); +} diff --git a/is/set_of_test.ts b/is/set_of_test.ts new file mode 100644 index 0000000..84498b4 --- /dev/null +++ b/is/set_of_test.ts @@ -0,0 +1,32 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { is } from "./mod.ts"; +import { isSetOf } from "./set_of.ts"; + +Deno.test("isSetOf", async (t) => { + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isSetOf(is.Number).name); + await assertSnapshot(t, isSetOf((_x): _x is string => false).name); + }); + + await t.step("returns true on T set", () => { + assertEquals(isSetOf(is.Number)(new Set([0, 1, 2])), true); + assertEquals(isSetOf(is.String)(new Set(["a", "b", "c"])), true); + assertEquals(isSetOf(is.Boolean)(new Set([true, false, true])), true); + }); + + await t.step("returns false on non T set", () => { + assertEquals(isSetOf(is.String)(new Set([0, 1, 2])), false); + assertEquals(isSetOf(is.Number)(new Set(["a", "b", "c"])), false); + assertEquals(isSetOf(is.String)(new Set([true, false, true])), false); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = new Set([0, 1, 2]); + if (isSetOf(is.Number)(a)) { + assertType>>(true); + } + }); +}); diff --git a/is/set_test.ts b/is/set_test.ts new file mode 100644 index 0000000..d072835 --- /dev/null +++ b/is/set_test.ts @@ -0,0 +1,6 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isSet } from "./set.ts"; + +Deno.test("isSet", async (t) => { + await testWithExamples(t, isSet, { validExamples: ["set"] }); +}); diff --git a/is/strict_of.ts b/is/strict_of.ts new file mode 100644 index 0000000..0181c2d --- /dev/null +++ b/is/strict_of.ts @@ -0,0 +1,54 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { IsPredObj } from "../_annotation.ts"; +import type { Predicate } from "../type.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is strictly follow the `ObjectOf`. + * + * It only supports modifing a predicate function annotated with `IsPredObj`, usually returned by the followings + * + * - {@linkcode isIntersectionOf} + * - {@linkcode isObjectOf} + * - {@linkcode isOmitOf} + * - {@linkcode isPartialOf} + * - {@linkcode isPickOf} + * - {@linkcode isReadonlyOf} + * - {@linkcode isRequiredOf} + * - {@linkcode isStrictOf} + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { as, is } from "@core/unknownutil"; + * + * const isMyType = is.StrictOf(is.ObjectOf({ + * a: is.Number, + * b: is.String, + * c: as.Optional(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< + T extends Record, + P extends Record>, +>( + pred: Predicate & IsPredObj

, +): + & Predicate + & IsPredObj

{ + const s = new Set(Object.keys(pred.predObj)); + return rewriteName( + (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)); + }, + "isStrictOf", + pred, + ) as Predicate & IsPredObj

; +} diff --git a/is/strict_of_test.ts b/is/strict_of_test.ts new file mode 100644 index 0000000..2b93f5a --- /dev/null +++ b/is/strict_of_test.ts @@ -0,0 +1,158 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { as } from "../as/mod.ts"; +import { is } from "./mod.ts"; +import { isStrictOf } from "./strict_of.ts"; + +Deno.test("isStrictOf", async (t) => { + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot( + t, + isStrictOf(is.ObjectOf({ a: is.Number, b: is.String, c: is.Boolean })) + .name, + ); + await assertSnapshot( + t, + isStrictOf(is.ObjectOf({ a: (_x): _x is string => false })).name, + ); + await assertSnapshot( + t, + isStrictOf( + is.ObjectOf({ + a: isStrictOf( + is.ObjectOf({ b: isStrictOf(is.ObjectOf({ c: is.Boolean })) }), + ), + }), + ).name, + ); + }); + + await t.step("returns true on T object", () => { + const predObj = { + a: is.Number, + b: is.String, + c: is.Boolean, + }; + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ a: 0, b: "a", c: true }), + true, + ); + }); + + await t.step("returns false on non T object", () => { + const predObj = { + a: is.Number, + b: is.String, + c: is.Boolean, + }; + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ a: 0, b: "a", c: "" }), + false, + "Object have a different type property", + ); + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ a: 0, b: "a" }), + false, + "Object does not have one property", + ); + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ + a: 0, + b: "a", + c: true, + d: "invalid", + }), + false, + "Object have an unknown property", + ); + }); + + await t.step("predicated type is correct", () => { + const predObj = { + a: is.Number, + b: is.String, + c: is.Boolean, + }; + const a: unknown = { a: 0, b: "a", c: true }; + if (isStrictOf(is.ObjectOf(predObj))(a)) { + assertType>(true); + } + }); + + await t.step("with optional properties", async (t) => { + await t.step("returns true on T object", () => { + const predObj = { + a: is.Number, + b: is.UnionOf([is.String, is.Undefined]), + c: as.Optional(is.Boolean), + }; + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ a: 0, b: "a", c: true }), + true, + ); + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ a: 0, b: "a" }), + true, + "Object does not have an optional property", + ); + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ a: 0, b: "a", c: undefined }), + true, + "Object has `undefined` as value of optional property", + ); + }); + + await t.step("returns false on non T object", () => { + const predObj = { + a: is.Number, + b: is.UnionOf([is.String, is.Undefined]), + c: as.Optional(is.Boolean), + }; + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ a: 0, b: "a", c: "" }), + false, + "Object have a different type property", + ); + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ a: 0, b: "a", c: null }), + false, + "Object has `null` as value of optional property", + ); + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ + a: 0, + b: "a", + c: true, + d: "invalid", + }), + false, + "Object have an unknown property", + ); + assertEquals( + isStrictOf(is.ObjectOf(predObj))({ + a: 0, + b: "a", + d: "invalid", + }), + false, + "Object have the same number of properties but an unknown property exists", + ); + }); + + await t.step("predicated type is correct", () => { + const predObj = { + a: is.Number, + b: is.UnionOf([is.String, is.Undefined]), + c: as.Optional(is.Boolean), + }; + const a: unknown = { a: 0, b: "a" }; + if (isStrictOf(is.ObjectOf(predObj))(a)) { + assertType< + Equal + >(true); + } + }); + }); +}); diff --git a/is/string.ts b/is/string.ts new file mode 100644 index 0000000..ffa8770 --- /dev/null +++ b/is/string.ts @@ -0,0 +1,15 @@ +/** + * Return `true` if the type of `x` is `string`. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = "a"; + * if (is.String(a)) { + * const _: string = a; + * } + * ``` + */ +export function isString(x: unknown): x is string { + return typeof x === "string"; +} diff --git a/is/string_test.ts b/is/string_test.ts new file mode 100644 index 0000000..be147d4 --- /dev/null +++ b/is/string_test.ts @@ -0,0 +1,6 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isString } from "./string.ts"; + +Deno.test("isString", async (t) => { + await testWithExamples(t, isString, { validExamples: ["string"] }); +}); diff --git a/is/symbol.ts b/is/symbol.ts new file mode 100644 index 0000000..d13f00a --- /dev/null +++ b/is/symbol.ts @@ -0,0 +1,15 @@ +/** + * Return `true` if the type of `x` is `symbol`. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = Symbol("symbol"); + * if (is.Symbol(a)) { + * const _: symbol = a; + * } + * ``` + */ +export function isSymbol(x: unknown): x is symbol { + return typeof x === "symbol"; +} diff --git a/is/symbol_test.ts b/is/symbol_test.ts new file mode 100644 index 0000000..f22f262 --- /dev/null +++ b/is/symbol_test.ts @@ -0,0 +1,6 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isSymbol } from "./symbol.ts"; + +Deno.test("isSymbol", async (t) => { + await testWithExamples(t, isSymbol, { validExamples: ["symbol"] }); +}); diff --git a/is/sync_function.ts b/is/sync_function.ts new file mode 100644 index 0000000..bf5bca4 --- /dev/null +++ b/is/sync_function.ts @@ -0,0 +1,19 @@ +const objectToString = Object.prototype.toString; + +/** + * Return `true` if the type of `x` is `function` (non async function). + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = () => {}; + * if (is.SyncFunction(a)) { + * const _: ((...args: unknown[]) => unknown) = a; + * } + * ``` + */ +export function isSyncFunction( + x: unknown, +): x is (...args: unknown[]) => unknown { + return objectToString.call(x) === "[object Function]"; +} diff --git a/is/sync_function_test.ts b/is/sync_function_test.ts new file mode 100644 index 0000000..b8c933e --- /dev/null +++ b/is/sync_function_test.ts @@ -0,0 +1,8 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isSyncFunction } from "./sync_function.ts"; + +Deno.test("isSyncFunction", async (t) => { + await testWithExamples(t, isSyncFunction, { + validExamples: ["syncFunction"], + }); +}); diff --git a/is/tuple_of.ts b/is/tuple_of.ts new file mode 100644 index 0000000..ac0d2f6 --- /dev/null +++ b/is/tuple_of.ts @@ -0,0 +1,102 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { Predicate, PredicateType } from "../type.ts"; +import { isArray } from "./array.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `TupleOf` or `TupleOf`. + * + * Use {@linkcode isUniformTupleOf} to check if the type of `x` is a tuple of uniform types. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.TupleOf([is.Number, is.String, is.Boolean]); + * const a: unknown = [0, "a", true]; + * if (isMyType(a)) { + * const _: [number, string, boolean] = a; + * } + * ``` + * + * With `predElse`: + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * 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)) { + * 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 "@core/unknownutil"; + * + * const predTup = [is.Number, is.String, is.Boolean] as const; + * const isMyType = is.TupleOf(predTup); + * const a: unknown = [0, "a", true]; + * if (isMyType(a)) { + * 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 rewriteName( + (x: unknown): x is TupleOf => { + if (!isArray(x) || x.length !== predTup.length) { + return false; + } + return predTup.every((pred, i) => pred(x[i])); + }, + "isTupleOf", + predTup, + ); + } else { + return rewriteName( + (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); + }, + "isTupleOf", + predTup, + predElse, + ); + } +} + +type TupleOf = { + -readonly [P in keyof T]: T[P] extends Predicate ? U : never; +}; diff --git a/is/tuple_of_test.ts b/is/tuple_of_test.ts new file mode 100644 index 0000000..c930390 --- /dev/null +++ b/is/tuple_of_test.ts @@ -0,0 +1,96 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { is } from "./mod.ts"; +import { isTupleOf } from "./tuple_of.ts"; + +Deno.test("isTupleOf", async (t) => { + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot( + t, + isTupleOf([is.Number, is.String, is.Boolean]).name, + ); + await assertSnapshot( + t, + isTupleOf([(_x): _x is string => false]).name, + ); + await assertSnapshot( + t, + isTupleOf([isTupleOf([isTupleOf([is.Number, is.String, is.Boolean])])]) + .name, + ); + }); + + await t.step("returns true on T tuple", () => { + const predTup = [is.Number, is.String, is.Boolean] as const; + assertEquals(isTupleOf(predTup)([0, "a", true]), true); + }); + + await t.step("returns false on non T tuple", () => { + const predTup = [is.Number, is.String, is.Boolean] as const; + assertEquals(isTupleOf(predTup)([0, 1, 2]), false); + assertEquals(isTupleOf(predTup)([0, "a"]), false); + assertEquals(isTupleOf(predTup)([0, "a", true, 0]), false); + }); + + await t.step("predicated type is correct", () => { + const predTup = [is.Number, is.String, is.Boolean] as const; + const a: unknown = [0, "a", true]; + if (isTupleOf(predTup)(a)) { + assertType>(true); + } + }); +}); + +Deno.test("isTupleOf", async (t) => { + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot( + t, + isTupleOf([is.Number, is.String, is.Boolean], is.Array).name, + ); + await assertSnapshot( + t, + isTupleOf([(_x): _x is string => false], is.ArrayOf(is.String)) + .name, + ); + await assertSnapshot( + t, + isTupleOf([ + isTupleOf( + [isTupleOf([is.Number, is.String, is.Boolean], is.Array)], + is.Array, + ), + ]).name, + ); + }); + + await t.step("returns true on T tuple", () => { + const predTup = [is.Number, is.String, is.Boolean] as const; + const predElse = is.ArrayOf(is.Number); + assertEquals(isTupleOf(predTup, predElse)([0, "a", true, 0, 1, 2]), true); + }); + + await t.step("returns false on non T tuple", () => { + const predTup = [is.Number, is.String, is.Boolean] as const; + const predElse = is.ArrayOf(is.String); + assertEquals(isTupleOf(predTup, predElse)([0, 1, 2, 0, 1, 2]), false); + assertEquals(isTupleOf(predTup, predElse)([0, "a", 0, 1, 2]), false); + assertEquals( + isTupleOf(predTup, predElse)([0, "a", true, 0, 0, 1, 2]), + false, + ); + assertEquals(isTupleOf(predTup, predElse)([0, "a", true, 0, 1, 2]), false); + }); + + await t.step("predicated type is correct", () => { + const predTup = [is.Number, is.String, is.Boolean] as const; + const predElse = is.ArrayOf(is.Number); + const a: unknown = [0, "a", true, 0, 1, 2]; + if (isTupleOf(predTup, predElse)(a)) { + assertType>( + true, + ); + } + }); +}); diff --git a/is/undefined.ts b/is/undefined.ts new file mode 100644 index 0000000..38dbd1c --- /dev/null +++ b/is/undefined.ts @@ -0,0 +1,15 @@ +/** + * Return `true` if the type of `x` is `undefined`. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a: unknown = undefined; + * if (is.Undefined(a)) { + * const _: undefined = a; + * } + * ``` + */ +export function isUndefined(x: unknown): x is undefined { + return typeof x === "undefined"; +} diff --git a/is/undefined_test.ts b/is/undefined_test.ts new file mode 100644 index 0000000..3b6ec26 --- /dev/null +++ b/is/undefined_test.ts @@ -0,0 +1,6 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isUndefined } from "./undefined.ts"; + +Deno.test("isUndefined", async (t) => { + await testWithExamples(t, isUndefined, { validExamples: ["undefined"] }); +}); diff --git a/is/uniform_tuple_of.ts b/is/uniform_tuple_of.ts new file mode 100644 index 0000000..d007912 --- /dev/null +++ b/is/uniform_tuple_of.ts @@ -0,0 +1,56 @@ +import { rewriteName } from "../_funcutil.ts"; +import type { Predicate } from "../type.ts"; +import { isArray } from "./array.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `UniformTupleOf`. + * + * Use {@linkcode isTupleOf} to check if the type of `x` is a tuple of `T`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.UniformTupleOf(5); + * const a: unknown = [0, 1, 2, 3, 4]; + * if (isMyType(a)) { + * const _: [unknown, unknown, unknown, unknown, unknown] = a; + * } + * ``` + * + * With predicate function: + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.UniformTupleOf(5, is.Number); + * const a: unknown = [0, 1, 2, 3, 4]; + * if (isMyType(a)) { + * const _: [number, number, number, number, number] = a; + * } + * ``` + */ +export function isUniformTupleOf( + n: N, + pred?: Predicate, +): Predicate> { + return rewriteName( + (x: unknown): x is UniformTupleOf => { + if (!isArray(x) || x.length !== n) { + return false; + } + return !pred || x.every((v) => pred(v)); + }, + "isUniformTupleOf", + n, + pred, + ); +} + +// https://stackoverflow.com/a/71700658/1273406 +type UniformTupleOf< + T, + N extends number, + R extends readonly T[] = [], +> = R["length"] extends N ? R : UniformTupleOf; diff --git a/is/uniform_tuple_of_test.ts b/is/uniform_tuple_of_test.ts new file mode 100644 index 0000000..3b4574f --- /dev/null +++ b/is/uniform_tuple_of_test.ts @@ -0,0 +1,43 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import type { Equal } from "../_testutil.ts"; +import { is } from "./mod.ts"; +import { isUniformTupleOf } from "./uniform_tuple_of.ts"; + +Deno.test("isUniformTupleOf", async (t) => { + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isUniformTupleOf(3).name); + await assertSnapshot(t, isUniformTupleOf(3, is.Number).name); + await assertSnapshot( + t, + isUniformTupleOf(3, (_x): _x is string => false).name, + ); + }); + + 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); + }); + + 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); + }); + + await t.step("predicated type is correct", () => { + const a: unknown = [0, 1, 2, 3, 4]; + if (isUniformTupleOf(5)(a)) { + assertType< + Equal + >(true); + } + + if (isUniformTupleOf(5, is.Number)(a)) { + assertType>( + true, + ); + } + }); +}); diff --git a/is/union_of.ts b/is/union_of.ts new file mode 100644 index 0000000..9e898ec --- /dev/null +++ b/is/union_of.ts @@ -0,0 +1,54 @@ +import { rewriteName } from "../_funcutil.ts"; +import { annotate } from "../_annotation.ts"; +import type { Predicate } from "../type.ts"; + +/** + * Return a type predicate function that returns `true` if the type of `x` is `UnionOf`. + * + * Use {@linkcode isIntersectionOf} to check if the type of `x` is an intersection of `T`. + * + * To enhance performance, users are advised to cache the return value of this function and mitigate the creation cost. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const isMyType = is.UnionOf([is.Number, is.String, is.Boolean]); + * const a: unknown = 0; + * if (isMyType(a)) { + * 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 "@core/unknownutil"; + * + * const preds = [is.Number, is.String, is.Boolean] as const; + * const isMyType = is.UnionOf(preds); + * const a: unknown = 0; + * if (isMyType(a)) { + * const _: number | string | boolean = a; + * } + * ``` + */ +export function isUnionOf< + T extends readonly [Predicate, ...Predicate[]], +>( + preds: T, +): Predicate> { + return annotate( + rewriteName( + (x: unknown): x is UnionOf => preds.some((pred) => pred(x)), + "isUnionOf", + preds, + ), + "union", + preds, + ); +} + +type UnionOf = T extends readonly [Predicate, ...infer R] + ? U | UnionOf + : never; diff --git a/is/union_of_test.ts b/is/union_of_test.ts new file mode 100644 index 0000000..8d54953 --- /dev/null +++ b/is/union_of_test.ts @@ -0,0 +1,47 @@ +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; +import { assertType } from "@std/testing/types"; +import { type Equal, testWithExamples } from "../_testutil.ts"; +import type { PredicateType } from "../type.ts"; +import { is } from "./mod.ts"; +import { isUnionOf } from "./union_of.ts"; + +Deno.test("isUnionOf", async (t) => { + await t.step("returns properly named predicate function", async (t) => { + await assertSnapshot(t, isUnionOf([is.Number, is.String, is.Boolean]).name); + }); + + await t.step("returns true on one of T", () => { + const preds = [is.Number, is.String, is.Boolean] 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 = [is.Number, is.String, is.Boolean] as const; + await testWithExamples(t, isUnionOf(preds), { + excludeExamples: ["number", "string", "boolean"], + }); + }); + + await t.step("predicated type is correct", () => { + const preds = [is.Number, is.String, is.Boolean] as const; + const a: unknown = [0, "a", true]; + if (isUnionOf(preds)(a)) { + assertType>(true); + } + }); + + await t.step("predicated type is correct (#49)", () => { + const isFoo = is.ObjectOf({ foo: is.String }); + const isBar = is.ObjectOf({ foo: is.String, bar: is.Number }); + type Foo = PredicateType; + type Bar = PredicateType; + const preds = [isFoo, isBar] as const; + const a: unknown = [0, "a", true]; + if (isUnionOf(preds)(a)) { + assertType>(true); + } + }); +}); diff --git a/is/unknown.ts b/is/unknown.ts new file mode 100644 index 0000000..e5de964 --- /dev/null +++ b/is/unknown.ts @@ -0,0 +1,17 @@ +/** + * Assume `x` is `unknown` and always return `true` regardless of the type of `x`. + * + * Use {@linkcode isAny} to assume that the type of `x` is `any`. + * + * ```ts + * import { is } from "@core/unknownutil"; + * + * const a = "a"; + * if (is.Unknown(a)) { + * const _: unknown = a; + * } + * ``` + */ +export function isUnknown(_x: unknown): _x is unknown { + return true; +} diff --git a/is/unknown_test.ts b/is/unknown_test.ts new file mode 100644 index 0000000..1f25945 --- /dev/null +++ b/is/unknown_test.ts @@ -0,0 +1,24 @@ +import { testWithExamples } from "../_testutil.ts"; +import { isUnknown } from "./unknown.ts"; + +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", + ], + }); +}); diff --git a/is_test.ts b/is_test.ts deleted file mode 100644 index baf3e53..0000000 --- a/is_test.ts +++ /dev/null @@ -1,1992 +0,0 @@ -import { assertEquals, assertStrictEquals } from "@std/assert"; -import { assertSnapshot } from "@std/testing/snapshot"; -import { assertType } from "@std/testing/types"; -import { type Equal, stringify } from "./_testutil.ts"; -import type { Predicate, PredicateType } from "./is.ts"; -import { - is, - isAllOf, - isAny, - isArray, - isArrayOf, - isAsyncFunction, - isBigInt, - isBoolean, - isFunction, - isInstanceOf, - isIntersectionOf, - isLiteralOf, - isLiteralOneOf, - isMap, - isMapOf, - isNull, - isNullish, - isNumber, - isObjectOf, - isOmitOf, - isOneOf, - isOptionalOf, - isParametersOf, - isPartialOf, - isPickOf, - isPrimitive, - isReadonlyOf, - isReadonlyTupleOf, - isReadonlyUniformTupleOf, - isRecord, - isRecordLike, - isRecordLikeOf, - isRecordObject, - isRecordObjectOf, - isRecordOf, - isRequiredOf, - isSet, - isSetOf, - isStrictOf, - isString, - isSymbol, - isSyncFunction, - isTupleOf, - isUndefined, - isUniformTupleOf, - isUnionOf, - isUnknown, - isUnwrapOptionalOf, - isUnwrapReadonlyOf, -} from "./is.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("isRecordObject", async (t) => { - await testWithExamples(t, isRecordObject, { - validExamples: ["record"], - }); -}); - -Deno.test("isRecord", async (t) => { - await testWithExamples(t, isRecord, { - validExamples: ["record", "date", "promise", "set", "map"], - }); -}); - -Deno.test("isRecordLike", async (t) => { - await testWithExamples(t, isRecordLike, { - validExamples: ["record", "date", "promise", "set", "map"], - }); -}); - -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("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 isRecordObject", async (t) => { - await testWithExamples(t, isOptionalOf(isRecordObject), { - 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 isRecordObject", async (t) => { - await testWithExamples( - t, - isUnwrapOptionalOf(isOptionalOf(isRecordObject)), - { - 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("isArrayOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isArrayOf(isNumber).name); - await assertSnapshot(t, isArrayOf((_x): _x is string => false).name); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = [0, 1, 2]; - if (isArrayOf(isNumber)(a)) { - assertType>(true); - } - }); - await t.step("returns true on T array", () => { - assertEquals(isArrayOf(isNumber)([0, 1, 2]), true); - assertEquals(isArrayOf(isString)(["a", "b", "c"]), true); - assertEquals(isArrayOf(isBoolean)([true, false, true]), true); - }); - await t.step("returns false on non T array", () => { - assertEquals(isArrayOf(isString)([0, 1, 2]), false); - assertEquals(isArrayOf(isNumber)(["a", "b", "c"]), false); - assertEquals(isArrayOf(isString)([true, false, true]), false); - }); - await testWithExamples(t, isArrayOf((_: unknown): _ is unknown => true), { - excludeExamples: ["array"], - }); -}); - -Deno.test("isSetOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isSetOf(isNumber).name); - await assertSnapshot(t, isSetOf((_x): _x is string => false).name); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = new Set([0, 1, 2]); - if (isSetOf(isNumber)(a)) { - assertType>>(true); - } - }); - await t.step("returns true on T set", () => { - assertEquals(isSetOf(isNumber)(new Set([0, 1, 2])), true); - assertEquals(isSetOf(isString)(new Set(["a", "b", "c"])), true); - assertEquals(isSetOf(isBoolean)(new Set([true, false, true])), true); - }); - await t.step("returns false on non T set", () => { - assertEquals(isSetOf(isString)(new Set([0, 1, 2])), false); - assertEquals(isSetOf(isNumber)(new Set(["a", "b", "c"])), false); - assertEquals(isSetOf(isString)(new Set([true, false, true])), false); - }); - await testWithExamples(t, isSetOf((_: unknown): _ is unknown => true), { - excludeExamples: ["set"], - }); -}); - -Deno.test("isTupleOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot( - t, - isTupleOf([isNumber, isString, isBoolean]).name, - ); - await assertSnapshot( - t, - isTupleOf([(_x): _x is string => false]).name, - ); - // Nested - await assertSnapshot( - t, - isTupleOf([isTupleOf([isTupleOf([isNumber, isString, isBoolean])])]).name, - ); - }); - await t.step("returns proper type predicate", () => { - const predTup = [isNumber, isString, isBoolean] as const; - const a: unknown = [0, "a", true]; - if (isTupleOf(predTup)(a)) { - assertType>(true); - } - }); - await t.step("returns true on T tuple", () => { - const predTup = [isNumber, isString, isBoolean] as const; - assertEquals(isTupleOf(predTup)([0, "a", true]), true); - }); - await t.step("returns false on non T tuple", () => { - const predTup = [isNumber, isString, isBoolean] as const; - assertEquals(isTupleOf(predTup)([0, 1, 2]), false); - assertEquals(isTupleOf(predTup)([0, "a"]), false); - assertEquals(isTupleOf(predTup)([0, "a", true, 0]), false); - }); - await testWithExamples(t, isTupleOf([(_: unknown): _ is unknown => true]), { - excludeExamples: ["array"], - }); -}); - -Deno.test("isTupleOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot( - t, - isTupleOf([isNumber, isString, isBoolean], isArray).name, - ); - await assertSnapshot( - t, - isTupleOf([(_x): _x is string => false], isArrayOf(isString)) - .name, - ); - // Nested - await assertSnapshot( - t, - isTupleOf([ - isTupleOf( - [isTupleOf([isNumber, isString, isBoolean], isArray)], - isArray, - ), - ]).name, - ); - }); - await t.step("returns proper type predicate", () => { - const predTup = [isNumber, isString, isBoolean] as const; - const predElse = isArrayOf(isNumber); - const a: unknown = [0, "a", true, 0, 1, 2]; - if (isTupleOf(predTup, predElse)(a)) { - assertType>( - true, - ); - } - }); - await t.step("returns true on T tuple", () => { - const predTup = [isNumber, isString, isBoolean] as const; - 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 = isArrayOf(isString); - assertEquals(isTupleOf(predTup, predElse)([0, 1, 2, 0, 1, 2]), false); - assertEquals(isTupleOf(predTup, predElse)([0, "a", 0, 1, 2]), false); - assertEquals( - isTupleOf(predTup, predElse)([0, "a", true, 0, 0, 1, 2]), - false, - ); - assertEquals(isTupleOf(predTup, predElse)([0, "a", true, 0, 1, 2]), false); - }); - const predElse = isArray; - await testWithExamples( - t, - isTupleOf([(_: unknown): _ is unknown => true], predElse), - { - excludeExamples: ["array"], - }, - ); -}); - -Deno.test("isParametersOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot( - t, - isParametersOf([isNumber, isString, isOptionalOf(isBoolean)]).name, - ); - await assertSnapshot( - t, - isParametersOf([(_x): _x is string => false]).name, - ); - await assertSnapshot( - t, - isParametersOf([]).name, - ); - // Nested - await assertSnapshot( - t, - isParametersOf([ - isParametersOf([ - isParametersOf([isNumber, isString, isOptionalOf(isBoolean)]), - ]), - ]).name, - ); - }); - await t.step("returns proper type predicate", () => { - const predTup = [ - isOptionalOf(isNumber), - isString, - isOptionalOf(isString), - isOptionalOf(isBoolean), - ] as const; - const a: unknown = [0, "a"]; - if (isParametersOf(predTup)(a)) { - assertType< - Equal - >(true); - } - }); - await t.step("returns true on T tuple", () => { - const predTup = [isNumber, isString, isOptionalOf(isBoolean)] as const; - assertEquals(isParametersOf(predTup)([0, "a", true]), true); - assertEquals(isParametersOf(predTup)([0, "a"]), true); - }); - await t.step("returns false on non T tuple", () => { - const predTup = [isNumber, isString, isOptionalOf(isBoolean)] as const; - assertEquals(isParametersOf(predTup)([0, 1, 2]), false); - assertEquals(isParametersOf(predTup)([0, "a", true, 0]), false); - }); - await testWithExamples( - t, - isParametersOf([(_: unknown): _ is unknown => true]), - { - excludeExamples: ["array"], - }, - ); -}); - -Deno.test("isParametersOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot( - t, - isParametersOf([isNumber, isString, isOptionalOf(isBoolean)], isArray) - .name, - ); - await assertSnapshot( - t, - isParametersOf([(_x): _x is string => false], isArrayOf(isString)) - .name, - ); - // Empty - await assertSnapshot( - t, - isParametersOf([], isArrayOf(isString)).name, - ); - // Nested - await assertSnapshot( - t, - isParametersOf([ - isParametersOf( - [isParametersOf( - [isNumber, isString, isOptionalOf(isBoolean)], - isArray, - )], - isArray, - ), - ]).name, - ); - }); - await t.step("returns proper type predicate", () => { - const predTup = [ - isOptionalOf(isNumber), - isString, - isOptionalOf(isString), - isOptionalOf(isBoolean), - ] as const; - const predElse = isArrayOf(isNumber); - const a: unknown = [0, "a"]; - if (isParametersOf(predTup, predElse)(a)) { - assertType< - Equal< - typeof a, - [number | undefined, string, string?, boolean?, ...number[]] - > - >( - true, - ); - } - }); - await t.step("returns true on T tuple", () => { - const predTup = [isNumber, isString, isOptionalOf(isBoolean)] as const; - const predElse = isArrayOf(isNumber); - assertEquals( - isParametersOf(predTup, predElse)([0, "a", true, 0, 1, 2]), - true, - ); - assertEquals( - isParametersOf(predTup, predElse)([0, "a", undefined, 0, 1, 2]), - true, - ); - assertEquals(isParametersOf(predTup, predElse)([0, "a"]), true); - }); - await t.step("returns false on non T tuple", () => { - const predTup = [isNumber, isString, isOptionalOf(isBoolean)] as const; - const predElse = isArrayOf(isString); - assertEquals(isParametersOf(predTup, predElse)([0, 1, 2, 0, 1, 2]), false); - assertEquals(isParametersOf(predTup, predElse)([0, "a", 0, 1, 2]), false); - assertEquals( - isParametersOf(predTup, predElse)([0, "a", true, 0, 1, 2]), - false, - ); - assertEquals( - isParametersOf(predTup, predElse)([0, "a", undefined, 0, 1, 2]), - false, - ); - assertEquals(isParametersOf(predTup, predElse)([0, "a", "b"]), false); - }); - const predElse = isArray; - await testWithExamples( - t, - isParametersOf([(_: unknown): _ is unknown => true], predElse), - { - excludeExamples: ["array"], - }, - ); -}); - -Deno.test("isReadonlyTupleOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot( - t, - isReadonlyTupleOf([isNumber, isString, isBoolean]).name, - ); - await assertSnapshot( - t, - isReadonlyTupleOf([(_x): _x is string => false]).name, - ); - // Nested - await assertSnapshot( - t, - isReadonlyTupleOf([ - isReadonlyTupleOf([isReadonlyTupleOf([isNumber, isString, isBoolean])]), - ]).name, - ); - }); - await t.step("returns proper type predicate", () => { - const predTup = [isNumber, isString, isBoolean] as const; - const a: unknown = [0, "a", true]; - if (isReadonlyTupleOf(predTup)(a)) { - assertType>(true); - } - }); - await t.step("returns true on T tuple", () => { - const predTup = [isNumber, isString, isBoolean] as const; - assertEquals(isReadonlyTupleOf(predTup)([0, "a", true]), true); - }); - await t.step("returns false on non T tuple", () => { - const predTup = [isNumber, isString, isBoolean] as const; - assertEquals(isReadonlyTupleOf(predTup)([0, 1, 2]), false); - assertEquals(isReadonlyTupleOf(predTup)([0, "a"]), false); - assertEquals(isReadonlyTupleOf(predTup)([0, "a", true, 0]), false); - }); - await testWithExamples( - t, - isReadonlyTupleOf([(_: unknown): _ is unknown => true]), - { - excludeExamples: ["array"], - }, - ); -}); - -Deno.test("isReadonlyTupleOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot( - t, - isReadonlyTupleOf([isNumber, isString, isBoolean], isArray) - .name, - ); - await assertSnapshot( - t, - isReadonlyTupleOf( - [(_x): _x is string => false], - isArrayOf(isString), - ).name, - ); - // Nested - await assertSnapshot( - t, - isReadonlyTupleOf([ - isReadonlyTupleOf([ - isReadonlyTupleOf([isNumber, isString, isBoolean], isArray), - ], isArray), - ], isArray).name, - ); - }); - await t.step("returns proper type predicate", () => { - const predTup = [isNumber, isString, isBoolean] as const; - const predElse = isArrayOf(isNumber); - const a: unknown = [0, "a", true, 0, 1, 2]; - if (isReadonlyTupleOf(predTup, predElse)(a)) { - assertType< - Equal - >(true); - } - }); - await t.step("returns true on T tuple", () => { - const predTup = [isNumber, isString, isBoolean] as const; - const predElse = isArrayOf(isNumber); - assertEquals( - isReadonlyTupleOf(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 = isArrayOf(isString); - assertEquals( - isReadonlyTupleOf(predTup, predElse)([0, 1, 2, 0, 1, 2]), - false, - ); - assertEquals( - isReadonlyTupleOf(predTup, predElse)([0, "a", 0, 1, 2]), - false, - ); - assertEquals( - isReadonlyTupleOf(predTup, predElse)([0, "a", true, 0, 0, 1, 2]), - false, - ); - assertEquals( - isReadonlyTupleOf(predTup, predElse)([0, "a", true, 0, 1, 2]), - false, - ); - }); - const predElse = isArray; - await testWithExamples( - t, - isReadonlyTupleOf([(_: unknown): _ is unknown => true], predElse), - { - excludeExamples: ["array"], - }, - ); -}); - -Deno.test("isUniformTupleOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isUniformTupleOf(3).name); - await assertSnapshot(t, isUniformTupleOf(3, isNumber).name); - await assertSnapshot( - t, - isUniformTupleOf(3, (_x): _x is string => false).name, - ); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = [0, 1, 2, 3, 4]; - if (isUniformTupleOf(5)(a)) { - assertType< - Equal - >(true); - } - - if (isUniformTupleOf(5, isNumber)(a)) { - assertType>( - true, - ); - } - }); - await t.step("returns true on mono-typed T tuple", () => { - assertEquals(isUniformTupleOf(3)([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, isNumber)(["a", "b", "c"]), false); - }); - await testWithExamples(t, isUniformTupleOf(4), { - excludeExamples: ["array"], - }); -}); - -Deno.test("isReadonlyUniformTupleOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isReadonlyUniformTupleOf(3).name); - await assertSnapshot(t, isReadonlyUniformTupleOf(3, isNumber).name); - await assertSnapshot( - t, - isReadonlyUniformTupleOf(3, (_x): _x is string => false).name, - ); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = [0, 1, 2, 3, 4]; - if (isReadonlyUniformTupleOf(5)(a)) { - assertType< - Equal< - typeof a, - readonly [unknown, unknown, unknown, unknown, unknown] - > - >(true); - } - - if (isReadonlyUniformTupleOf(5, isNumber)(a)) { - assertType< - Equal - >(true); - } - }); - await t.step("returns true on mono-typed T tuple", () => { - assertEquals(isReadonlyUniformTupleOf(3)([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, isNumber)(["a", "b", "c"]), - false, - ); - }); - await testWithExamples(t, isReadonlyUniformTupleOf(4), { - excludeExamples: ["array"], - }); -}); - -Deno.test("isRecordObjectOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isRecordObjectOf(isNumber).name); - await assertSnapshot(t, isRecordObjectOf((_x): _x is string => false).name); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = { a: 0 }; - if (isRecordObjectOf(isNumber)(a)) { - assertType>>(true); - } - }); - await t.step("returns true on T record", () => { - assertEquals(isRecordObjectOf(isNumber)({ a: 0 }), true); - assertEquals(isRecordObjectOf(isString)({ a: "a" }), true); - assertEquals(isRecordObjectOf(isBoolean)({ a: true }), true); - }); - await t.step("returns false on non T record", () => { - assertEquals(isRecordObjectOf(isString)({ a: 0 }), false); - assertEquals(isRecordObjectOf(isNumber)({ a: "a" }), false); - assertEquals(isRecordObjectOf(isString)({ a: true }), false); - }); - await testWithExamples( - t, - isRecordObjectOf((_: unknown): _ is unknown => true), - { - excludeExamples: ["record"], - }, - ); -}); - -Deno.test("isRecordObjectOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isRecordObjectOf(isNumber, isString).name); - await assertSnapshot( - t, - isRecordObjectOf((_x): _x is string => false, isString).name, - ); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = { a: 0 }; - if (isRecordObjectOf(isNumber, isString)(a)) { - assertType>>(true); - } - }); - await t.step("returns true on T record", () => { - assertEquals(isRecordObjectOf(isNumber, isString)({ a: 0 }), true); - assertEquals(isRecordObjectOf(isString, isString)({ a: "a" }), true); - assertEquals(isRecordObjectOf(isBoolean, isString)({ a: true }), true); - }); - await t.step("returns false on non T record", () => { - assertEquals(isRecordObjectOf(isString, isString)({ a: 0 }), false); - assertEquals(isRecordObjectOf(isNumber, isString)({ a: "a" }), false); - assertEquals(isRecordObjectOf(isString, isString)({ a: true }), false); - }); - await t.step("returns false on non K record", () => { - assertEquals(isRecordObjectOf(isNumber, isNumber)({ a: 0 }), false); - assertEquals(isRecordObjectOf(isString, isNumber)({ a: "a" }), false); - assertEquals(isRecordObjectOf(isBoolean, isNumber)({ a: true }), false); - }); - await testWithExamples( - t, - isRecordObjectOf((_: unknown): _ is unknown => true), - { - excludeExamples: ["record"], - }, - ); -}); - -Deno.test("isRecordOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isRecordOf(isNumber).name); - await assertSnapshot(t, isRecordOf((_x): _x is string => false).name); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = { a: 0 }; - if (isRecordOf(isNumber)(a)) { - assertType>>(true); - } - }); - await t.step("returns true on T record", () => { - assertEquals(isRecordOf(isNumber)({ a: 0 }), true); - assertEquals(isRecordOf(isString)({ a: "a" }), true); - assertEquals(isRecordOf(isBoolean)({ a: true }), true); - }); - await t.step("returns false on non T record", () => { - assertEquals(isRecordOf(isString)({ a: 0 }), false); - assertEquals(isRecordOf(isNumber)({ a: "a" }), false); - assertEquals(isRecordOf(isString)({ a: true }), false); - }); - await testWithExamples( - t, - isRecordOf((_: unknown): _ is unknown => true), - { - excludeExamples: ["record", "date", "promise", "set", "map"], - }, - ); -}); - -Deno.test("isRecordOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isRecordOf(isNumber, isString).name); - await assertSnapshot( - t, - isRecordOf((_x): _x is string => false, isString).name, - ); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = { a: 0 }; - if (isRecordOf(isNumber, isString)(a)) { - assertType>>(true); - } - }); - await t.step("returns true on T record", () => { - assertEquals(isRecordOf(isNumber, isString)({ a: 0 }), true); - assertEquals(isRecordOf(isString, isString)({ a: "a" }), true); - assertEquals(isRecordOf(isBoolean, isString)({ a: true }), true); - }); - await t.step("returns false on non T record", () => { - assertEquals(isRecordOf(isString, isString)({ a: 0 }), false); - assertEquals(isRecordOf(isNumber, isString)({ a: "a" }), false); - assertEquals(isRecordOf(isString, isString)({ a: true }), false); - }); - await t.step("returns false on non K record", () => { - assertEquals(isRecordOf(isNumber, isNumber)({ a: 0 }), false); - assertEquals(isRecordOf(isString, isNumber)({ a: "a" }), false); - assertEquals(isRecordOf(isBoolean, isNumber)({ a: true }), false); - }); - await testWithExamples( - t, - isRecordOf((_: unknown): _ is unknown => true), - { - excludeExamples: ["record", "date", "promise", "set", "map"], - }, - ); -}); - -Deno.test("isRecordLikeOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isRecordLikeOf(isNumber).name); - await assertSnapshot(t, isRecordLikeOf((_x): _x is string => false).name); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = { a: 0 }; - if (isRecordLikeOf(isNumber)(a)) { - assertType>>(true); - } - }); - await t.step("returns true on T record", () => { - assertEquals(isRecordLikeOf(isNumber)({ a: 0 }), true); - assertEquals(isRecordLikeOf(isString)({ a: "a" }), true); - assertEquals(isRecordLikeOf(isBoolean)({ a: true }), true); - }); - await t.step("returns false on non T record", () => { - assertEquals(isRecordLikeOf(isString)({ a: 0 }), false); - assertEquals(isRecordLikeOf(isNumber)({ a: "a" }), false); - assertEquals(isRecordLikeOf(isString)({ a: true }), false); - }); - await testWithExamples( - t, - isRecordLikeOf((_: unknown): _ is unknown => true), - { - excludeExamples: ["record", "date", "promise", "set", "map"], - }, - ); -}); - -Deno.test("isRecordLikeOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isRecordLikeOf(isNumber, isString).name); - await assertSnapshot( - t, - isRecordLikeOf((_x): _x is string => false, isString).name, - ); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = { a: 0 }; - if (isRecordLikeOf(isNumber, isString)(a)) { - assertType>>(true); - } - }); - await t.step("returns true on T record", () => { - assertEquals(isRecordLikeOf(isNumber, isString)({ a: 0 }), true); - assertEquals(isRecordLikeOf(isString, isString)({ a: "a" }), true); - assertEquals(isRecordLikeOf(isBoolean, isString)({ a: true }), true); - }); - await t.step("returns false on non T record", () => { - assertEquals(isRecordLikeOf(isString, isString)({ a: 0 }), false); - assertEquals(isRecordLikeOf(isNumber, isString)({ a: "a" }), false); - assertEquals(isRecordLikeOf(isString, isString)({ a: true }), false); - }); - await t.step("returns false on non K record", () => { - assertEquals(isRecordLikeOf(isNumber, isNumber)({ a: 0 }), false); - assertEquals(isRecordLikeOf(isString, isNumber)({ a: "a" }), false); - assertEquals(isRecordLikeOf(isBoolean, isNumber)({ a: true }), false); - }); - await testWithExamples( - t, - isRecordLikeOf((_: unknown): _ is unknown => true), - { - excludeExamples: ["record", "date", "promise", "set", "map"], - }, - ); -}); - -Deno.test("isMapOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isMapOf(isNumber).name); - await assertSnapshot(t, isMapOf((_x): _x is string => false).name); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = new Map([["a", 0]]); - if (isMapOf(isNumber)(a)) { - assertType>>(true); - } - }); - await t.step("returns true on T map", () => { - assertEquals(isMapOf(isNumber)(new Map([["a", 0]])), true); - assertEquals(isMapOf(isString)(new Map([["a", "a"]])), true); - assertEquals(isMapOf(isBoolean)(new Map([["a", true]])), true); - }); - await t.step("returns false on non T map", () => { - assertEquals(isMapOf(isString)(new Map([["a", 0]])), false); - assertEquals(isMapOf(isNumber)(new Map([["a", "a"]])), false); - assertEquals(isMapOf(isString)(new Map([["a", true]])), false); - }); - await testWithExamples(t, isMapOf((_: unknown): _ is unknown => true), { - excludeExamples: ["map"], - }); -}); - -Deno.test("isMapOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isMapOf(isNumber, isString).name); - await assertSnapshot( - t, - isMapOf((_x): _x is string => false, isString).name, - ); - }); - await t.step("returns proper type predicate", () => { - const a: unknown = new Map([["a", 0]]); - if (isMapOf(isNumber, isString)(a)) { - assertType>>(true); - } - }); - await t.step("returns true on T map", () => { - assertEquals(isMapOf(isNumber, isString)(new Map([["a", 0]])), true); - assertEquals(isMapOf(isString, isString)(new Map([["a", "a"]])), true); - assertEquals(isMapOf(isBoolean, isString)(new Map([["a", true]])), true); - }); - await t.step("returns false on non T map", () => { - assertEquals(isMapOf(isString, isString)(new Map([["a", 0]])), false); - assertEquals(isMapOf(isNumber, isString)(new Map([["a", "a"]])), false); - assertEquals(isMapOf(isString, isString)(new Map([["a", true]])), false); - }); - await t.step("returns false on non K map", () => { - assertEquals(isMapOf(isNumber, isNumber)(new Map([["a", 0]])), false); - assertEquals(isMapOf(isString, isNumber)(new Map([["a", "a"]])), false); - assertEquals(isMapOf(isBoolean, isNumber)(new Map([["a", true]])), false); - }); - await testWithExamples(t, isMapOf((_: unknown): _ is unknown => true), { - excludeExamples: ["map"], - }); -}); - -Deno.test("isObjectOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot( - t, - isObjectOf({ a: isNumber, b: isString, c: isBoolean }).name, - ); - await assertSnapshot( - t, - isObjectOf({ a: (_x): _x is string => false }).name, - ); - // Nested - await assertSnapshot( - t, - isObjectOf({ a: isObjectOf({ b: 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 (isObjectOf(predObj)(a)) { - assertType>(true); - } - }); - await t.step("returns true on T object", () => { - const predObj = { - a: isNumber, - b: isString, - c: isBoolean, - }; - assertEquals(isObjectOf(predObj)({ a: 0, b: "a", c: true }), true); - assertEquals( - isObjectOf(predObj, { strict: true })({ a: 0, b: "a", c: true }), - true, - "Specify `{ strict: true }`", - ); - assertEquals( - isObjectOf(predObj)({ a: 0, b: "a", c: true, d: "ignored" }), - true, - "Object have an unknown property", - ); - assertEquals( - isObjectOf(predObj)( - Object.assign(() => void 0, { a: 0, b: "a", c: true }), - ), - true, - "Function object", - ); - }); - await t.step("returns false on non T object", () => { - const predObj = { - a: isNumber, - b: isString, - c: isBoolean, - }; - assertEquals(isObjectOf(predObj)("a"), false, "Value is not an object"); - assertEquals( - isObjectOf(predObj)({ a: 0, b: "a", c: "" }), - false, - "Object have a different type property", - ); - assertEquals( - isObjectOf(predObj)({ a: 0, b: "a" }), - false, - "Object does not have one property", - ); - assertEquals( - isObjectOf(predObj, { strict: true })({ - a: 0, - b: "a", - c: true, - d: "invalid", - }), - false, - "Specify `{ strict: true }` and object have an unknown property", - ); - assertEquals( - isObjectOf({ 0: isString })(["a"]), - false, - "Value is not an object", - ); - }); - await t.step("returns true on T instance", () => { - const date = new Date(); - const predObj = { - getFullYear: isFunction, - }; - assertEquals(isObjectOf(predObj)(date), true, "Value is not an object"); - }); - await testWithExamples( - 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: isUnionOf([isString, isUndefined]), - c: isOptionalOf(isBoolean), - }; - const a: unknown = { a: 0, b: "a" }; - if (isStrictOf(isObjectOf(predObj))(a)) { - assertType< - Equal - >(true); - } - }); - await t.step("returns true on T object", () => { - const predObj = { - a: isNumber, - b: isUnionOf([isString, isUndefined]), - c: isOptionalOf(isBoolean), - }; - assertEquals( - isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: true }), - true, - ); - assertEquals( - isStrictOf(isObjectOf(predObj))({ a: 0, b: "a" }), - true, - "Object does not have an optional property", - ); - assertEquals( - isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: undefined }), - true, - "Object has `undefined` as value of optional property", - ); - }); - await t.step("returns false on non T object", () => { - const predObj = { - a: isNumber, - b: isUnionOf([isString, isUndefined]), - c: isOptionalOf(isBoolean), - }; - assertEquals( - isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: "" }), - false, - "Object have a different type property", - ); - assertEquals( - isStrictOf(isObjectOf(predObj))({ a: 0, b: "a", c: null }), - false, - "Object has `null` as value of optional property", - ); - assertEquals( - isStrictOf(isObjectOf(predObj))({ - a: 0, - b: "a", - c: true, - d: "invalid", - }), - false, - "Object have an unknown property", - ); - assertEquals( - isStrictOf(isObjectOf(predObj))({ - a: 0, - b: "a", - d: "invalid", - }), - false, - "Object have the same number of properties but an unknown property exists", - ); - }); - }); -}); - -Deno.test("isInstanceOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isInstanceOf(Date).name); - await assertSnapshot(t, isInstanceOf(class {}).name); - }); - await t.step("returns true on T instance", () => { - class Cls {} - assertEquals(isInstanceOf(Cls)(new Cls()), true); - assertEquals(isInstanceOf(Date)(new Date()), true); - assertEquals(isInstanceOf(Promise)(new Promise(() => {})), true); - }); - await t.step("with user-defined class", async (t) => { - class Cls {} - await testWithExamples(t, isInstanceOf(Cls)); - }); - await t.step("with Date", async (t) => { - await testWithExamples(t, isInstanceOf(Date), { validExamples: ["date"] }); - }); - await t.step("with Promise", async (t) => { - await testWithExamples(t, isInstanceOf(Promise), { - validExamples: ["promise"], - }); - }); - await t.step("returns proper type predicate", () => { - class Cls {} - const a: unknown = new Cls(); - if (isInstanceOf(Cls)(a)) { - assertType>(true); - } - - const b: unknown = new Date(); - if (isInstanceOf(Date)(b)) { - assertType>(true); - } - - const c: unknown = new Promise(() => {}); - if (isInstanceOf(Promise)(c)) { - assertType>>(true); - } - }); -}); - -Deno.test("isLiteralOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isLiteralOf("hello").name); - await assertSnapshot(t, isLiteralOf(100).name); - await assertSnapshot(t, isLiteralOf(100n).name); - await assertSnapshot(t, isLiteralOf(true).name); - await assertSnapshot(t, isLiteralOf(null).name); - await assertSnapshot(t, isLiteralOf(undefined).name); - await assertSnapshot(t, isLiteralOf(Symbol("asdf")).name); - }); - await t.step("returns proper type predicate", () => { - const pred = "hello"; - const a: unknown = "hello"; - if (isLiteralOf(pred)(a)) { - assertType>(true); - } - }); - await t.step("returns true on literal T", () => { - const pred = "hello"; - assertEquals(isLiteralOf(pred)("hello"), true); - }); - await t.step("returns false on non literal T", async (t) => { - const pred = "hello"; - await testWithExamples(t, isLiteralOf(pred)); - }); -}); - -Deno.test("isLiteralOneOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isLiteralOneOf(["hello", "world"]).name); - }); - await t.step("returns proper type predicate", () => { - const preds = ["hello", "world"] as const; - const a: unknown = "hello"; - if (isLiteralOneOf(preds)(a)) { - assertType>(true); - } - }); - await t.step("returns true on literal T", () => { - const preds = ["hello", "world"] as const; - assertEquals(isLiteralOneOf(preds)("hello"), true); - assertEquals(isLiteralOneOf(preds)("world"), true); - }); - await t.step("returns false on non literal T", async (t) => { - const preds = ["hello", "world"] as const; - await testWithExamples(t, isLiteralOneOf(preds)); - }); -}); - -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, - "Should return `isObjectOf`, if all predicates that", - ); - await assertSnapshot( - t, - isIntersectionOf([ - isString, - ]).name, - "Should return as is, if there is only one predicate", - ); - await assertSnapshot( - t, - isIntersectionOf([ - isFunction, - isObjectOf({ b: isString }), - ]).name, - ); - }); - await t.step("returns proper type predicate", () => { - const objPreds = [ - isObjectOf({ a: isNumber }), - isObjectOf({ b: isString }), - ] as const; - const funcPreds = [ - isFunction, - isObjectOf({ b: isString }), - ] as const; - const a: unknown = { a: 0, b: "a" }; - if (isIntersectionOf(objPreds)(a)) { - assertType>(true); - } - if (isIntersectionOf([isString])(a)) { - assertType>(true); - } - if (isIntersectionOf(funcPreds)(a)) { - assertType< - Equal< - typeof a, - & ((...args: unknown[]) => unknown) - & { b: string } - > - >(true); - } - }); - await t.step("returns true on all of T", () => { - const objPreds = [ - isObjectOf({ a: isNumber }), - isObjectOf({ b: isString }), - ] as const; - const funcPreds = [ - isFunction, - isObjectOf({ b: isString }), - ] as const; - const f = Object.assign(() => void 0, { b: "a" }); - assertEquals(isIntersectionOf(objPreds)({ a: 0, b: "a" }), true); - assertEquals(isIntersectionOf([isString])("a"), true); - assertEquals(isIntersectionOf(funcPreds)(f), 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"], - }); - }); - await t.step("returns false on non of T with any predicates", async (t) => { - const preds = [ - isFunction, - isObjectOf({ b: isString }), - ] as const; - assertEquals( - isIntersectionOf(preds)({ b: "a" }), - false, - "Not a function object", - ); - assertEquals( - isIntersectionOf(preds)(() => void 0), - false, - "Some properties does not exists in Function object", - ); - 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("isOneOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot(t, isOneOf([isNumber, isString, isBoolean]).name); - }); - await t.step("returns proper type predicate", () => { - const preds = [isNumber, isString, isBoolean] as const; - const a: unknown = [0, "a", true]; - if (isOneOf(preds)(a)) { - assertType>(true); - } - }); - await t.step("returns proper type predicate (#49)", () => { - const isFoo = isObjectOf({ foo: isString }); - const isBar = isObjectOf({ foo: isString, bar: isNumber }); - type Foo = PredicateType; - type Bar = PredicateType; - const preds = [isFoo, isBar] as const; - const a: unknown = [0, "a", true]; - if (isOneOf(preds)(a)) { - assertType>(true); - } - }); - await t.step("returns true on one of T", () => { - const preds = [isNumber, isString, isBoolean] as const; - assertEquals(isOneOf(preds)(0), true); - assertEquals(isOneOf(preds)("a"), true); - assertEquals(isOneOf(preds)(true), true); - }); - await t.step("returns false on non of T", async (t) => { - const preds = [isNumber, isString, isBoolean] as const; - await testWithExamples(t, isOneOf(preds), { - excludeExamples: ["number", "string", "boolean"], - }); - }); -}); - -Deno.test("isAllOf", async (t) => { - await t.step("returns properly named function", async (t) => { - await assertSnapshot( - t, - isAllOf([ - isObjectOf({ a: isNumber }), - isObjectOf({ b: isString }), - ]).name, - ); - }); - await t.step("returns proper type predicate", () => { - const preds = [ - isObjectOf({ a: isNumber }), - isObjectOf({ b: isString }), - ] as const; - const a: unknown = { a: 0, b: "a" }; - if (isAllOf(preds)(a)) { - assertType>(true); - } - }); - await t.step("returns true on all of T", () => { - const preds = [ - isObjectOf({ a: isNumber }), - isObjectOf({ b: isString }), - ] as const; - assertEquals(isAllOf(preds)({ a: 0, b: "a" }), true); - }); - await t.step("returns false on non of T", async (t) => { - const preds = [ - isObjectOf({ a: isNumber }), - isObjectOf({ b: isString }), - ] as const; - assertEquals( - isAllOf(preds)({ a: 0, b: 0 }), - false, - "Some properties has wrong type", - ); - assertEquals( - isAllOf(preds)({ a: 0 }), - false, - "Some properties does not exists", - ); - await testWithExamples(t, isAllOf(preds), { - excludeExamples: ["record"], - }); - }); -}); - -Deno.test("is", async (t) => { - const mod = await import("./is.ts"); - const casesOfAliasAndIsFunction = Object.entries(mod) - .filter(([k, _]) => k.startsWith("is") && k !== "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/maybe.ts b/maybe.ts new file mode 100644 index 0000000..87e52a0 --- /dev/null +++ b/maybe.ts @@ -0,0 +1,22 @@ +import type { Predicate } from "./type.ts"; + +/** + * Returns the input value if it satisfies the provided predicate, or `undefined` otherwise. + * + * ```ts + * import { is, maybe } from "@core/unknownutil"; + * + * const a: unknown = "hello"; + * const _: string = maybe(a, is.String) ?? "default value"; + * ``` + * + * @param x The value to be tested. + * @param pred The predicate function to test the value against. + * @returns The input value `x` if it satisfies the predicate, or `undefined` otherwise. + */ +export function maybe( + x: unknown, + pred: Predicate, +): T | undefined { + return pred(x) ? x : undefined; +} diff --git a/maybe_test.ts b/maybe_test.ts new file mode 100644 index 0000000..d15d831 --- /dev/null +++ b/maybe_test.ts @@ -0,0 +1,22 @@ +import { assertStrictEquals } from "@std/assert"; +import { maybe } from "./maybe.ts"; + +const x: unknown = Symbol("x"); + +function truePredicate(_x: unknown): _x is string { + return true; +} + +function falsePredicate(_x: unknown): _x is string { + return false; +} + +Deno.test("maybe", async (t) => { + await t.step("returns `x` as-is on true predicate", () => { + assertStrictEquals(maybe(x, truePredicate), x); + }); + + await t.step("returns `undefined` on false predicate", () => { + assertStrictEquals(maybe(x, falsePredicate), undefined); + }); +}); diff --git a/metadata.ts b/metadata.ts deleted file mode 100644 index 8c5da06..0000000 --- a/metadata.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { Predicate } from "./is.ts"; -import { inspect } from "./inspect.ts"; - -/** - * A type that has metadata. - */ -export type WithMetadata = { - __unknownutil_metadata: 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).__unknownutil_metadata; -} - -/** - * 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, { - __unknownutil_metadata: { - 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 362dc5e..8f2d971 100644 --- a/mod.ts +++ b/mod.ts @@ -6,7 +6,7 @@ * It provides `is` module for type predicate functions and `assert`, `ensure`, and * `maybe` helper functions. * - * ### is\* + * ### is\* and as\* * * Type predicate function is a function which returns `true` if a given value is * expected type. For example, `isString` (or `is.String`) returns `true` if a @@ -21,10 +21,12 @@ * } * ``` * - * For more complex types, you can use `is*Of` (or `is.*Of`) functions like: + * For more complex types, you can use `is*Of` (or `is.*Of`) functions and `as*` + * (or `as.*`) functions like: * * ```typescript * import { + * as, * is, * PredicateType, * } from "@core/unknownutil"; @@ -41,8 +43,8 @@ * }), * ]), * ), - * createTime: is.OptionalOf(is.InstanceOf(Date)), - * updateTime: is.OptionalOf(is.InstanceOf(Date)), + * createTime: as.Optional(is.InstanceOf(Date)), + * updateTime: as.Optional(is.InstanceOf(Date)), * }); * * // Infer the type of `Article` from the definition of `isArticle` @@ -73,7 +75,7 @@ * similar to TypeScript's `Pick`, `Omit`, `Partial`, `Required` utility types. * * ```typescript - * import { is } from "@core/unknownutil"; + * import { as, is } from "@core/unknownutil"; * * const isArticle = is.ObjectOf({ * title: is.String, @@ -87,8 +89,8 @@ * }), * ]), * ), - * createTime: is.OptionalOf(is.InstanceOf(Date)), - * updateTime: is.OptionalOf(is.InstanceOf(Date)), + * createTime: as.Optional(is.InstanceOf(Date)), + * updateTime: as.Optional(is.InstanceOf(Date)), * }); * * const isArticleCreateParams = is.PickOf(isArticle, ["title", "body", "refs"]); @@ -125,8 +127,8 @@ * const isArticlePatchParams = is.PartialOf(isArticleUpdateParams); * // is equivalent to * //const isArticlePatchParams = is.ObjectOf({ - * // body: is.OptionalOf(is.String), - * // refs: is.OptionalOf(is.ArrayOf( + * // body: as.Optional(is.String), + * // refs: as.Optional(is.ArrayOf( * // is.UnionOf([ * // is.String, * // is.ObjectOf({ @@ -239,6 +241,11 @@ * @module */ -export * from "./is.ts"; -export * from "./metadata.ts"; -export * from "./util.ts"; +export type * from "./type.ts"; + +export * from "./as/mod.ts"; +export * from "./is/mod.ts"; + +export * from "./assert.ts"; +export * from "./ensure.ts"; +export * from "./maybe.ts"; diff --git a/mod_test.ts b/mod_test.ts new file mode 100644 index 0000000..87a64f0 --- /dev/null +++ b/mod_test.ts @@ -0,0 +1,60 @@ +import { assertArrayIncludes } from "@std/assert"; +import { basename, globToRegExp } from "@std/path"; +import { parse } from "@std/jsonc"; +import { ensure } from "./ensure.ts"; +import { is } from "./is/mod.ts"; + +const excludes = [ + "mod.ts", + "*_test.ts", +]; + +Deno.test("JSR exports must have all `as` modules", async () => { + const moduleNames = await listModuleNames( + new URL(import.meta.resolve("./as")), + ); + const jsrExports = await loadJsrExports(); + assertArrayIncludes(Object.entries(jsrExports.exports), [ + ["./as", "./as/mod.ts"], + ...moduleNames.map(( + v, + ) => [`./as/${v.replaceAll("_", "-")}`, `./as/${v}.ts`]), + ]); +}); + +Deno.test("JSR exports must have all `is` modules", async () => { + const moduleNames = await listModuleNames( + new URL(import.meta.resolve("./is")), + ); + const jsrExports = await loadJsrExports(); + assertArrayIncludes(Object.entries(jsrExports.exports), [ + ["./is", "./is/mod.ts"], + ...moduleNames.map(( + v, + ) => [`./is/${v.replaceAll("_", "-")}`, `./is/${v}.ts`]), + ]); +}); + +async function listModuleNames(path: URL | string): Promise { + const patterns = excludes.map((p) => globToRegExp(p)); + const names: string[] = []; + for await (const entry of Deno.readDir(path)) { + if (!entry.isFile || !entry.name.endsWith(".ts")) continue; + if (patterns.some((p) => p.test(entry.name))) continue; + names.push(basename(entry.name, ".ts")); + } + return names; +} + +async function loadJsrExports(): Promise<{ exports: Record }> { + const text = await Deno.readTextFile( + new URL(import.meta.resolve("./deno.jsonc")), + ); + const json = ensure( + parse(text), + is.ObjectOf({ + exports: is.RecordOf(is.String, is.String), + }), + ); + return { exports: json.exports }; +} diff --git a/type.ts b/type.ts new file mode 100644 index 0000000..e53d27e --- /dev/null +++ b/type.ts @@ -0,0 +1,53 @@ +/** + * A type predicate function. + * + * ```ts + * import { as, is, type Predicate } from "@core/unknownutil"; + * + * type Person = { + * name: string; + * age: number; + * address?: string; + * }; + * const isPerson = is.ObjectOf({ + * name: is.String, + * age: is.Number, + * address: as.Optional(is.String), + * }) satisfies Predicate; + * ``` + */ +export type Predicate = (x: unknown) => x is T; + +/** + * A type predicated by {@linkcode Predicate}. + * + * ```ts + * import { as, is, type PredicateType } from "@core/unknownutil"; + * + * const isPerson = is.ObjectOf({ + * name: is.String, + * age: is.Number, + * address: as.Optional(is.String), + * }); + * + * type Person = PredicateType; + * // type Person = { + * // name: string; + * // age: number; + * // address?: string; + * // }; + * ``` + */ +export type PredicateType

= P extends Predicate ? T : never; + +/** + * JavaScript primitive types. + */ +export type Primitive = + | string + | number + | bigint + | boolean + | null + | undefined + | symbol;