diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..e69de29 diff --git a/.github/hooks/pre-commit b/.github/hooks/pre-commit index 53f5b2d..8db0579 100755 --- a/.github/hooks/pre-commit +++ b/.github/hooks/pre-commit @@ -168,23 +168,29 @@ function __pre_commit_hook() { # run fmt eval "$__fmt" || return $? + # run lint eval "$__lint" || return $? + # run test - eval "$__test" || return $? + local tests=$(eval "$__test" 2>&1) + if [[ $tests =~ ^.*("No test modules"|"no test specified").*$ ]]; then + : # noop + else + echo "$tests" + return $? + fi # still here? commit! return 0 } if [[ $1 =~ ^(-{0,2}(help|info|usage))|-(h|?)$ ]]; then usage - [[ $DEBUG =~ ^(1|true)$ ]] && debug return 1 fi precommit "$@" - [[ $DEBUG =~ ^(1|true)$ ]] && debug return $? } diff --git a/.github/settings.yml b/.github/settings.yml new file mode 100644 index 0000000..1e4bb47 --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,91 @@ +repository: + security_and_analysis: + advanced_security: + status: enabled + secret_scanning: + status: enabled + has_issues: true + has_projects: false + has_wiki: false + default_branch: main + allow_squash_merge: true + allow_merge_commit: false + allow_rebase_merge: false + allow_auto_merge: false + delete_branch_on_merge: true + enable_automated_security_fixes: true + enable_vulnerability_alerts: true +labels: + - name: "Complexity: High" + color: F9D0C4 + - name: "Complexity: Medium" + color: FEF2C0 + - name: "Complexity: Low" + color: D4C5F9 + - name: "Priority: Critical" + color: "000000" + - name: "Priority: High" + color: F9D0C4 + - name: "Priority: Medium" + color: FEF2C0 + - name: "Priority: Low" + color: D4C5F9 + - name: "State: Help Wanted" + color: 0E8A16 + - name: "State: Work in Progress" + color: "5319E7" + - name: "State: Conversation" + color: 27828C + - name: "State: Blocked" + color: "000000" + - name: "State: Postponed" + color: C5DEF5 + - name: "State: Unassigned" + color: BFDADC + - name: "Type: Enhancement" + color: 1D76DB + - name: "Type: Bug" + color: B60205 + - name: "Type: Infrastructure" + color: FBCA04 + - name: "Type: Dependencies" + color: D4C5F9 + - name: "Meta: Question" + color: BFDADC + - name: "Meta: Duplicate" + color: BFDADC + - name: "Meta: Abandoned" + color: "808080" +teams: + - name: core + permission: admin + - name: pm + permission: maintain + - name: dev + permission: push + - name: qa + permission: pull + - name: triage + permission: triage +branches: + - name: main + protection: + required_pull_request_reviews: + dismiss_stale_reviews: true + require_code_owner_reviews: true + required_approving_review_count: 1 + dismissal_restrictions: + users: [] + teams: [] + required_status_checks: + strict: true + contexts: [] + enforce_admins: false + required_linear_history: true + allow_force_pushes: false + allow_deletions: false + required_conversation_resolution: true + restrictions: + apps: [] + users: [] + teams: [] diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..3da8480 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,23 @@ +daysUntilStale: 14 +daysUntilClose: 3 +exemptLabels: + - "Priority: Critical" + - "Priority: High" + - "Priority: Medium" + - "Priority: Low" + - "State: Help Wanted" + - "State: Postponed" + - "State: Work in Progress" +staleLabel: "Meta: Abandoned" +markComment: > + This submission has been flagged as abandoned due to lack of activity. Is there something specific that is causing this to be delayed? + + Please leave a comment if this issue has not yet been resolved, otherwise it will be automatically closed in the near future. +closeComment: > + Following with the previous comment, this submission has been automatically closed. + + This was due to the following: + - No activity has been made for the past 14 days. + - There are no labels that indicate this is a protected issue. + + If this issue is still relevant and needs to be opened again in the future, please leave a comment elaborating upon the reason to continue with this issue. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c3fd201 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: "🦕 Deno CI" +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +defaults: + run: + shell: bash + working-directory: ./ + +permissions: + contents: read + +jobs: + deno: + runs-on: ${{matrix.os}} + # Continue in case the canary run does not succeed + continue-on-error: ${{matrix.canary}} + # env: + # DENO_DIR: .deno-cache + # CACHE_VERSION: ${{secrets.CACHE_VERSION}} + strategy: + matrix: + os: [ubuntu-20.04, macos-11, windows-2019] + deno: [v1.x] + canary: [false] + include: + - deno: canary + os: ubuntu-20.04 + canary: true + steps: + - name: "(env) gitconfig" + run: | + sudo git config --system core.autocrlf false + sudo git config --system core.eol lf + + - name: "(cache) setup" + uses: actions/cache@v2 + with: + path: ${{env.DENO_DIR}} + key: deno911-${{hashFiles('./{deps.ts,import_map.json}')}} + + - name: "(env) checkout" + uses: actions/checkout@v3 + + - name: "(setup) 🦕 ${{matrix.deno}}" + uses: denoland/setup-deno@main + with: + deno-version: ${{matrix.deno}} + + - name: "(cache) reload" + run: deno cache --reload --lock-write deps.ts + + - name: "(run) fmt" + run: deno fmt --unstable + + - name: "(run) lint" + run: deno lint --unstable --no-bad-types -- + + - name: "(run) test" + run: deno test -A --unstable --no-check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8046a10 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: "🦕 Release" +on: + push: + tags: + - "*" +jobs: + release: + runs-on: ubuntu-latest + # Continue in case the canary run does not succeed + continue-on-error: false + steps: + - name: "(env) gitconfig" + run: | + sudo git config --system core.autocrlf false + sudo git config --system core.eol lf + + - name: "(env) checkout" + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: "(setup) 🦕 deno" + uses: denoland/setup-deno@main + with: + deno-version: 1.x + + - run: deno run --unstable -A npm:conventional-github-releaser -p angular diff --git a/.gitignore b/.gitignore index 3f0719b..c9da9cf 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ Thumbs.db node deno.lock _mod.ts +_descriptors.json +__* +!__test__ diff --git a/.vscode/settings.json b/.vscode/settings.json index 4914402..7e65fd3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,5 +17,6 @@ "https://x.nest.land": true }, "deno.suggest.names": true, - "deno.suggest.completeFunctionCalls": true -} \ No newline at end of file + "deno.suggest.completeFunctionCalls": true, + "github-actions.workflows.pinned.refresh.enabled": true +} diff --git a/_test/deps.ts b/_test/deps.ts new file mode 100644 index 0000000..32d66a2 --- /dev/null +++ b/_test/deps.ts @@ -0,0 +1 @@ +import "https://deno.land/x/this@0.160.1/mod.ts"; diff --git a/_test/is.test.ts b/_test/is.test.ts new file mode 100644 index 0000000..963caa5 --- /dev/null +++ b/_test/is.test.ts @@ -0,0 +1,56 @@ +import { is } from "../mod.ts"; +import "./deps.ts"; + +const { assign, freeze } = Object; + +describe("is(value: unknown) => TypeName", () => { + it("should accurately return primitive type names", () => + ([ + ["string", "string"], + [0o644, "number"], + [true, "boolean"], + [0o644n, "bigint"], + [Symbol(), "symbol"], + ] as const).forEach( + ([input, output]) => assertStrictEquals(is(input as any), output), + )); + + it("should differentiate 'null' and 'undefined'", () => { + assertEquals(is(null), "null"); + assertEquals(is(undefined), "undefined"); + }); + + it( + "should differentiate 'number' and NaN", + () => assertEquals(is(NaN), "NaN"), + ); + + it( + "should differentiate 'number' and Infinity", + () => assertEquals(is(Infinity), "Infinity"), + ); + + it("can identify TemplateStringsArray (new)", () => { + const tsa: TemplateStringsArray = assign( + ["hello ", "world"], + freeze({ raw: ["hello ", "world"] }), + ); + assert(is.templateStringsArray(tsa)); + assertEquals(is(tsa), "TemplateStringsArray"); + }); + + it( + "can identify ArrayLike objects", + () => assertEquals(is(freeze({ "length": 1, "0": "hello!" })), "ArrayLike"), + ); + + it( + "should identify functions as 'Function'", + () => assertEquals(is(() => {}), "Function"), + ); + + it( + "should identify async functions as 'AsyncFunction'", + () => assertEquals(is(async () => {}), "AsyncFunction"), + ); +}); diff --git a/_util.ts b/_util.ts index 0fbcb66..69996f6 100644 --- a/_util.ts +++ b/_util.ts @@ -1,219 +1,8 @@ -import { - objectTypeNames, - primitiveTypeNames, - typedArrayTypeNames, -} from "./types.ts"; - -import type { - ArrayMethod, - ObjectTypeName, - Predicate, - PrimitiveTypeName, - TypedArrayTypeName, - TypeName, -} from "./types.ts"; - -export { objectTypeNames, primitiveTypeNames, typedArrayTypeNames }; - -export type { ObjectTypeName, PrimitiveTypeName, TypedArrayTypeName, TypeName }; - -export const predicateOnArray = ( - method: ArrayMethod, - predicate: Predicate, - values: unknown[], -) => { - if (typeof predicate !== "function") { - throw new TypeError(`Invalid predicate: ${JSON.stringify(predicate)}`); - } - - if (values.length === 0) { - throw new TypeError("Invalid number of values"); - } - - return method.call(values, predicate); -}; - -export function isTypedArrayName(name: unknown): name is TypedArrayTypeName { - return typedArrayTypeNames.includes(name as TypedArrayTypeName); -} - -export function isPrimitiveTypeName(name: unknown): name is PrimitiveTypeName { - return primitiveTypeNames.includes(name as PrimitiveTypeName); -} - -export function isOfType( - type: PrimitiveTypeName | "function", -) { - // deno-lint-ignore valid-typeof - return (value: unknown): value is T => typeof value === type; -} - -/** - * Objects - */ -export function isObjectTypeName(name: unknown): name is ObjectTypeName { - return objectTypeNames.includes(name as ObjectTypeName); -} - -export const { toString } = Object.prototype; - -const NODE_TYPE_ELEMENT = 1; - -const DOM_PROPERTIES_TO_CHECK: Array<(keyof HTMLElement)> = [ - "innerHTML", - "ownerDocument", - "style", - "attributes", - "nodeValue", -]; - -export const isDomElement = (value: unknown): value is HTMLElement => { - return (typeof value === "object" && value !== null) && - (value as HTMLElement).nodeType === NODE_TYPE_ELEMENT && - typeof ((value as HTMLElement).nodeName) === "string" && - !(toString.call(value).slice(8, -1) !== "Object") && - DOM_PROPERTIES_TO_CHECK.every((property) => property in value); -}; - -export const getObjectType = (value: unknown): ObjectTypeName | undefined => { - if (typeof value !== "object") return undefined; - - const objectTypeName = toString.call(value).slice(8, -1); - - if (/(HTML|SVG)\w+Element/.test(objectTypeName) && isDomElement(value)) { - return "HTMLElement"; - } - - if (isObjectTypeName(objectTypeName)) { - return objectTypeName; - } - - return "Object"; -}; - -export const isObjectOfType = (type: ObjectTypeName) => - function (value: unknown): value is T { - return getObjectType(value) === type; - }; - -/** - * Some few keywords are reserved, but we'll populate them for Node.js users. - * @see https://github.com/Microsoft/TypeScript/issues/2536 - */ -type union = Record; -// for (const prop of ["class_", "function_", "null_"] as const) { - -interface DeprecateOptions { - hide?: boolean; - seal?: boolean; -} - -export function deprecate( - target: T, - key: (keyof T) | Array, - options: DeprecateOptions, -): void; - -export function deprecate( - target: T, - ...arg: Array -): void; - -export function deprecate(target: T, ...rest: unknown[]): void { - const options: DeprecateOptions = {}; - const _keys: string[] = []; - if (Array.isArray(rest) && rest.length > 0) { - _keys.push( - ...(rest.filter((a) => typeof a === "string" || Array.isArray(a))), - ); - Object.assign( - options, - ...( - rest.filter((a) => typeof a === "object" && !Array.isArray(a)) - ), - ); - } - - const keyExists = Object.hasOwn.bind(target, target); - // assemble the list of keys to deprecate, ensure they exist - const keys = [_keys].flat(2).filter(keyExists); - // get all property descriptors for the target object - const descriptorMap = Object.getOwnPropertyDescriptors(target); - // only include the descriptors that we are concerned with deprecating - const descriptors = Object.entries(descriptorMap).filter( - ([key]) => keys.includes(key), - ); - - const { hide = true, seal = true } = (options || {}); - - for (const [key, desc] of descriptors) { - const descriptor: PropertyDescriptor = { - enumerable: !hide, - configurable: !seal, - }; - - if ( - ("value" in desc && typeof desc.value !== "undefined") || - ("writable" in desc && typeof desc.writable === "boolean") - ) { - const { value, writable = !seal } = desc; - descriptor.writable = seal ? false : writable; - Reflect.defineProperty(target, key, { ...descriptor, value }); - } else if ( - ("get" in desc && typeof desc.get === "function") || - ("set" in desc && typeof desc.set === "function") - ) { - const { get, set = undefined } = desc; - Reflect.defineProperty(target, key, { ...descriptor, get, set }); - } - } -} - -export function freeze(object: T): void; -export function freeze(...object: T): void; -export function freeze(...objects: unknown[]): void { - for (const o of objects) { - Object.freeze(o); - // freeze prototypes too - if (typeof o === "function" && o.prototype !== undefined) { - Object.freeze(o.prototype); - } - } -} - -/** Type-safe version of Object.assign */ -export function assign( - target: T, - source: U, -): asserts target is T & U; -/** Type-safe version of Object.assign */ -export function assign( - to: T, - s_0: U, - s_1: V, -): asserts to is T & U & V; -export function assign( - to: T, - s0: U, - s1: V, - s2: W, -): asserts to is T & U & V & W; -export function assign( - to: T, - s0: U, - s1: V, - s2: W, - s3: X, -): asserts to is T & U & V & W & X; -export function assign( - to: T, - s0: U, - s1: V, - s2: W, - s3: X, - s4: Y, -): asserts to is T & U & V & W & X & Y; - -export function assign(target: object, ...source: object[]) { - Object.assign(target, ...source); -} +export * from "./_util/ansi.ts"; +export * from "./_util/assign.ts"; +export * from "./_util/constants.ts"; +export * from "./_util/decorators.ts"; +export * from "./_util/predicates.ts"; +export * from "./_util/prototypes.ts"; +export * from "./_util/typenames.ts"; +export * from "./_util/types.d.ts"; diff --git a/_util/ansi.ts b/_util/ansi.ts new file mode 100644 index 0000000..c2a3d63 --- /dev/null +++ b/_util/ansi.ts @@ -0,0 +1,11 @@ +export const ansi = (s: string, pre = 1, post = 22) => + `\x1b[${pre}m${s}\x1b[${post}m`; + +ansi.bold = (s: string) => ansi(s, 1, 22); +ansi.dim = (s: string) => ansi(s, 2, 22); +ansi.italic = (s: string) => ansi(s, 3, 23); +ansi.underline = (s: string) => ansi(s, 4, 24); +ansi.inverse = (s: string) => ansi(s, 7, 27); +ansi.hidden = (s: string) => ansi(s, 8, 28); +ansi.strikethrough = (s: string) => ansi(s, 9, 29); +ansi.reset = (s: string) => ansi(s, 0, 0); diff --git a/_util/assign.ts b/_util/assign.ts new file mode 100644 index 0000000..36a0821 --- /dev/null +++ b/_util/assign.ts @@ -0,0 +1,17 @@ +import type { UnionToIntersection } from "./types.d.ts"; + +/** Type-safe version of Object.assign */ +export function assign( + target: T, + source: U, +): asserts target is T & U; + +/** Type-safe version of Object.assign */ +export function assign( + target: T, + ...source: U +): asserts target is T & UnionToIntersection; + +export function assign(target: object, ...source: object[]) { + Object.assign(target, ...source); +} diff --git a/_util/constants.ts b/_util/constants.ts new file mode 100644 index 0000000..f21d1f1 --- /dev/null +++ b/_util/constants.ts @@ -0,0 +1,9 @@ +export enum MetadataKey { + Alias = "metadata:is.alias", + Negated = "metadata:is.negated", + Deprecated = "metadata:is.deprecated", +} + +export const AliasSymbol = Symbol.for(MetadataKey.Alias); +export const NegatedSymbol = Symbol.for(MetadataKey.Negated); +export const DeprecatedSymbol = Symbol.for(MetadataKey.Deprecated); diff --git a/_util/decorators.ts b/_util/decorators.ts new file mode 100644 index 0000000..28732e5 --- /dev/null +++ b/_util/decorators.ts @@ -0,0 +1,5 @@ +export * from "./decorators/alias.decorator.ts"; +export * from "./decorators/deprecated.decorator.ts"; +export * from "./decorators/enumerable.decorator.ts"; +export * from "./decorators/freeze.decorator.ts"; +export * from "./decorators/inspect.decorator.ts"; diff --git a/_util/decorators/alias.decorator.ts b/_util/decorators/alias.decorator.ts new file mode 100644 index 0000000..002a5ef --- /dev/null +++ b/_util/decorators/alias.decorator.ts @@ -0,0 +1,79 @@ +import "https://deno.land/x/reflection@0.0.2/mod.ts"; + +export const MetadataKeyAlias = "metadata:alias"; + +export function alias( + alias: A, +): (target: T, propertyKey: string | symbol) => void; + +export function alias(alias: A): ( + target: T, + propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor, +) => void | TypedPropertyDescriptor; + +export function alias(alias: A): + | (( + target: T, + propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor, + ) => void | TypedPropertyDescriptor) + | ((target: T, propertyKey: string | symbol) => void) { + return (target: T, propertyKey: string | symbol, descriptor?: any) => { + if (typeof alias === "string") { + alias = (target as any)[alias] ?? null; + } else if (typeof alias === "symbol") { + alias = (target as any)[alias] ?? null; + } + + // sanity check + if (typeof alias !== "function" || alias === null) { + throw new TypeError( + `The @alias decorator must be provided with a valid identifier for a class method/property, or a string containing a valid identifier. Received ${typeof alias}: ${alias}`, + ); + } + + Reflect.defineMetadata( + MetadataKeyAlias, + alias, + target, + propertyKey, + ); + + const AliasSymbol = Symbol.for(MetadataKeyAlias); + const existingMetadata = + Reflect.getOwnPropertyDescriptor(target, AliasSymbol)?.value ?? {}; + + Reflect.defineProperty(target, AliasSymbol, { + value: { + ...existingMetadata, + [propertyKey]: alias, + }, + configurable: true, + writable: true, + enumerable: false, + }); + + if (typeof propertyKey === "symbol") { + return; + } + + if (typeof descriptor.value === "function") { + return { + ...descriptor, + value: function aliasedMethod() { + return descriptor.value?.apply(this, arguments); + }, + }; + } else { + return { + ...descriptor, + get: function aliasedGetter() { + return descriptor?.get?.() ?? descriptor?.value ?? ( + (target as any)[propertyKey] + ); + }, + }; + } + }; +} diff --git a/_util/decorators/deprecated.decorator.ts b/_util/decorators/deprecated.decorator.ts new file mode 100644 index 0000000..7ee932a --- /dev/null +++ b/_util/decorators/deprecated.decorator.ts @@ -0,0 +1,276 @@ +import "https://deno.land/x/reflection@0.0.2/mod.ts"; + +import { ansi } from "../ansi.ts"; +import type { Flatten } from "../types.d.ts"; +import { DeprecatedSymbol, MetadataKey } from "../constants.ts"; + +export interface DeprecateOptions { + hide?: boolean; + seal?: boolean; + since?: string | number | Date; + until?: string | number | Date; + substitute?: string | symbol | object; + message?: false | string | DeprecationMessageFn; + url?: string | URL; +} + +export interface DeprecateInit extends Record { + options?: DeprecateOptions; + parent?: string | object; + property: string | symbol; +} + +// deno-fmt-ignore +export interface DeprecationMessageFn { + (ctx: Flatten< + & { options?: Omit } + & Omit, + true + >): unknown | string; +} + +export const defaultDeprecateInit = { + options: { + hide: true, + seal: true, + } as DeprecateOptions, +} as DeprecateInit; + +/** + * Deprecation warning message helper. Accepts a parent string/object and a + * property name, and optionally a "since" version and substitution string. + * The data provided is used to generate a standardized deprecation message + * that is logged to the console / stderr. + * + * @param parent The parent string or object (eg. class name) + * @param property The name of the deprecated method/property + * @param [options.since] The version in which the property was deprecated + * @param [options.until] The version in which the property will be removed + * @param [options.substitute] Suggestion for a method/property to use instead + * @param [options.message] Custom message to display instead of the default, or `false` to disable message, or a function to generate a custom message from the deprecated method context. + * @param [options.hide] Whether to hide the deprecation message + * @param [options.seal] Whether to seal the parent object and hide the property from enumeration + * + * @example + * ```ts + * deprecationWarning("Deno", "readTextFile", { since: "1.0.0" }); + * // => `Deno.readTextFile is deprecated since 1.0.0.` + * ``` + * + * @example + * ```ts + * deprecationWarning("is", "function_", { since: "0.1.0", substitute: "function" }); + * // => `is.function_ is deprecated since 0.1.0. Please use is.function instead.` + * ``` + */ +export function deprecationWarning( + parent: string | object, + property: string | symbol, + options: DeprecateOptions = { ...defaultDeprecateInit.options }, +): void { + const { since, until, substitute, message } = options; + + let output = "", parentName = ""; + + if (typeof parent === "object" && parent !== null) { + parentName = {}.toString.call(parent).slice(8, -1) || + (parent as any)?.name || (parent as any)?.constructor?.name || null; + } else if (typeof parent === "string") { + parentName = String(parent); + } else { + parentName = ""; + } + + let { url = "" } = options; + try { + url = new URL(url ?? ""); + } catch { + url = ""; + } + + // early-exit if message is set to false + if (message === false) { + return; + } // if the message is a function, call it with the context and log the result + // (this allows custom messages to be generated on the fly!) + else if (typeof message === "function") { + const customOutput = message({ + parent, + parentName: parentName, + property, + options, + }); + + if (typeof customOutput === "string") { + output = customOutput; + } else if (customOutput === false) { + return; + } + } // otherwise, generate a default message from the context data + else { + const name = [parentName, property].filter(Boolean).join("."); + + output = `${ + typeof message === "string" + ? message + : `${ansi.bold(ansi.underline("DEPRECATED"))}: ${ + ansi.bold(ansi.strikethrough(name)) + } ${ + since + ? `was deprecated in ${ansi.underline(String(since))}` + : "is now deprecated" + }${ + until + ? `, and is ${ansi.bold("scheduled to be removed")} by ${ + /^(next)$/i.test(until + "") + ? "the next major release" + : ansi.underline(String(until)) + }` + : "" + }.${ + typeof substitute === "string" + ? ` Please use ${ + ansi.bold(ansi.underline( + [parentName, substitute].filter(Boolean).join("."), + )) + } instead.` + : typeof substitute === "function" + ? ` Please use ${ + ansi.bold(ansi.underline( + [parentName, substitute.name].filter(Boolean).join("."), + )) + } instead.` + : "" + }` + }${ + typeof url === "string" && url.length > 0 + ? `\n\n${ansi.dim(`More info: ${ansi.underline(url)}`)}` + : "" + }`; + } + + try { + console.warn(output); + } catch { /* noop */ } +} + +export function deprecated( + options: DeprecateOptions, +): (target: T, propertyKey: string | symbol) => void; + +export function deprecated( + options: DeprecateOptions, +): ( + target: T, + propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor, +) => void | TypedPropertyDescriptor; + +export function deprecated( + options: DeprecateOptions = { ...defaultDeprecateInit.options }, +): MethodDecorator | PropertyDecorator { + return (target: any, propertyKey: string | symbol, descriptor?: any) => { + const data: DeprecateOptions = { + ...defaultDeprecateInit.options, + ...options, + }; + + Reflect.defineMetadata( + MetadataKey.Deprecated, + data, + target, + propertyKey, + ); + + const existingMetadata = + Reflect.getOwnPropertyDescriptor(target, DeprecatedSymbol)?.value ?? {}; + + Reflect.defineProperty(target, DeprecatedSymbol, { + value: { + ...existingMetadata, + [propertyKey]: data, + }, + configurable: true, + writable: true, + enumerable: false, + }); + + if (typeof propertyKey === "symbol") return; + + const metadataObject = + Reflect.getOwnPropertyDescriptor(target, DeprecatedSymbol)?.value ?? + {}; + + const deprecatedData = Reflect.getMetadata( + MetadataKey.Deprecated, + target, + propertyKey, + ) ?? metadataObject[propertyKey]; + + const value = function deprecatedMethod(this: any) { + if (deprecatedData != null) { + deprecationWarning(target, propertyKey.toString(), deprecatedData); + } + + if (typeof descriptor.value === "function") { + return descriptor.value?.apply(this, arguments); + } else { + return descriptor.value ?? target[propertyKey]; + } + }; + + // mark it as deprecated + Object.defineProperties(value, { + "name": { + value: `${propertyKey.toString()}_deprecated`, + }, + [Symbol.toStringTag]: { + value: `${propertyKey.toString()} (deprecated)`, + }, + }); + + const desc = { + configurable: !options.seal, + enumerable: !options.hide, + writable: !options.seal, + value, + }; + + return desc; + }; +} + +export function isDeprecated( + target: object | Function | undefined, + key: string | symbol, + descriptor?: PropertyDescriptor, +): boolean { + if (target === undefined) return false; + + descriptor ??= Reflect.getOwnPropertyDescriptor(target, key); + if (descriptor?.value === undefined) return false; + + if ( + // check Metadata for this property key + Reflect.hasOwnMetadata(MetadataKey.Deprecated, target, key) || + Reflect.hasMetadata(MetadataKey.Deprecated, target, key) + ) return true; + + if ( + // check if the target has the hidden deprecated property + Reflect.has(target, DeprecatedSymbol) && + Reflect.has(Reflect.get(target, DeprecatedSymbol, target), key) + ) return true; + + // last ditch effort: + // check descriptor name/toStringTag for _deprecated suffix + if (typeof descriptor?.value === "function") { + if ( + String(descriptor?.value?.name).endsWith("_deprecated") || + String(descriptor?.value?.[Symbol.toStringTag]).endsWith("_deprecated") + ) return true; + } + + // nothing? then I guess it isn't deprecated. + return false; +} diff --git a/_util/decorators/enumerable.decorator.ts b/_util/decorators/enumerable.decorator.ts new file mode 100644 index 0000000..4fbc2f6 --- /dev/null +++ b/_util/decorators/enumerable.decorator.ts @@ -0,0 +1,24 @@ +import "https://deno.land/x/reflection@0.0.2/mod.ts"; + +export function enumerable(value: boolean) { + return function ( + target: T, + prop: string | symbol, + descriptor?: TypedPropertyDescriptor, + ): void | any { + // property decorator + if (!descriptor) { + descriptor ||= ( + Object.getOwnPropertyDescriptor(target, prop) || {} + ) as TypedPropertyDescriptor; + descriptor.enumerable = value; + Object.defineProperty(target, prop, descriptor); + return; + } + // method decorator + descriptor ||= {}; + descriptor.enumerable = value; + Object.defineProperty(target, prop, descriptor); + return descriptor; + }; +} diff --git a/_util/decorators/freeze.decorator.ts b/_util/decorators/freeze.decorator.ts new file mode 100644 index 0000000..3fe9647 --- /dev/null +++ b/_util/decorators/freeze.decorator.ts @@ -0,0 +1,73 @@ +import "https://deno.land/x/reflection@0.0.2/mod.ts"; + +export function freeze(object: T): T; +export function freeze(...object: T): T; +export function freeze(...object: T) { + const objects = object.map((o) => { + // freeze prototypes too + if (typeof o === "function" && o.prototype !== undefined) { + Object.freeze(o.prototype); + } + return Object.freeze(o); + }); + + return objects.length > 1 ? objects : objects[0] as Readonly; +} + +export function sealed(target: T): T | void; +export function sealed( + target: T, + property: K, +): void; +export function sealed< + T extends Function | Object, + K extends string | symbol, + D = unknown, +>( + target: T, + property: K, + descriptor: TypedPropertyDescriptor, +): TypedPropertyDescriptor | void; +export function sealed( + target: any, + property?: string | symbol, + descriptor?: PropertyDescriptor, +): any { + try { + if (typeof target === "function" && arguments.length === 1) { + Object.seal(target); + if (typeof target?.prototype < "u") { + Object.seal(target.prototype); + } + } else if (property) { + // if not descriptor, try to get one + descriptor ||= Object.getOwnPropertyDescriptor(target, property); + + // still no descriptor? try to make one + const value = target[property]; + descriptor ??= { value, writable: true, configurable: false }; + + // for data properties + if (descriptor?.value) { + descriptor.value = Object.seal(descriptor.value); + descriptor.writable = true; + } + // for accessor properties + if (descriptor?.set) { + descriptor.set = undefined; + Reflect.deleteProperty(descriptor, "set"); + Reflect.deleteProperty(descriptor, "writable"); + } + + // Object.seal(descriptor) sets configurable to false + descriptor.configurable = false; + + // return the property descriptor if it was provided in arguments + // otherwise return undefined + if (arguments.length > 2) { + return descriptor; // method decorator + } + return; + } + } catch { /* ignore */ } +} diff --git a/_util/decorators/inspect.decorator.ts b/_util/decorators/inspect.decorator.ts new file mode 100644 index 0000000..4821e90 --- /dev/null +++ b/_util/decorators/inspect.decorator.ts @@ -0,0 +1,88 @@ +import { getPropertyDescriptor } from "../prototypes.ts"; + +import "https://deno.land/x/reflection@0.0.2/mod.ts"; + +// @inspect decorator for functions and getters +export function inspect( + options: Deno.InspectOptions = {}, +) { + options = { + colors: true, + compact: false, + showHidden: true, + showProxy: true, + getters: true, + strAbbreviateSize: 50, + iterableLimit: 10, + depth: 5, + trailingComma: true, + ...options, + }; + + return function ( + target: T, + key: string | symbol, + descriptor?: PropertyDescriptor, + ): void | any { + descriptor ??= getPropertyDescriptor(target, key)!; + const original = descriptor?.value; + + const logger = (result: any, args?: any[]) => { + const name = ((target as any)?.name ?? "is") + + (typeof key === "symbol" ? `[${String(key)}]` : `.${key}`); + + console.log( + `[DEBUG][${Deno.inspect(new Date(), { colors: true })}]\n\n${ + name + (typeof original === "function" + ? `(${ + args?.map((a) => Deno.inspect(a, options)).join() + }) => ` + : " = ") + }${Deno.inspect(result, options)}\n`, + ); + }; + + if (typeof descriptor?.value === "function") { + descriptor.value = new Proxy(original, { + apply(t, thisArg, args) { + if (typeof original === "function") { + try { + const DEBUG = ("DEBUG" in Deno.env.toObject()); + const result = Reflect.apply(t, thisArg, args); + if (DEBUG) { + logger(result, args); + } + return result; + } catch (error) { + logger(error, args); + } + } + return original; + }, + }); + descriptor.writable = true; + } else { + Reflect.deleteProperty(descriptor, "value"); + Reflect.deleteProperty(descriptor, "writable"); + + descriptor.get = new Proxy(original, { + apply(t, thisArg, _a) { + try { + const DEBUG = ("DEBUG" in Deno.env.toObject()); + const result = original ?? Reflect.apply(t, thisArg, []); + if (DEBUG) { + logger(result); + } + return result; + } catch (error) { + logger(error); + } + }, + }); + } + + descriptor.configurable = true; + Reflect.defineProperty(target, key, descriptor); + if (arguments.length === 3) return descriptor; + }; +} diff --git a/_util/negated.ts b/_util/negated.ts new file mode 100644 index 0000000..66d0d04 --- /dev/null +++ b/_util/negated.ts @@ -0,0 +1,207 @@ +import { renameFunction } from "./prototypes.ts"; +import { DenoCustomInspect, IsDeprecated } from "./constants.ts"; + +export interface NegatedOptions { + /** Names/Symbols of properties to exclude from the proxied object. */ + excluded?: (string | symbol)[]; + + /** + * Custom value to use for the `Symbol.toStringTag` property. + * @default "is.not" + */ + toStringTag?: string; + + /** + * Create a revocable negated object using `Proxy.revocable`. This returns an + * additional property alongside `proxy` (named `revoke`), which will destroy + * the proxy instance and free it for garbage collection once it is invoked. + * + * **Note**: Proxy revocation is a one-way operation. It cannot be undone. + * @see https://mdn.io/Proxy.revocable + * @default false + */ + revocable?: boolean; + + /** + * Rename proxied functions for methods and getters/setters. Helps the final + * object appear slightly more indistinguishable from the original target. + * @default true + */ + maskMethodNames?: boolean; + + /** + * Sort the proxied method names in ascending alphabetic order. + */ + sorted?: boolean; +} + +export interface RevocableOptions extends NegatedOptions { + revocable: true; +} + +// deno-fmt-ignore +const isPromiseLike = (value: unknown): value is PromiseLike => ( + (typeof value === "function" || (typeof value === "object" && value !== null)) && (typeof (value as Promise)?.then === "function")); + +const isPromise = (value: unknown): value is Promise => + (value instanceof Promise) || (typeof value === "function" || + (typeof value === "object" && value !== null)) && ( + typeof (value as Promise)?.then === "function" && + typeof (value as Promise)?.catch === "function" && + typeof (value as Promise)?.finally === "function" + ); + +const isPromisable = ( + value: unknown, +): value is Promise | PromiseLike => + isPromise(value) || isPromiseLike(value); + +const isString = (value: unknown): value is string => ( + typeof value === "string" || value instanceof String +); + +/** + * Handle the results of one of the typecheck / assertion methods, applying + * the negation modifier whenever it seems appropriate. + */ +function handleResult( + this: any, + target: any, + args: unknown[], + result: unknown, +) { + const maybeNegated = (result: T): T | boolean => { + if (typeof result === "boolean" && !!this.$options.negated) { + this.$options.negated = false; + return !result; + } + return result; + }; + + if (typeof result === "function" && !isPromisable(result)) { + result = result.apply(target, args); + } + if (isPromisable(result)) { + return result.then(maybeNegated); + } + return maybeNegated(result); +} + +export function createNegated( + obj: T, +): { proxy: U }; +export function createNegated( + obj: T, + options: RevocableOptions, +): { proxy: U; revoke(): void }; +export function createNegated( + obj: T, + options?: NegatedOptions, +): { proxy: U }; +export function createNegated( + obj: T, + options: NegatedOptions = { + revocable: false, + maskMethodNames: true, + }, +): any { + options.sorted ??= true; + options.maskMethodNames ??= true; + options.toStringTag ??= "is.not"; + options.excluded ??= [ + // "assert", + // "assertType", + "typeName", + "negated", + "namespaceModule", + ]; + + // properties to exclude from the returned object + const deprecatedMetadata = obj[IsDeprecated] ?? {}; + const deprecatedProperties = Reflect.ownKeys(deprecatedMetadata).filter( + isString, + ); + + const excluded = Array.from( + new Set([ + "not", + "$options", + "assertType", + ...options?.excluded, + IsDeprecated, + DenoCustomInspect, + ...deprecatedProperties, + ]), + ); + + /** Check if a property is marked for exclusion from the proxy. */ + const isExcluded = (property: string | symbol): boolean => { + if (typeof property === "symbol") { + return excluded.includes(property); + } + + return excluded.some((pattern) => + typeof pattern !== "symbol" && new RegExp(pattern).test(property) + ); + }; + + // return (revocable ? Proxy.revocable : new Proxy)(this as unknown as U, + const handler: ProxyHandler = { + get(target, p, receiver) { + if (isExcluded(p)) { + return undefined; + } + + if (p === Symbol.toStringTag) { + return options.toStringTag ?? "is.not"; + } + + // equivalent to `value = target[p];` + let value = Reflect.get(target, p, target); + value ??= Reflect.get(target, p, receiver); + value ??= Reflect.getOwnPropertyDescriptor(target, p)?.value!; + + if (typeof value === "function") { + const proxiedMethod = function proxiedMethod( + this: any, + ...args: any[] + ) { + const result = (value as Function).apply( + this === receiver ? target : this, + args, + ); + return handleResult.call( + this === receiver ? target : this, + target, + args, + result, + ); + }; + + if (options?.maskMethodNames) { + renameFunction(proxiedMethod, p); + } + return proxiedMethod; + } + // otherwise... + return value; + }, + ownKeys(target) { + const keys = Array.from( + new Set(Reflect.ownKeys(target)).add(Symbol.toStringTag), + ).filter((key) => !excluded.includes(key)); + + return options?.sorted + ? keys.toSorted((a, b) => + String(typeof a === "symbol" ? a?.description ?? a : a).localeCompare( + String(typeof b === "symbol" ? b?.description ?? b : b), + ) + ) + : keys; + }, + }; + + return options?.revocable + ? Proxy.revocable(obj as unknown as U, handler) + : { proxy: new Proxy(obj as unknown as U, handler) }; +} diff --git a/_util/predicates.ts b/_util/predicates.ts new file mode 100644 index 0000000..6be43c9 --- /dev/null +++ b/_util/predicates.ts @@ -0,0 +1,152 @@ +import { + type ObjectTypeName, + objectTypeNames, + type PrimitiveTypeName, + primitiveTypeNames, + type TypedArrayTypeName, + typedArrayTypeNames, + type TypeName, + type TypeNameMap, +} from "./typenames.ts"; + +import type { ArrayMethod, Predicate, Primitive } from "./types.d.ts"; + +export type { TypeName, TypeNameMap }; + +export const predicateOnArray = ( + method: ArrayMethod, + predicate: Predicate, + values: unknown[], +) => { + if (typeof predicate !== "function") { + throw new TypeError(`Invalid predicate: ${JSON.stringify(predicate)}`); + } + + if (values.length === 0) { + throw new TypeError("Invalid number of values"); + } + + return method.call(values, predicate); +}; + +export const isAbsoluteMod2 = + (remainder: number) => (value: number): value is number => + Number.isInteger(value) && Math.abs(value % 2) === remainder; +/** + * Check if a value is of the valid length for its given type. + * @param value + * @returns `boolean` + */ +export const isValidLength = (value: unknown): value is number => + Number.isSafeInteger(value) && +(value as number) >= 0; + +export function isTypedArrayName(name: unknown): name is TypedArrayTypeName { + return typedArrayTypeNames.includes(name as TypedArrayTypeName); +} + +export function isPrimitiveTypeName(name: unknown): name is PrimitiveTypeName { + return primitiveTypeNames.includes(name as PrimitiveTypeName); +} + +export function isOfType( + type: PrimitiveTypeName | "function", +) { + // deno-lint-ignore valid-typeof + return (value: unknown): value is T => typeof value === type; +} + +/** + * Objects + */ +export function isObjectTypeName(name: unknown): name is ObjectTypeName { + return objectTypeNames.includes(name as ObjectTypeName); +} + +export const { toString } = Object.prototype; + +const NODE_TYPE_ELEMENT = 1; + +const DOM_PROPERTIES_TO_CHECK: Array<(keyof HTMLElement)> = [ + "style", +]; + +const SVG_PROPERTIES_TO_CHECK: Array<(keyof SVGElement)> = [ + "style", + "ownerSVGElement", +]; + +const GENERIC_PROPERTIES_TO_CHECK: Array<(keyof Element)> = [ + "innerHTML", + "ownerDocument", + "attributes", + "nodeValue", +]; + +export const isElement = ( + value: unknown, + properties = GENERIC_PROPERTIES_TO_CHECK, +): value is Element => { + const objectTypeName = toString.call(value).slice(8, -1); + return ( + (typeof value === "object" && value !== null) && + (value as Element).nodeType === NODE_TYPE_ELEMENT && + typeof ((value as Element).nodeName) === "string" && + (objectTypeName !== "Object") && + /Element/.test(objectTypeName) && + properties.every((property) => property in value) + ); +}; + +export const isDomElement = ( + value: unknown, + properties = DOM_PROPERTIES_TO_CHECK, +): value is HTMLElement => { + const objectTypeName = toString.call(value).slice(8, -1); + return ( + isElement(value) && + properties.every((property) => property in value) && + /HTML\w*Element/.test(objectTypeName) + ); +}; + +export const isSvgElement = ( + value: unknown, + properties = SVG_PROPERTIES_TO_CHECK, +): value is HTMLElement => { + const objectTypeName = toString.call(value).slice(8, -1); + return ( + isElement(value) && + properties.every((property) => property in value) && + /SVG\w*Element/.test(objectTypeName) + ); +}; + +export const getObjectType = ( + value: unknown, +): keyof TypeNameMap | undefined => { + if (typeof value !== "object") return undefined; + + const objectTypeName = toString.call(value).slice(8, -1); + + if (isDomElement(value)) { + return "HTMLElement"; + } else if (isSvgElement(value)) { + return "SVGElement"; + } else if (isElement(value)) { + return "Element"; + } + + if (isObjectTypeName(objectTypeName)) { + return objectTypeName; + } + + return "Object"; +}; + +export const isObjectOfType = < + T extends TypeNameMap[K extends keyof TypeNameMap ? K : "unknown"], + K extends keyof TypeNameMap = keyof TypeNameMap, +>(type: K) => + function (value: unknown): value is T { + return getObjectType(value) === type; + }; diff --git a/_util/prototypes.ts b/_util/prototypes.ts new file mode 100644 index 0000000..d28ed97 --- /dev/null +++ b/_util/prototypes.ts @@ -0,0 +1,325 @@ +import { mapEntries } from "../deps.ts"; +import { isDeprecated } from "./decorators/deprecated.decorator.ts"; +import type { Class } from "./types.d.ts"; + +/** + * Walk up the prototype chain and find the property descriptor for the + * given property + * + * @param target The parent object to search for the property on + * @param property The name of the property to search for + * @return The associated property descriptor, or `undefined` if none exists + */ +export function getPropertyDescriptor< + T extends object, + P extends PropertyKey, + D = unknown, +>(target: T | null, property: P): TypedPropertyDescriptor | undefined { + if (target === undefined || target === null) return; + + return ( + Reflect.getOwnPropertyDescriptor(target, property) || + getPropertyDescriptor(Reflect.getPrototypeOf(target), property) + ); +} + +export function isExtensionOf( + childClass: Class, + parentClass: Class, +) { + return (childClass.prototype instanceof parentClass) || + (parentClass.prototype instanceof childClass); +} + +export function formatPropertyDescriptors( + target: object | Function | undefined, + { sealed = false, hideDeprecated = true, parent }: { + parent?: object | Function | undefined; + sealed?: boolean; + hideDeprecated?: boolean; + } = {}, +) { + if (target === undefined) { + return; + } + + parent ??= target; + const ignoredProperties = [ + "prototype", + "length", + "name", + "caller", + "arguments", + ]; + + const descriptors = mapEntries( + Object.getOwnPropertyDescriptors(target), + ([key, descriptor]) => { + if (ignoredProperties.includes(key)) { + return [key, descriptor]; + } + + if ( + isDeprecated(target, key, descriptor) || + isDeprecated(parent, key, descriptor) + ) { + return [key, { + ...descriptor, + configurable: !sealed, + enumerable: !hideDeprecated, + ...(descriptor.value ? { writable: !sealed } : {}), + }]; + } + + return [key, { + ...descriptor, + configurable: !sealed, + enumerable: true, + ...(descriptor.value ? { writable: !sealed } : {}), + }]; + }, + ); + + Object.defineProperties(target, descriptors); +} + +export interface Flags extends Record { + object?: any; + negate?: boolean; + message?: string; + // custom + [key: string | symbol]: unknown; +} + +export const kFlags = Symbol("is:flags"); +export const kObject = Symbol("is:object"); +export const kMethods = Symbol("is:methods"); + +export type Flagged = { + [kFlags]?: Flags | undefined; +} & T; + +export function flag( + obj: Flagged, + key: keyof Flags, + value?: V, +): V | void { + const flags: Flags = obj[kFlags] || (obj[kFlags] = Object.create(null)); + if (value) { + flags[key] = value as V; + } else { + return flags[key] as V; + } +} + +export function getActual(obj: T, ...args: any[]) { + return args.length > 4 + ? args[4] + : (Reflect.has(obj, kObject)) + ? Reflect.get(obj, kObject, obj) + : undefined; +} + +const config = { + includeStack: false, + showDiff: true, + truncateThreshold: 40, + useProxy: true, + proxyExcludedKeys: ["then", "catch", "inspect", "toJSON"], +}; + +function inspect(obj: Flagged) { + const str = Deno.inspect(obj); + const type2 = Object.prototype.toString.call(obj); + + if (config.truncateThreshold && str.length >= config.truncateThreshold) { + if (typeof obj === "function" || obj instanceof Function) { + return !obj.name || obj.name === "" + ? "[Function]" + : `[Function: ${obj.name}]`; + } else if (Array.isArray(obj)) { + return `[ Array(${obj.length}) ]`; + } else if ( + typeof obj === "object" && obj !== null && type2 === "[object Object]" + ) { + const keys = Object.keys(obj); + const kstr = keys.length > 2 + ? keys.splice(0, 2).join(", ") + ", ..." + : keys.join(", "); + return `{ Object (${kstr}) }`; + } else { + return str; + } + } else { + return str; + } +} + +export function getMessage( + obj: Flagged, + message: string | (() => string), + negatedMessage: string | (() => string), + expected: any, +): string; + +export function getMessage(...args: any[]) { + const [obj, ...rest] = args; + let [msg, negatedMsg, expected = true] = rest; + + const negated = flag(obj, "negate"); + const valueOf = flag(obj, "object") as any; + const flagMsg = flag(obj, "message"); + + const actual = getActual(obj, rest); + + const message = + ((msg = negated ? negatedMsg : msg), + String((typeof msg === "function" ? msg() : msg) || "")) + .replace(/[#\$]?\{(this|obj(ect)?|target|val(ue)?)\}/ig, inspect(valueOf)) + .replace(/[#\$]?\{(act(ual)?|received|input)\}/ig, inspect(actual)) + .replace(/[#\$]?\{(exp(ect)?|allow|accept)(ed)?\}/ig, inspect(expected)); + + return [flagMsg, message].filter(Boolean).join(": "); +} + +function isProxyEnabled() { + return config?.useProxy && typeof Proxy < "u" && typeof Reflect < "u"; +} + +const builtins = [kFlags, kMethods, kObject, "assert"]; + +export function proxify( + obj: T, + nonChainableMethodName?: string, +) { + if (!isProxyEnabled()) return obj; + + return new Proxy(obj, { + get: function $getter(target, property) { + if ( + typeof property === "string" && + config.proxyExcludedKeys.indexOf(property) === -1 && + !Reflect.has(target, property) + ) { + if (nonChainableMethodName || builtins.indexOf(property) > -1) { + throw Error( + `Invalid property: "${nonChainableMethodName}.${property}". See docs for proper usage of "${nonChainableMethodName}".`, + ); + } + let suggestion = null; + let suggestionDistance = 4; + + Reflect.ownKeys(target).forEach((prop) => { + if (typeof prop === "string" && !Reflect.has(target, prop)) { + const dist = stringDistanceCapped( + String(property), + String(prop), + +suggestionDistance, + ); + if (dist < suggestionDistance) { + suggestion = prop; + suggestionDistance = dist; + } + } + }); + throw TypeError( + `Invalid property: "${property}".${ + suggestion ? ` Did you mean "${suggestion}"?` : "" + }`, + ); + } + if (!flag(target, "lockSsfi") && builtins.indexOf(property) === -1) { + flag(target, "ssfi", $getter); + } + return Reflect.get(target, property); + }, + }); +} + +function stringDistanceCapped( + strA: string, + strB: string, + cap: number, +): number { + if (Math.abs(strA.length - strB.length) >= cap) { + return cap; + } + + const memo = []; + + for (let i = 0; i <= strA.length; i++) { + memo[i] = Array(strB.length + 1).fill(0); + memo[i][0] = i; + } + for (let j = 0; j < strB.length; j++) { + memo[0][j] = j; + } + + for (let i = 1; i <= strA.length; i++) { + const ch = strA.charCodeAt(i - 1); + for (let j = 1; j <= strB.length; j++) { + if (Math.abs(i - j) >= cap) { + memo[i][j] = cap; + continue; + } + memo[i][j] = Math.min( + memo[i - 1][j] + 1, + memo[i][j - 1] + 1, + memo[i - 1][j - 1] + + (ch === strB.charCodeAt(j - 1) ? 0 : 1), + ); + } + } + + return memo[strA.length][strB.length]; +} + +/** Attempt to rename a function by changing the object's `name` property. */ +export function renameFunction(fn: Function, value: string | symbol) { + if (typeof value === "symbol" && value.description) { + value = value.description; + } + if (typeof value === "string") { + if (typeof fn === "function") { + const desc = getPropertyDescriptor(fn, "name") ?? { + configurable: true, + writable: false, + enumerable: false, + }; + // remove the old name descriptor + Reflect.deleteProperty(fn, "name"); + // define the new name descriptor + Reflect.defineProperty(fn, "name", { ...desc, value }); + } + } + return fn; +} + +// export function isDeprecated( +// target: object | Function | undefined, +// key: string | symbol, +// descriptor?: PropertyDescriptor, +// ): boolean { +// if (target === undefined) return false; +// if (descriptor?.value === undefined) return false; +// if (typeof descriptor?.value === "function") { +// if ( +// String(descriptor?.value?.name).endsWith("_deprecated") || +// String(descriptor?.value?.[Symbol.toStringTag]).endsWith("_deprecated") +// ) return true; +// } +// const DeprecatedSymbol = Symbol.for(MetadataKeyDeprecated); +// const deprecatedList = (target as any)[DeprecatedSymbol] ?? {}; + +// const hasDeprecatedMetadata = Reflect.hasMetadata( +// MetadataKeyDeprecated, +// target, +// key, +// ); + +// return ( +// (deprecatedList && (key in deprecatedList)) || +// (DeprecatedSymbol in (target as any)?.[key] ?? {}) || +// (!!hasDeprecatedMetadata) +// ); +// } diff --git a/_util/typenames.ts b/_util/typenames.ts new file mode 100644 index 0000000..76930fe --- /dev/null +++ b/_util/typenames.ts @@ -0,0 +1,348 @@ +import { NodeJS } from "../types.ts"; +import type { + AccessorDescriptor, + AsyncFunction, + Buffer, + Class, + DataDescriptor, + Infinity, + LastOf, + MapIterator, + Module, + NegativeInfinity, + Numeric, + ObservableLike, + PositiveInfinity, + ReadonlyAccessorDescriptor, + ReadonlyDataDescriptor, + SetIterator, + Zero, +} from "./types.d.ts"; + +/** + * TypedArrays + */ +export const typedArrayTypeNames = [ + "Int8Array", + "Uint8Array", + "Uint8ClampedArray", + "Int16Array", + "Uint16Array", + "Int32Array", + "Uint32Array", + "Float32Array", + "Float64Array", + "BigInt64Array", + "BigUint64Array", +] as const; + +export const objectTypeNames = [ + "Function", + "Generator", + "GeneratorFunction", + "AsyncFunction", + "AsyncGenerator", + "AsyncGeneratorFunction", + "Observable", + "Array", + "ArrayLike", + "ReadonlyArray", + "Buffer", + "Blob", + "Object", + "RegExp", + "RegExpMatchArray", + "RegExpExecArray", + "TemplateStringsArray", + "Date", + "Error", + "Iterable", + "Iterator", + "IterableIterator", + "AsyncIterable", + "AsyncIterator", + "AsyncIterableIterator", + "Map Iterator", + "Set Iterator", + "String Iterator", + "Array Iterator", + "Map", + "Set", + "WeakMap", + "WeakSet", + "WeakRef", + "ArrayBuffer", + "SharedArrayBuffer", + "DataView", + "Module", + "Promise", + "FormData", + "Headers", + "Request", + "Response", + "ReadableStream", + "WritableStream", + "TransformStream", + "Reader", + "Writer", + "FileInfo", + "URLSearchParams", + "URLPattern", + "URL", + "HTMLElement", + "SVGElement", + "Element", + "NaN", + // TypedArrayTypeNames + "Int8Array", + "Uint8Array", + "Uint8ClampedArray", + "Int16Array", + "Uint16Array", + "Int32Array", + "Uint32Array", + "Float32Array", + "Float64Array", + "BigInt64Array", + "BigUint64Array", +] as const; + +/** + * Primitives + */ +export const primitiveTypeNames = [ + "null", + "undefined", + "string", + "number", + "bigint", + "boolean", + "symbol", + "object", + "function", +] as const; + +export type PrimitiveTypeName = (typeof primitiveTypeNames)[number]; +export type TypedArrayTypeName = (typeof typedArrayTypeNames)[number]; +export type ObjectTypeName = (typeof objectTypeNames)[number]; +export namespace TypeNameMap { + export interface Primitives { + "null": null; + "undefined": undefined; + "string": string; + "number": number; + "bigint": bigint; + "boolean": boolean; + "symbol": symbol; + "object": object; + "function": Function; + } + + export interface Objects { + "Function": Function; + "AsyncFunction": AsyncFunction; + "Promise": Promise; + "PromiseLike": PromiseLike; + "Array": Array; + "Object": Object; + "RegExp": RegExp; + "Date": Date; + "Error": Error; + "Map": Map; + "Set": Set; + } + + export interface Additional { + "Module": Module; + "Observable": ObservableLike; + "WeakMap": WeakMap; + "WeakSet": WeakSet; + "WeakRef": WeakRef; + } + + export interface StructuredData { + // TypedArrays + "Int8Array": Int8Array; + "Uint8Array": Uint8Array; + "Uint8ClampedArray": Uint8ClampedArray; + "Int16Array": Int16Array; + "Uint16Array": Uint16Array; + "Int32Array": Int32Array; + "Uint32Array": Uint32Array; + "Float32Array": Float32Array; + "Float64Array": Float64Array; + "BigInt64Array": BigInt64Array; + "BigUint64Array": BigUint64Array; + // ArrayBufferLike + "Buffer": Buffer; + "ArrayBuffer": ArrayBuffer; + "SharedArrayBuffer": SharedArrayBuffer; + "DataView": DataView; + "Blob": Blob; + } + + export interface ArrayLikes { + "RegExpMatchArray": RegExpMatchArray; + "RegExpExecArray": RegExpExecArray; + "TemplateStringsArray": TemplateStringsArray; + "ArrayLike": ArrayLike; + "ReadonlyArray": ReadonlyArray; + } + + export interface Iterators { + "Generator": Generator; + "GeneratorFunction": GeneratorFunction; + "AsyncGenerator": AsyncGenerator; + "AsyncGeneratorFunction": AsyncGeneratorFunction; + "Iterable": Iterable; + "Iterator": Iterator; + "IterableIterator": IterableIterator; + "AsyncIterable": AsyncIterable; + "AsyncIterator": AsyncIterator; + "AsyncIterableIterator": AsyncIterableIterator; + "Map Iterator": MapIterator; + "Set Iterator": SetIterator; + "String Iterator": IterableIterator; + "Array Iterator": IterableIterator; + } + + export interface WebAPI { + "FormData": FormData; + "Headers": Headers; + "Request": Request; + "Response": Response; + "URL": URL; + "URLSearchParams": URLSearchParams; + "URLPattern": URLPattern; + } + + export interface NodeAPI { + "Buffer": NodeJS.Buffer; + "Readable": NodeJS.Readable; + "Writable": NodeJS.Writable; + "ReadableStream": NodeJS.ReadableStream; + "WritableStream": NodeJS.WritableStream; + "Stream": NodeJS.Stream; + "EventEmitter": NodeJS.EventEmitter; + } + + export interface DenoAPI { + "Buffer": Buffer; + "Reader": Deno.Reader; + "Writer": Deno.Writer; + "Closer": Deno.Closer; + "Listener": Deno.Listener; + "ReaderWriter": Deno.Reader & Deno.Writer; + "ReaderWriterCloser": Deno.Reader & Deno.Writer & Deno.Closer; + "FileInfo": Deno.FileInfo; + "FsFile": Deno.FsFile; + "DirEntry": Deno.DirEntry; + "Permissions": Deno.Permissions; + "Process": Deno.Process; + } + + export interface DOM { + "HTMLElement": HTMLElement; + "SVGElement": SVGElement; + "Element": Element; + } + + export interface Numerics { + "Infinity": Infinity; + "PositiveInfinity": PositiveInfinity; + "NegativeInfinity": NegativeInfinity; + "NaN": unknown; + "NumericString": number | `${number}`; + // "Integer": Integer; + // "Negative": Negative; + // "NonNegative": NonNegative; + // "Positive": NonNegative; + // "NegativeInteger": NegativeInteger; + // "NonNegativeInteger": NonNegativeInteger; + // "PositiveInteger": NonNegativeInteger; + // "Float": Float; + // "NegativeFloat": NegativeFloat; + "Zero": Zero; + } + + export interface Streams { + "ReadableStream": ReadableStream; + "WritableStream": WritableStream; + "TransformStream": TransformStream; + } + + export interface Descriptors { + "PropertyDescriptor": PropertyDescriptor; + "AccessorDescriptor": AccessorDescriptor; + "DataDescriptor": DataDescriptor; + "ReadonlyAccessorDescriptor": ReadonlyAccessorDescriptor; + "ReadonlyDataDescriptor": ReadonlyDataDescriptor; + } + + export interface Misc { + "unknown": unknown; + } +} + +export interface TypeNameMap + extends + TypeNameMap.Additional, + TypeNameMap.ArrayLikes, + TypeNameMap.DOM, + TypeNameMap.Descriptors, + TypeNameMap.Iterators, + TypeNameMap.Numerics, + TypeNameMap.Objects, + TypeNameMap.Primitives, + TypeNameMap.Streams, + TypeNameMap.StructuredData, + TypeNameMap.WebAPI, + TypeNameMap.DenoAPI, + TypeNameMap.Misc { + /* extended typename map */ +} + +export type TypeName = keyof TypeNameMap; +export type TypeNameAlt = + | ObjectTypeName + | TypedArrayTypeName + | PrimitiveTypeName; + +type GetTypeNameImpl = { + [K in keyof TypeNameMap]: TypeNameMap[K] extends T ? K + : never; +}[keyof TypeNameMap]; + +// type-world implementation of getTypeName() +// deno-fmt-ignore +export type GetTypeName = + | T extends Promise ? "Promise" + : T extends PromiseLike ? "PromiseLike" + : T extends AsyncGenerator ? "AsyncGenerator" + : T extends () => AsyncGenerator ? "AsyncGenerator" + : T extends AsyncIterable ? "AsyncIterable" + : T extends () => AsyncIterable ? "AsyncIterable" + : T extends AsyncIterableIterator ? "AsyncIterableIterator" + : T extends () => AsyncIterableIterator ? "AsyncIterableIterator" + : T extends AsyncIterator ? "AsyncIterator" + : T extends IterableIterator ? "IterableIterator" + : T extends Iterable ? "Iterable" + : T extends Iterator ? "Iterator" + : T extends (...a: unknown[]) => Promise ? "AsyncFunction" + : T extends Generator ? "Generator" + : T extends () => Generator ? "Generator" + : T extends symbol ? "symbol" + : T extends Map ? "Map" + : T extends WeakMap ? "WeakMap" + : T extends Set ? "Set" + : T extends WeakSet ? "WeakSet" + : T extends RegExp ? "RegExp" + : T extends Date ? "Date" + : T extends readonly unknown[] ? "ReadonlyArray" + : T extends Class ? "Class" + : T extends (...a: unknown[]) => unknown ? "Function" + : T extends object ? "Object" + : [T] extends [never] ? "undefined" + : GetTypeNameImpl extends infer U + ? [U] extends [never] ? TypeName : `${LastOf & string}` + : never; diff --git a/_util/types.d.ts b/_util/types.d.ts new file mode 100644 index 0000000..a90e0cd --- /dev/null +++ b/_util/types.d.ts @@ -0,0 +1,807 @@ +export { Buffer } from "../deps.ts"; + +export type Falsy = false | 0 | 0n | "" | null | undefined; + +export type ArrayMethod = ( + fn: (value: unknown, index: number, array: unknown[]) => boolean, + thisArg?: unknown, +) => boolean; + +export interface ArrayAssertion { + (element: unknown): asserts element is T; + (element: unknown, index: number): asserts element is T; + (element: unknown, index: number, array: T[]): asserts element is T; +} + +export type SubsetOf = { + [K in keyof T as U[K] extends never ? never : K]: V; +}; + +export type SupersetOf = { + [K in keyof T | string]: V; +}; + +export type ExactShapeOf = { + [K in keyof T]: T[K]; +}; + +export type IsAny = 0 extends (1 & T) ? Y : N; +export type NotAny = true extends IsAny ? N : Y; + +/** + * Matches a `class` constructor. + * @see https://mdn.io/Classes. + */ +export interface Class< + Proto = unknown, + Args extends any[] = any[], +> extends Constructor { + readonly prototype: Proto; +} + +export interface Constructor { + new (...args: Args): Proto; +} + +export type Predicate = (value: unknown) => value is T; + +/** + * Matches any primitive value. + * @see https://mdn.io/Primitive + */ +export type Primitive = + | null + | undefined + | string + | number + | boolean + | symbol + | bigint; + +/** + * @see {@link Printable} for more information on Printable Primitive types. + */ +export type MaybePrintable = Exclude; + +/** + * The "Printable" Primitives - `string`, `number`, `boolean`, `bigint` - are + * the subset of the Primitive types that can be printed in Template Literal + * types (a feature of TypeScript 4.1+). + * + * _Technically_ `null` and `undefined` are also printable, but only as the + * literal strings `"null"` and `"undefined"`, respectively. As such, they + * are not included in this type. + * + * @see {@linkcode MaybePrintable} if you need to include `null` and `undefined` in the Printable type for your use case. + */ +export type Printable = NonNullable; + +/** + * Matches any [typed array](https://mdn.io/TypedArray). + * @see https://mdn.io/TypedArray + */ +export type TypedArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array + | BigInt64Array + | BigUint64Array; + +// built-ins + +// deno-fmt-ignore +export interface MapIterator extends + IterableIterator { + readonly [Symbol.toStringTag]?: "Map Iterator"; +} + +// deno-fmt-ignore +export interface SetIterator extends IterableIterator { + readonly [Symbol.toStringTag]?: "Set Iterator"; +} + +// deno-fmt-ignore +export interface StringIterator extends IterableIterator { + readonly [Symbol.toStringTag]?: "String Iterator"; +} + +// deno-fmt-ignore +export interface ArrayIterator extends IterableIterator { + readonly [Symbol.toStringTag]?: "Array Iterator"; +} + +declare global { + interface Set { + keys(): SetIterator; + values(): SetIterator; + entries(): SetIterator<[T, T]>; + [Symbol.iterator](): SetIterator; + } + interface Map { + keys(): MapIterator; + values(): MapIterator; + entries(): MapIterator<[K, V]>; + [Symbol.iterator](): MapIterator<[K, V]>; + } + interface String { + [Symbol.iterator](): StringIterator; + } + interface Array { + keys(): ArrayIterator; + values(): ArrayIterator; + entries(): ArrayIterator<[number, T]>; + [Symbol.iterator](): ArrayIterator; + } +} + +/** + * Describes the general shape of a JavaScript / TypeScript module that was + * imported using the namespace syntax, e.g. `import * as is from ...` + * + * @see {@linkcode is.module} + * @see {@linkcode is.namespaceModule} + */ +export interface Module { + [name: string]: unknown; + exports?: { + [name: string]: unknown; + default?: unknown; + }; + default?: unknown; + readonly __esModule?: true; + readonly [Symbol.toStringTag]: "Module"; +} + +type Accessors = { + get: () => T; + set: (value: T | {}) => void; +}; + +export type AccessorDescriptor = Flatten< + & { + enumerable?: boolean; + configurable?: boolean; + } + & ( + | Accessors + | PartialByKeys, "set"> + ) +>; + +export type ReadonlyAccessorDescriptor = Flatten< + & Omit, "set"> + & RequiredByKeys, "get"> + & { set?: never } +>; + +export type DataDescriptor = Flatten< + & { + enumerable?: boolean; + configurable?: boolean; + } + & ( + | { writable: boolean; value: T } + | { writable: boolean; value?: T } + | { writable?: boolean; value: T } + ) +>; + +export type ReadonlyDataDescriptor = Flatten< + & Omit, "writable" | "configurable"> + & RequiredByKeys, "value"> + & { writable?: false; configurable?: false } +>; + +/** + * Matches a value that is like an Observable. + * @see https://github.com/tc39/proposal-observable + */ +export interface ObservableLike { + subscribe(observer: (value: unknown) => void): void; + [Symbol.observable](): ObservableLike; +} + +export interface WeakRef { + readonly [Symbol.toStringTag]: "WeakRef"; + deref(): T | undefined; +} + +export type AsyncFunction = ( + ...args: any[] +) => Promise; + +export interface RGB { + r: number; + g: number; + b: number; +} +export interface RGBA extends RGB { + a: number; +} + +// numeric + +export type Zero = 0 | 0n; +export type PositiveInfinity = 1e999; +export type NegativeInfinity = -1e999; +export type Infinity = PositiveInfinity | NegativeInfinity; +export type Numeric = bigint | number | `${bigint}` | `${number}`; + +type Digit = + | 0 + | 1 + | 2 + | 3 + | 4 + | 5 + | 6 + | 7 + | 8 + | 9; + +export type Printable = string | number | bigint | boolean | null | undefined; + +export type NormalizeNumber< + N extends Printable, + T extends Printable = ParseInt, + I extends unknown[] = [], +> = `${T}` extends `${infer A}${infer B}` ? `${A extends + | Digit + | `${Digit}` + | ( + I["length"] extends 0 ? "-" /* allow negative for first digit only */ + : never + ) ? A + : never}${NormalizeNumber, [...I, unknown]>}` + : T; + +export type NumberToBigInt = `${T}` extends + `${infer N extends bigint}` ? N : T; + +export type ParseInt = `${T}` extends + `${infer N extends number}` ? `${N}` extends `${bigint}` ? N + : `${N}` extends `${infer MSB}.${infer _}` ? ParseInt + : N + : T; + +// deno-fmt-ignore +export type IsNaN = + | `${T}` extends `-${infer A extends number}` ? IsNaN + : `${T}` extends `${NormalizeNumber}` ? false + : `${ParseInt}` extends `${number}` + ? [NormalizeNumber] extends [never] + ? true + : false + : [T] extends [Infinity] ? false + : Strict extends true ? never : true; + +export type NumberIsNaN = IsNaN; + +// deno-fmt-ignore +export type IsInfinity = + | [T] extends [Infinity] ? true + : `${ParseInt}` extends Infinity ? true + : false; + +/** + * @category Numeric + */ +export declare type Finite = T extends + PositiveInfinity | NegativeInfinity ? never : T; + +/** + * @category Numeric + */ +export declare type Infinite = T extends + PositiveInfinity | NegativeInfinity ? T : never; + +/** + * @category Numeric + */ +export declare type Integer = `${T}` extends `${bigint}` ? T + : never; + +/** + * @category Numeric + */ +export declare type Float = T extends Integer ? never + : T; + +/** + * A negative (`-∞ < x < 0`) `number` that is not an integer. + * Equivalent to `Negative>`. + * + * Use-case: Validating and documenting parameters. + * + * @see {@linkcode Negative} + * @see {@linkcode Float} + * + * @category Numeric + */ +export declare type NegativeFloat = Negative< + Float +>; + +/** + * A negative `number`/`bigint` (`-∞ < x < 0`) + * + * Use-case: Validating and documenting parameters. + * + * @see {@linkcode NegativeInteger} + * @see {@linkcode NonNegative} + * + * @category Numeric + */ +export declare type Negative = T extends Zero ? never + : `${T}` extends `-${string}` ? T + : never; + +// export declare type Absolute = `${T}` extends +// `-${infer N extends Numeric}` ? N : ParseInt<`${T}`>; + +// type AbsTest1 = Absolute<-200>; // 200 +// type AbsTest2 = Absolute<"-200">; // error +// type AbsTest3 = Absolute<200>; // 200 + +/** + * A negative (`-∞ < x < 0`) `number` that is an integer. + * Equivalent to `Negative>`. + * + * You can't pass a `bigint` as they are already guaranteed to be integers, + * instead use `Negative`. + * + * Use-case: Validating and documenting parameters. + * + * @see {@linkcode Negative} + * @see {@linkcode Integer} + * + * @category Numeric + */ +export declare type NegativeInteger = Negative< + Integer +>; + +/** + * A non-negative `number`/`bigint` (`0 <= x < ∞`). + * + * Use-case: Validating and documenting parameters. + * + * @see {@linkcode NonNegativeInteger} + * @see {@linkcode Negative} + * + * @example ```ts + * import type {NonNegative} from 'type-fest'; + * + * declare function setLength(length: NonNegative): void; + * ``` + * + * @category Numeric + */ +export declare type NonNegative = T extends Zero ? T + : Negative extends never ? T + : never; + +/** + * A non-negative (`0 <= x < ∞`) `number` that is an integer. + * Equivalent to `NonNegative>`. + * + * You can't pass a `bigint` as they are already guaranteed to be integers, + * instead use `NonNegative`. + * + * Use-case: Validating and documenting parameters. + * + * @see {@linkcode NonNegative} + * @see {@linkcode Integer} + * @example ```ts + * declare function setLength(length: NonNegativeInteger): void; + * ``` + * @category Numeric + */ +export declare type NonNegativeInteger = NonNegative< + Integer +>; + +// utilities + +export type UnionToIntersection = + (U extends any ? (k: U) => void : never) extends ( + k: infer I, + ) => void ? I + : never; + +export type UnionToFunction = T extends any ? () => T : never; + +export type LastOf = UnionToIntersection> extends + () => infer R ? R + : never; + +export type Push = [ + ...T, + ...(V extends unknown[] ? V : [V]), +]; + +export type UnionToTuple< + T, + L = LastOf, + N = [T] extends [never] ? true : false, +> = true extends N ? [] : Push>, L>; + +export type nullish = null | undefined; + +/** + * Obtain the parameters of a constructor function type in a tuple + */ +// deno-fmt-ignore +export type ConstructorArgs = + | T extends Constructor ? P : never; + +/** + * Obtain the return type of a constructor function type + */ +// deno-fmt-ignore +export type Instance = + | ConstructorFn extends Constructor ? InstanceType : any; + +/** + * Obtain the parameters of a function type in a tuple + */ +export type FunctionArgs any> = T extends + (...args: infer P extends unknown[]) => any ? P : never; + +/** + * Obtain the return type of a function type + */ +// deno-fmt-ignore +export type Returns any> = + | T extends (...args: any) => infer R ? R : any; + +/** + * Recursively unwraps the "awaited type" of a type. Non-promise "thenables" should resolve to `never`. This emulates the behavior of `await`. + */ +export type Await = T extends nullish ? T + : T extends object & { then(resolver: infer F): any } + ? F extends ((val: infer V, ...args: any[]) => any) ? Await : never + : T; + +export interface ArrayLike { + readonly length: number; + readonly [n: number]: T; +} + +/** + * Make all properties in T optional. If Deep is true, then all sub-properties + * will be optional, and sub-properties of those sub-properties, and so on. + * @see {@linkcode Require} for the opposite transformation. + * @see {@linkcode DeepOptional} for the shorthand of `Optional`. + */ +export type Optional = T extends Keyable ? { + [P in keyof T]?: Deep extends true ? Optional : T[P]; + } + : T; + +/** + * Make all properties in T required. If Deep is true, then all sub-properties + * will be required, and sub-properties of those sub-properties, and so on. + * @see {@linkcode Optional} for the opposite transformation. + * @see {@linkcode DeepRequire} for the shorthand of `Require`. + */ +export type Require = T extends Keyable ? { + [P in keyof T]-?: Deep extends true ? Require : T[P]; + } + : T; + +/** + * Make all properties in T readonly. If Deep is true, then all sub-properties + * will be readonly, and sub-properties of those sub-properties, and so on. + * @see {@linkcode Writable} for the opposite transformation. + * @see {@linkcode DeepReadOnly} for the shorthand of `ReadOnly`. + */ +export type ReadOnly = T extends Keyable ? { + readonly [P in keyof T]: Deep extends true ? ReadOnly : T[P]; + } + : T; + +/** + * Make all properties in T mutable. If Deep is true, then all sub-properties + * will be mutable, and sub-properties of those sub-properties, and so on. + * @see {@linkcode ReadOnly} for the opposite transformation. + * @see {@linkcode DeepWritable} for the shorthand of `Writable`. + */ +export type Writable = T extends Keyable ? { + -readonly [P in keyof T]: Deep extends true ? Writable : T[P]; + } + : T; + +export type Keyable = { [key: string]: any } | readonly unknown[] | unknown[]; + +/** + * Make all properties in T optional. This is a shorthand for `Partial`. + * + * @example + * ```ts + * type Foo = { + * bar: number; + * baz: { qux: string }; + * }; + * + * type FooPartial = DeepPartial; + * // type FooPartial = { + * // bar?: number | undefined; + * // baz?: { + * // qux?: string | undefined; + * // } | undefined; + * // } + * ``` + * + * @see {@linkcode Optional} for the underlying type this relies on. + * @see {@linkcode DeepRequired} for the opposite transformation. + */ +export type DeepPartial = Optional; + +/** + * Make all properties in T required, using the `-?` modifier after property + * names. This is a shorthand for `Required`. + * + * @example + * ```ts + * type Foo = { + * bar?: number; + * baz?: { qux?: string }; + * }; + * + * type FooRequired = DeepRequired; + * // type FooRequired = { + * // bar: number; + * // baz: { + * // qux: string; + * // }; + * // } + * ``` + * + * @see {@linkcode Require} for the underlying type this relies on. + * @see {@linkcode DeepPartial} for the opposite transformation. + */ +export type DeepRequired = Require; + +/** + * Recursively add the `readonly` directive to all properties of T. + * + * This is a shorthand for `Readonly`. + * + * @example + * ```ts + * type Foo = { + * bar: { + * baz: number; + * }; + * }; + * + * type FooReadonly = DeepReadonly; + * // FooReadonly = { + * // readonly bar: { + * // readonly baz: number; + * // }; + * // }; + * ``` + * + * @see {@linkcode Readonly} for the underlying type this relies on. + * @see {@linkcode DeepMutable} for the opposite transformation. + */ +export type DeepReadonly = ReadOnly; + +/** + * Recursively remove the `readonly` directive from all properties of T. + * + * This is a shorthand for `Mutable`. + * + * @example + * ```ts + * type Foo = { + * readonly bar: { + * readonly qux: readonly [1, 2, 3]; + * }; + * }; + * + * type Bar = DeepMutable; + * // { bar: { qux: [1, 2, 3]; }; } + * + * // Notice the difference between this and "shallow" Mutable: + * type Bar2 = Mutable; + * // { bar: { readonly qux: readonly [1, 2, 3]; }; } + * ``` + * + * @see {@linkcode Writable} for the underlying type this relies on. + * @see {@linkcode DeepReadonly} for the opposite transformation. + */ +export type DeepMutable = Writable; + +export type { DeepMutable as DeepWritable }; + +/** + * From T, pick a set of properties whose keys are in the union K + */ +export type Pick = { + [P in K]: T[P]; +}; + +/** + * Construct a type with a set of properties K of type T + */ +export type Record = { + [P in K]: T; +}; + +export type Awaitable = T | Promise; + +export type Arrayable = T | T[]; + +export type Args = T extends ((...args: infer A) => any) ? A + : T extends abstract new (...args: infer A) => any ? A + : never; + +export type Shift = T extends [unknown, ...infer A extends unknown[]] ? A + : never; + +export type RestArgs = Shift>; + +// deno-fmt-ignore +export type Flatten = + | T extends Keyable + ? Infer extends true + ? T extends infer U + ? Flatten + : never + : { [K in keyof T]: Deep extends true ? Flatten : T[K] } + : T; + +export type PartialByKeys = + Flatten< + Optional>, Deep> & Omit + >; + +export type RequiredByKeys = + Flatten< + Require>, Deep> & Omit + >; + +export type Conditional = NonNullable< + { [K in keyof Base]: Base[K] extends Condition ? Base[K] : never } +>; + +export type ConditionalKeys = NonNullable< + { [K in keyof Base]: Base[K] extends Condition ? K : never }[keyof Base] +>; + +export type ConditionalExcept = Except< + Base, + ConditionalKeys +>; + +export type Filter = Utils.Equal extends true + ? never + : (Key extends Exclusions ? never : Key); + +export type Except = { + [Key in keyof TObject as Filter]: TObject[Key]; +}; + +export namespace Utils { + export type Id = U extends Record + ? { [K in keyof U]: Id } + : U; + + export type IsBothExtends = FirstType extends + BaseType ? SecondType extends BaseType ? true + : false + : false; + + /** @see https://github.com/type-challenges/type-challenges */ + export type Expect = T; + export type ExpectTrue = T; + export type ExpectFalse = T; + export type IsTrue = T extends true ? T : false; + export type IsFalse = T extends false ? true : false; + export type Equal = (() => T extends X ? 1 : 2) extends + (() => T extends Y ? 1 : 2) ? true + : false; + export type NotEqual = true extends Equal ? false : true; + export type Alike = Equal, Id>; + export type ExpectExtends = Expected extends Actual ? true + : false; + + export type ExpectArguments< + Func extends (...args: unknown[]) => any, + Args extends any[], + > = Args extends Parameters ? true : false; + + /** + * @see https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360 + */ + export type IsAny = 0 extends (1 & T) ? Yes : No; + + export type NotAny = true extends IsAny ? No + : Yes; + + export type Debug = { [K in keyof T]: T[K] }; + + export type OneOf = T extends void ? V + : [T] extends [unknown] ? V + : [T] extends [never] ? V + : T & {}; + + export type MergeExclusive = Id< + ( + L extends void ? R + : R extends void ? L + : Omit & R + ) + >; + + export type MergeRecursive = L extends void ? Id + : R extends void ? Id + : Id; +} + +// from lib.es5.d.ts + +// interface TypedPropertyDescriptor { +// enumerable?: boolean; +// configurable?: boolean; +// writable?: boolean; +// value?: T; +// get?: () => T; +// set?: (value: T) => void; +// } + +export declare namespace Decorator { + interface Class { + (target: Function): Function | void; + (target: T): T | void; + } + + interface Property { + (target: Object, key: string | symbol): void; + ( + target: T, + propertyKey: K, + ): void; + } + + interface Method { + ( + target: Object, + key: string | symbol, + desc: PropertyDescriptor, + ): any | void; + ( + target: T, + propertyKey: K, + descriptor: TypedPropertyDescriptor, + ): TypedPropertyDescriptor | void; + } + + interface Parameter { + (target: Object, key: string | symbol, index: number): void; + ( + target: T, + propertyKey: K, + parameterIndex: P, + ): void; + } + + type Legacy = Class | Method | Property | Parameter; +} + +export type Decorator = + | Decorator.Class + | Decorator.Method + | Decorator.Property + | Decorator.Parameter; + +export type LegacyDecorator = Decorator.Legacy; diff --git a/assert.ts b/assert.ts index a677531..7160fab 100644 --- a/assert.ts +++ b/assert.ts @@ -2,6 +2,7 @@ import { Buffer, NodeJS } from "./types.ts"; import type { AccessorDescriptor, Class, + ConditionalExcept, DataDescriptor, MapIterator, Module, @@ -14,52 +15,84 @@ import type { export enum AssertionTypeDescription { class_ = "Class", - numericString = "string with a number", + + nullish = "null or undefined (nullish)", nullOrUndefined = "null or undefined", - nullish = "nullish (null or undefined)", - iterable = "Iterable", - asyncIterable = "AsyncIterable", - nativePromise = "native Promise", - urlString = "string with a URL", - url = "URL instance or a string with a URL", + truthy = "truthy", falsy = "falsy", - nan = "NaN", primitive = "primitive", - integer = "integer", - safeInteger = "integer", + printable = "printable primitive", + plainObject = "plain object", + arrayLike = "array-like", typedArray = "TypedArray", - domElement = "HTMLElement", - nodeStream = "Node.js Stream", - infinite = "infinite number", + entry = "tuple-pair entry", entries = "tuple-pair entries", + + iterable = "Iterable", + asyncIterable = "AsyncIterable", + + key = "key exists in the given object", + value = "value exists in the given object", + enumKey = "key exists in the given enum", + enumCase = "value exists in the given enum", + + nativePromise = "native Promise", + + urlSearchParams = "URLSearchParams instance", + urlInstance = "URL instance", + urlString = "string with a URL", + url = "URL instance or a string with a URL", + headers = "Headers instance", + response = "Response instance", + request = "Request instance", + formData = "FormData instance", + blob = "Blob instance", + file = "File instance", + fileInfo = "FileInfo instance", + namespaceModule = "module imported via `import * as ...` syntax", + + domElement = "HTMLElement", + svgElement = "SVGElement", + nodeStream = "Node.js Stream", + nodeBuffer = "Node.js Buffer", + denoBuffer = "Deno Buffer", + buffer = "Buffer (Node.js or Deno)", + sparseArray = "sparse array", emptyArray = "empty array", - nonEmptyArray = "non-empty array", - emptyString = "empty string", whitespace = "whitespace string", + emptyString = "empty string", emptyStringOrWhitespace = "empty string or whitespace", + emptyObject = "empty object", + emptySet = "empty Set", + emptyMap = "empty Map", + + nonEmptyArray = "non-empty array", nonEmptyString = "non-empty string", nonEmptyStringAndNotWhitespace = "non-empty string and not whitespace", - emptyObject = "empty object", nonEmptyObject = "non-empty object", - emptySet = "empty set", - nonEmptySet = "non-empty set", - emptyMap = "empty map", - nonEmptyMap = "non-empty map", + nonEmptySet = "non-empty Set", + nonEmptyMap = "non-empty Map", + nan = "NaN (not a number)", + numericString = "string with a number", + safeInteger = "integer (safe)", evenInteger = "even integer", oddInteger = "odd integer", - - directInstanceOf = "T", + infinite = "infinite number (+Infinity or -Infinity)", inRange = "in range", + integer = "integer", - any = "predicate returns truthy for any value", - all = "predicate returns truthy for all values", - none = "predicate returns falsy for all values / truthy for none", + instanceOf = "instanceof T", + directInstanceOf = "instanceof T", + + any = "predicate is truthy for any one value", + all = "predicate is truthy for **all** of the values", + none = "predicate is truthy for **none** of the values", some = any, every = all, } @@ -67,33 +100,23 @@ export enum AssertionTypeDescription { /** * Type assertions have to be declared with an explicit type. */ -export interface Assertions { +export interface assert { string: (value: unknown) => asserts value is string; number: (value: unknown) => asserts value is number; bigint: (value: unknown) => asserts value is bigint; boolean: (value: unknown) => asserts value is boolean; symbol: (value: unknown) => asserts value is symbol; numericString: (value: unknown) => asserts value is string; - array: ( value: unknown, assertion?: (element: unknown) => asserts element is T, ) => asserts value is T[]; - - // Unknowns. undefined: (value: unknown) => asserts value is undefined; nullOrUndefined: (value: unknown) => asserts value is null | undefined; nullish: (value: unknown) => asserts value is null | undefined; null: (value: unknown) => asserts value is null; - /** @deprecated use {@linkcode assert.null} instead */ - null_: (value: unknown) => asserts value is null; - function: (value: unknown) => asserts value is Function; - /** @deprecated use {@linkcode assert.function} instead */ - function_: (value: unknown) => asserts value is Function; class:

(value: unknown) => asserts value is Class

; - /** @deprecated use {@linkcode assert.class} instead */ - class_:

(value: unknown) => asserts value is Class

; iterable: (value: unknown) => asserts value is Iterable; asyncIterable: ( @@ -113,7 +136,6 @@ export interface Assertions { regex: (value: unknown) => asserts value is RegExp; date: (value: unknown) => asserts value is Date; error: (value: unknown) => asserts value is Error; - map: ( value: unknown, ) => asserts value is Map; @@ -127,7 +149,6 @@ export interface Assertions { weakRef: ( value: unknown, ) => asserts value is WeakRef; - typedArray: (value: unknown) => asserts value is TypedArray; int8Array: (value: unknown) => asserts value is Int8Array; uint8Array: (value: unknown) => asserts value is Uint8Array; @@ -140,19 +161,16 @@ export interface Assertions { float64Array: (value: unknown) => asserts value is Float64Array; bigInt64Array: (value: unknown) => asserts value is BigInt64Array; bigUint64Array: (value: unknown) => asserts value is BigUint64Array; - arrayBuffer: (value: unknown) => asserts value is ArrayBuffer; sharedArrayBuffer: (value: unknown) => asserts value is SharedArrayBuffer; dataView: (value: unknown) => asserts value is DataView; buffer: (value: unknown) => asserts value is Buffer; - object: ( value: unknown, ) => asserts value is Record; namespaceModule: (value: unknown) => asserts value is Module; mapIterator: (value: unknown) => asserts value is MapIterator; setIterator: (value: unknown) => asserts value is SetIterator; - propertyKey: (value: unknown) => asserts value is PropertyKey; propertyDescriptor: ( value: unknown, @@ -163,7 +181,79 @@ export interface Assertions { dataDescriptor: ( value: unknown, ) => asserts value is DataDescriptor; + exact: ( + shape: T, + value: unknown, + ) => asserts value is T; + /** + * Determines if an object is a **subset** of another (the "shape" object). + * The target object **may not** have any keys that aren't present in the + * shape. However, it does not need to have ***all*** of the shape's keys. + * + * @template T + * @param shape The object used as a schema shape to compare against + * @param obj The target object to inspect + * @returns `true` if the target is a subset of the shape, `false` otherwise + * + * @see {@linkcode is.superset} for a typecheck that is the opposite of this. + * + * @example + * ```ts + * is.assert.subset( + * { a: 0, b: 0 }, // the schema/shape object + * { a: 1, b: 2, c: 3 }, // the target object + * ) // => ❌ target has keys not defined in shape! + * ``` + * + * @example + * ```ts + * is.assert.subset( + * { a: 1, b: 2, c: 3 }, // shape + * { a: 1, c: 3 }, // target + * ) // => ✔️ target is missing `b`, but all other keys are in the shape! + * ``` + */ + subset: >( + shape: T, + value: unknown, + ) => asserts value is { + [K in keyof T as (typeof value)[K] extends never ? never : K]: unknown; + }; + /** + * Determines if an object is a **superset** of a given "shape". The object + * must have **all** of the keys in the shape, but (unlike the + * {@link is.subset} typecheck) it may also have other keys/properties that + * are not defined in the shape object. Another way to think of this is that + * the target object is an **_extension_** of the shape object. + * + * @template T + * @param shape The object used as a "schema" to compare against + * @param obj The target object to inspect + * @returns `true` if the target is a superset of the shape, `false` otherwise + * + * @see {@linkcode assert.subset} for a typecheck that is the opposite of this. + * + * @example + * ```ts + * is.assert.superset( + * { a: 0, b: 0 }, // the schema/shape object + * { a: 1, b: 2, c: 3 }, // the target object + * ) // => ✔️ target has all the keys as required by the shape + * ``` + * + * @example + * ```ts + * is.assert.superset( + * { a: 1, b: 2, c: 3 }, // shape + * { a: 1, c: 3, d: 4 }, // target + * ) // => ❌ target has an unknown key not defined by the shape! + * ``` + */ + superset: >( + shape: T, + value: unknown, + ) => asserts value is { [K in keyof T | string]: unknown }; key: = {}>( value: unknown, target: T, @@ -180,12 +270,10 @@ export interface Assertions { value: unknown, targetEnum: T, ) => asserts value is T[keyof T]; - plainObject: ( value: unknown, ) => asserts value is Record; arrayLike: (value: unknown) => asserts value is ArrayLike; - entry: ( value: unknown, ) => asserts value is readonly [K, V]; @@ -238,7 +326,6 @@ export interface Assertions { oddInteger: (value: number) => asserts value is number; inRange: (value: number, range: number | number[]) => asserts value is number; infinite: (value: unknown) => asserts value is number; - directInstanceOf: ( instance: unknown, class_: Class, @@ -247,8 +334,6 @@ export interface Assertions { instance: unknown, class_: Class, ) => asserts instance is T; - - // Variadic functions. any: ( predicate: Predicate | Predicate[], ...values: unknown[] @@ -275,17 +360,68 @@ export interface AssertOptions { multipleValues?: boolean; } -export interface Assert extends Assertions { +export type AssertionToTypeCheck< + Source, + Keys extends keyof Source = keyof Source, +> = ConditionalExcept< + { + [K in Keys]: Source[K] extends (( + value: unknown, + ...args: infer A extends unknown[] + ) => asserts value is infer Type) + ? ((value: unknown, ...args: A) => value is Type) + : Source[K] extends ( + ( + value: unknown, + ...args: infer Args extends unknown[] + ) => asserts value is infer Type + ) ? ((value: unknown, ...args: Args) => value is Type) + : never; + }, + never +>; + +// assemble a list of assertions based on the available is.{method} typechecks +// (with a teeny bit of inference magic to extract types for args and return) +export type TypeChecksToAssertions< + Source, + Keys extends keyof Source = keyof Source, +> = ConditionalExcept< + { + [K in Keys]: ( + Source[K] extends ( + (value: unknown) => value is infer R + ) ? ( + (value: unknown) => asserts value is R + ) + : Source[K] extends ( + ( + value: unknown, + ...args: infer Rest extends unknown[] + ) => value is infer R + ) ? ( + (value: unknown, ...args: Rest) => asserts value is R + ) + : Source[K] extends ( + (other: infer Other, value: unknown) => value is infer R + ) ? ((other: Other, value: unknown) => asserts value is R) + : never + ); + }, + never +>; + +export interface Assert extends assert { (expression: boolean, message?: string): asserts expression; ( - expression: boolean, - value: unknown, - message: string, - ): asserts expression; + condition: boolean, + valueReceived: unknown, + errorMessage: string, + ): asserts condition; ( expression: boolean, - received: unknown, - expected: string, + valueReceived: unknown, + valueExpected: unknown, options?: AssertOptions, ): asserts expression; } diff --git a/deno.jsonc b/deno.jsonc index 39797b2..1cd3045 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,4 +1,13 @@ { + "importMap": "./import_map.json", + "tasks": { + "test": "deno test --unstable --allow-all --import-map=import_map.json", + "test-watch": "deno test --unstable --allow-all --import-map=import_map.json --watch", + "test-coverage": "deno test --unstable --allow-all --coverage=coverage --import-map=import_map.json && deno coverage --unstable --lcov coverage > coverage.lcov", + "lint": "deno lint --unstable --import-map=import_map.json", + "format": "deno fmt --unstable --import-map=import_map.json", + "format-check": "deno fmt --unstable --import-map=import_map.json --check" + }, "compilerOptions": { "lib": [ "deno.ns", @@ -7,14 +16,23 @@ "dom.iterable" ], "types": [ - "./types.ts" - ] + "./types.ts", + "./_util/types.d.ts", + "./_util/typenames.ts" + ], + "emitDecoratorMetadata": true, + "strictPropertyInitialization": false, + "suppressImplicitAnyIndexErrors": true }, "fmt": { "files": { "exclude": [ - ".vscode" + ".vscode", + ".github" ] + }, + "options": { + "proseWrap": "preserve" } }, "lint": { @@ -26,5 +44,6 @@ "ban-types" ] } - } -} \ No newline at end of file + }, + "lock": false +} diff --git a/deps.ts b/deps.ts index 83f57e7..b81ba7f 100644 --- a/deps.ts +++ b/deps.ts @@ -1,12 +1,20 @@ -export * as ansi from "https://deno.land/std@0.166.0/fmt/colors.ts"; -export { $ } from "https://deno.land/x/dax@0.15.0/mod.ts"; -export { Buffer } from "https://deno.land/std@0.166.0/io/buffer.ts"; +export * as ansi from "https://deno.land/std@0.167.0/fmt/colors.ts"; +export { $ } from "https://deno.land/x/dax@0.17.0/mod.ts"; +export { Buffer } from "https://deno.land/std@0.167.0/io/buffer.ts"; // Node compatibility export { Buffer as NodeBuffer, -} from "https://deno.land/std@0.166.0/node/buffer.ts"; -export { Stream as NodeStream } from "https://deno.land/std@0.166.0/node/stream.ts"; -export { Readable as NodeReadable } from "https://deno.land/std@0.166.0/node/stream.ts"; -export { Writable as NodeWritable } from "https://deno.land/std@0.166.0/node/stream.ts"; -export { EventEmitter as NodeEventEmitter } from "https://deno.land/std@0.166.0/node/events.ts"; +} from "https://deno.land/std@0.167.0/node/buffer.ts"; +export { Stream as NodeStream } from "https://deno.land/std@0.167.0/node/stream.ts"; +export { Readable as NodeReadable } from "https://deno.land/std@0.167.0/node/stream.ts"; +export { Writable as NodeWritable } from "https://deno.land/std@0.167.0/node/stream.ts"; +export { EventEmitter as NodeEventEmitter } from "https://deno.land/std@0.167.0/node/events.ts"; + +export * from "https://deno.land/std@0.167.0/collections/map_keys.ts"; +export * from "https://deno.land/std@0.167.0/collections/map_values.ts"; +export * from "https://deno.land/std@0.167.0/collections/map_entries.ts"; +export * from "https://deno.land/std@0.167.0/collections/filter_keys.ts"; +export * from "https://deno.land/std@0.167.0/collections/filter_values.ts"; +export * from "https://deno.land/std@0.167.0/collections/filter_entries.ts"; +export * from "https://deno.land/std@0.167.0/collections/deep_merge.ts"; diff --git a/egg.json b/egg.json deleted file mode 100644 index b8f3692..0000000 --- a/egg.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "https://x.nest.land/eggs@0.3.10/src/schema.json", - "name": "dis", - "entry": "./mod.ts", - "description": "Typechecking with ease. An extended, hard fork of @sindresorhus/is, for Deno 🦕", - "homepage": "https://github.com/deno911/dis", - "version": "0.2.0", - "releaseType": "minor", - "unstable": false, - "unlisted": false, - "files": [ - "./is.ts", - "./assert.ts", - "./mod.ts", - "./node.d.ts", - "./types.ts", - "./deps.ts", - "./deno.jsonc", - "./_util.ts", - "./LICENSE", - "./README.md" - ], - "ignore": [ - "./.github/**/*", - "./.vscode/*", - "./.gitpod.yml", - "./.gitignore", - "./*.lock" - ], - "checkFormat": true, - "checkTests": false, - "checkInstallation": false, - "check": true, - "checkAll": true -} diff --git a/egg.yaml b/egg.yaml new file mode 100644 index 0000000..4cec4e5 --- /dev/null +++ b/egg.yaml @@ -0,0 +1,29 @@ +$schema: "https://deno.land/x/eggs/src/schema.json" +name: dis +version: 0.3.0-rc.3 +entry: ./mod.ts +description: "Extensive collection of Typeguards and Assertions for Deno🦕" +homepage: https://github.com/deno911/dis +repository: https://github.com/deno911/dis +releaseType: false +unstable: true +unlisted: false +files: + - ./*.ts + - ./_util/**/* + - ./src/**/* + - ./deno.jsonc + - ./import_map.json + - ./LICENSE + - ./README.md +ignore: + - ./.github/**/* + - ./.vscode/* + - ./.gitpod.yml + - ./.gitignore + - ./*.lock +checkFormat: true +checkTests: false +checkInstallation: false +check: true +checkAll: true diff --git a/is.ts b/is.ts index c717ce2..171e93d 100644 --- a/is.ts +++ b/is.ts @@ -1,1360 +1,2905 @@ -import { Buffer, Buffer as DenoBuffer, NodeBuffer } from "./types.ts"; +/// +/// +/// + +import { Buffer, Buffer as DenoBuffer, NodeBuffer } from "~/deps.ts"; import type { AccessorDescriptor, + ArrayAssertion, + AsyncFunction, Class, + ConditionalExcept, DataDescriptor, + ExactShapeOf, + Except, Falsy, + Filter, + Flatten, + GetTypeName, + Infinity, + IsAny, MapIterator, Module, NodeJS, + NotAny, + Numeric, ObservableLike, Predicate, Primitive, + RGB, SetIterator, + SubsetOf, + SupersetOf, TypedArray, TypeName, } from "./types.ts"; import { - assign, - deprecate, - freeze, + createNegated, + DenoCustomInspect, + deprecated, + enumerable, getObjectType, + isAbsoluteMod2, + isDeprecated, isDomElement, + isElement, + IsNegated, isObjectOfType, isOfType, isPrimitiveTypeName, + isSvgElement, isTypedArrayName, + isValidLength, predicateOnArray, -} from "./_util.ts"; +} from "~/_util.ts"; -import { - type Assert, - type Assertions, - AssertionTypeDescription, - type AssertOptions, -} from "./assert.ts"; +import { type Assert as IAssert, AssertionTypeDescription } from "~/assert.ts"; + +const { assign } = Object; + +const $options = Symbol.for("is.options"); + +interface IsOptions extends Record { + negated: boolean; + inspect: Partial; +} /** - * Determine the {@linkcode TypeName} of an arbitrary value of unknown type. - * - * @example - * ```ts - * import { is } from "https://deno.land/x/dis/mod.ts"; - * - * is("🦕") // => "string" - * is(100n) // => "bigint" - * is({ foo: "bar" }) // => "Object" - * is(new Uint8Array()) // => "Uint8Array" - * ``` + * Check if a value is a given type, or retrieve its {@linkcode TypeName}. + * @param value The value to check */ -function is(value: unknown): TypeName { - if (value === null) { - return "null"; +class is { + @enumerable(false) + private static [$options] = { + negated: false, + inspect: { + colors: true, + compact: true, + depth: 1, + getters: true, + showHidden: false, + showProxy: false, + sorted: true, + trailingComma: true, + } as Deno.InspectOptions, + }; + + @enumerable(false) + static get $options(): IsOptions { + return this[$options]; } - switch (typeof value) { - case "undefined": - return "undefined"; - case "string": - return "string"; - case "number": - return Number.isNaN(value) ? "NaN" : "number"; - case "boolean": - return "boolean"; - case "function": - return "Function"; - case "bigint": - return "bigint"; - case "symbol": - return "symbol"; - default: + static set $options(value: Record) { + this[$options] = Object.assign(this[$options], value); } - if (is.observable(value)) { - return "Observable"; + @enumerable(true) + public static get not() { + this.$options.negated = true; + + return createNegated(this as unknown as is, { + revocable: false, + sorted: true, + maskMethodNames: true, + toStringTag: "is.not", + excluded: ["not", "assert", IsNegated, "$options", "__negated"], + }).proxy as unknown as isnt; } - if (is.array(value)) { - return "Array"; - } + @enumerable(false) + static assertType< + Negated extends boolean = (typeof is.$options.negated), + Expected extends boolean = (Negated extends true ? false : true), + >( + condition: boolean, + description: string, + value: unknown, + options: { + multipleValues?: boolean; + negated?: Negated; + } = { + multipleValues: false, + negated: (is.$options.negated) as Negated, + }, + ): asserts condition is Expected { + const { multipleValues, negated } = options; - if (is.nodeBuffer(value)) { - return "Buffer"; - } + if (negated === true) { + description = `not ${description}`; + condition = !condition; + } - if (is.denoBuffer(value)) { - return "Buffer"; - } + is.$options.negated &&= false; + + if (!condition) { + const values = [...new Set([value as any].flat())]; + const msg = multipleValues + ? `values of types ${ + values.map((v, i) => + `${i === values.length - 1 ? "and " : ""}\`${getTypeName(v)}\`` + ).join(", ") + }` + : `value of type \`${getTypeName(value)}\``; - if (is.boxedPrimitive(value)) { - throw new TypeError("Please don't use object wrappers for primitive types"); + throw new TypeError( + `Assertion Failure: Expected value \`${description}\`${ + negated ? " (negated)" : "" + }. Received ${msg}.`, + ); + } } - return getObjectType(value) ?? "Object"; -} + static assert: Assert; -is.undefined = isOfType("undefined"); + // --------- // -is.string = isOfType("string"); + static string = function string(value: unknown): value is string { + return isOfType("string")(value); + }; -is.number = (value: unknown): value is number => - isOfType("number")(value) && !is.NaN(value); + static number = function number(value: unknown): value is number { + return isOfType("number")(value) && !is.NaN(value); + }; -is.bigint = isOfType("bigint"); + static bigint = function bigint(value: unknown): value is bigint { + return isOfType("bigint")(value); + }; -is.boolean = (value: unknown): value is boolean => - value === true || value === false; + static boolean = function boolean(value: unknown): value is boolean { + return value === true || value === false; + }; -is.symbol = isOfType("symbol"); + static symbol = function symbol(value: unknown): value is symbol { + return isOfType("symbol")(value); + }; -is.array = ( - value: unknown, - assertion?: (value: T) => value is T, -): value is T[] => { - if (!Array.isArray(value)) return false; - if (!is.function(assertion)) return true; - return value.every(assertion); -}; + /** Check if a value is `null`, using strict equality comparison. */ + static null = (value: unknown): value is null => value === null; -/** Check if a value is `null`, using strict equality comparison. */ -is.null = (value: unknown): value is null => value === null; + static undefined = function (value: unknown): value is undefined { + return isOfType("undefined")(value); + }; -is.nullOrUndefined = (value: unknown): value is null | undefined => - is.null(value) || is.undefined(value); + /** @see {@linkcode is.nullOrUndefined} */ + static nullish = function nullish(value: unknown): value is null | undefined { + return is.nullOrUndefined(value); + }; -type nullish = null | undefined; + static nullOrUndefined = function nullOrUndefined( + value: unknown, + ): value is null | undefined { + return is.null(value) || is.undefined(value); + }; -/** @see {@linkcode is.nullOrUndefined} */ -is.nullish = (value: unknown): value is nullish => is.nullOrUndefined(value); + /** + * Check if a value is of the generic "object" type, and not null. This will + * also check true for Function and builtin objects like Map, Set, etc. + * + * If you need a higher level of precision, consider using typeguards such as + * {@linkcode is.plainObject}, {@linkcode is.nonEmptyObject}, {@linkcode is.} + */ + static object = function object(value: unknown): value is object { + return !is.null(value) && (typeof value === "object" || is.function(value)); + }; -/** - * Check if a value is of the generic "object" type, and not null. This will - * also check true for Function and builtin objects like Map, Set, etc. - * - * If you need a higher level of precision, consider using typeguards such as - * {@linkcode is.plainObject}, {@linkcode is.nonEmptyObject}, {@linkcode is.} - */ -is.object = (value: unknown): value is object => - !is.null(value) && (typeof value === "object" || is.function(value)); + /** + * Tests if a value is either null or of a primitive type. Possible values: + * - `string` + * - `number` + * - `bigint` + * - `boolean` + * - `symbol` + * @param value The value to inspect. + * @returns `boolean` + */ + static primitive = function primitive(value: unknown): value is Primitive { + return is.null(value) || isPrimitiveTypeName(typeof value); + }; + + static boxedPrimitive = (value: unknown): boolean => { + if (!is.object(value)) return false; + const typeName = toString.call(value).slice(8, -1); + return ( + ((value as any) instanceof String && typeName === "String") || + ((value as any) instanceof Number && typeName === "Number") || + ((value as any) instanceof BigInt && typeName === "BigInt") || + ((value as any) instanceof Symbol && typeName === "Symbol") || + ((value as any) instanceof Boolean && typeName === "Boolean") + ); + }; -/** - * Tests if a value is either null or of a primitive type. Possible values: - * - `string` - * - `number` - * - `bigint` - * - `boolean` - * - `symbol` - * @param value The value to inspect. - * @returns `boolean` - */ -is.primitive = (value: unknown): value is Primitive => - is.null(value) || isPrimitiveTypeName(typeof value); - -is.boxedPrimitive = (value: unknown): boolean => { - if (!is.object(value)) return false; - const typeName = toString.call(value).slice(8, -1); - return ( - ((value as any) instanceof String && typeName === "String") || - ((value as any) instanceof Number && typeName === "Number") || - ((value as any) instanceof BigInt && typeName === "BigInt") || - ((value as any) instanceof Symbol && typeName === "Symbol") || - ((value as any) instanceof Boolean && typeName === "Boolean") - ); -}; + /** + * Check if a value is truthy, meaning it cannot be falsy (`false`, `0`, `""`, + * `undefined`, or `null`). + * + * @param value The value to inspect. + * @returns `boolean` + * + * @example ```ts + * is.truthy = (value: unknown): value is (not false | not 0 | not '' | not undefined | not null) => Boolean(value); + * ``` + */ + static truthy = function truthy(value: T | Falsy): value is T { + return Boolean(value); + }; -/** - * Check if a value is truthy, meaning it cannot be falsy (`false`, `0`, `""`, - * `undefined`, or `null`). - * - * @param value The value to inspect. - * @returns `boolean` - * - * @example ```ts - * is.truthy = (value: unknown): value is (not false | not 0 | not '' | not undefined | not null) => Boolean(value); - * ``` - */ -is.truthy = (value: T | Falsy): value is T => Boolean(value); + /** + * Check if a value is falsy: `false`, `0`, `""`, `undefined` or `null`. + * + * @param value The value to inspect. + * @returns `boolean` + * + * @example ```ts + * is.falsy = (value: unknown): value is + * (not true | 0 | '' | undefined | null) => Boolean(value); + * ``` + */ + static falsy = function falsy(value: T | Falsy): value is Falsy { + return !value; + }; -/** - * Check if a value is falsy: `false`, `0`, `""`, `undefined` or `null`. - * - * @param value The value to inspect. - * @returns `boolean` - * - * @example ```ts - * is.falsy = (value: unknown): value is - * (not true | 0 | '' | undefined | null) => Boolean(value); - * ``` - */ -is.falsy = (value: T | Falsy): value is Falsy => !value; + /** + * Alias for `is.falsy`. + * @see {@link is.falsy} + */ + @enumerable(false) + static falsey = function falsey(value: T | Falsy): value is Falsy { + return !value; + }; -/** - * Alias for `is.falsy`. - * @see {@link is.falsy} - */ -is.falsey = (value: T | Falsy): value is Falsy => !value; + static function = function (value: unknown): value is Function { + return isOfType("function")(value); + }; -is.function = isOfType("function"); + static fn = function fn(value: unknown): value is Function { + return isOfType("function")(value); + }; -is.iterable = (value: unknown): value is Iterable => - is.function((value as Iterable)?.[Symbol.iterator]); + static iterable = function iterable( + value: unknown, + ): value is Iterable { + return is.function((value as Iterable)?.[Symbol.iterator]); + }; -is.generator = (value: unknown): value is Generator => - is.iterable(value) && is.function((value as Generator)?.next) && - is.function((value as Generator)?.throw); + static generatorFunction = function generatorFunction( + value: unknown, + ): value is GeneratorFunction { + return isObjectOfType("GeneratorFunction")(value); + }; -is.generatorFunction = isObjectOfType("GeneratorFunction"); + static asyncGenerator = function asyncGenerator( + value: unknown, + ): value is AsyncGenerator { + return is.asyncIterable(value) && + is.function((value as AsyncGenerator).next) && + is.function((value as AsyncGenerator).throw); + }; -is.asyncFunction = ( - value: unknown, -): value is (...args: any[]) => Promise => - getObjectType(value) === "AsyncFunction"; + static asyncGeneratorFunction = function asyncGeneratorFunction( + value: unknown, + ): value is AsyncGeneratorFunction { + return isObjectOfType("AsyncGeneratorFunction")( + value, + ); + }; -is.asyncIterable = (value: unknown): value is AsyncIterable => - is.function((value as AsyncIterable)?.[Symbol.asyncIterator]); + static generator = assign( + function generator(value: unknown): value is Generator { + return is.iterable(value) && is.function((value as Generator)?.next) && + is.function((value as Generator)?.throw); + }, + { + function: assign({}, is.generatorFunction, { + async: is.asyncGeneratorFunction, + }), + async: is.asyncGenerator, + }, + ); -is.asyncGenerator = (value: unknown): value is AsyncGenerator => - is.asyncIterable(value) && is.function((value as AsyncGenerator).next) && - is.function((value as AsyncGenerator).throw); + static asyncFunction = function asyncFunction( + value: unknown, + ): value is AsyncFunction { + const AsyncFunction = ((async function () {}).constructor) as AsyncFunction; -is.asyncGeneratorFunction = ( - value: unknown, -): value is (...args: any[]) => Promise => - getObjectType(value) === "AsyncGeneratorFunction"; + return is.function(value) && ( + (value instanceof AsyncFunction) || + isObjectOfType("AsyncFunction")(value) + ); + }; -is.boundFunction = (value: unknown): value is Function => - is.function(value) && - !Object.prototype.hasOwnProperty.call(value, "prototype"); + static asyncIterable = function asyncIterable( + value: unknown, + ): value is AsyncIterable { + return is.function((value as AsyncIterable)?.[Symbol.asyncIterator]); + }; + + static async = assign({}, { + function: is.asyncFunction, + generator: assign({}, is.asyncGenerator, { + function: is.asyncGeneratorFunction, + }), + generatorFunction: is.asyncGeneratorFunction, + iterable: is.asyncIterable, + }); + + static boundFunction = function boundFunction( + value: unknown, + ): value is Function { + return is.function(value) && + !Object.prototype.hasOwnProperty.call(value, "prototype"); + }; -/** - * Check if a Function appears to be a constructable Class. - */ -is.class = (value: unknown): value is Class => { - if (!is.function(value)) return false; - if (!is.object(value.prototype)) return false; - return Function.prototype.toString.call(value).startsWith("class "); -}; -/** - * Check if a value is a direct instance of a class. - * @param instance The value to inspect. - * @param class_ the class to check against - * @returns `boolean` - */ -is.directInstanceOf = (instance: unknown, class_: Class): instance is T => - Object.getPrototypeOf(instance) === class_.prototype; + /** + * Check if a Function appears to be a constructable Class. + */ + static class = function ( + value: unknown, + ): value is Class { + if (!is.function(value)) return false; + if (!is.object(value.prototype)) return false; + return Function.prototype.toString.call(value).startsWith("class "); + }; + + static instanceOf = function instanceOf( + instance: unknown, + class_: Class, + ): instance is T { + return (is.directInstanceOf(instance, class_)) || + (instance instanceof class_); + }; + + /** + * Check if a value is a direct instance of a class. + * @param instance The value to inspect. + * @param class_ the class to check against + * @returns `boolean` + */ + static directInstanceOf = function directInstanceOf( + instance: unknown, + class_: Class, + ): instance is T { + return Object.getPrototypeOf(instance) === class_.prototype; + }; + + static promise = function promise( + value: unknown, + ): value is Promise { + const hasPromiseApi = (value: unknown): value is Promise => + is.function((value as Promise)?.then) && + is.function((value as Promise)?.catch); + + return is.nativePromise(value) || hasPromiseApi(value); + }; -is.instanceOf = (instance: unknown, class_: Class): instance is T => - (is.directInstanceOf(instance, class_)) || (instance instanceof class_); + static nativePromise = function nativePromise( + value: unknown, + ): value is Promise { + return isObjectOfType>("Promise")(value); + }; -is.promise = (value: unknown): value is Promise => - is.nativePromise(value) || hasPromiseApi(value); + static regExp = function regExp(value: unknown): value is RegExp { + return isObjectOfType("RegExp")(value); + }; -is.nativePromise = (value: unknown): value is Promise => - isObjectOfType>("Promise")(value); + @enumerable(false) + static regex = function regex(value: unknown): value is RegExp { + return isObjectOfType("RegExp")(value); + }; -const hasPromiseApi = (value: unknown): value is Promise => - is.function((value as Promise)?.then) && - is.function((value as Promise)?.catch); + static date = function date(value: unknown): value is Date { + return isObjectOfType("Date")(value); + }; -is.regExp = isObjectOfType("RegExp"); -is.regex = isObjectOfType("RegExp"); -is.date = isObjectOfType("Date"); -is.error = isObjectOfType("Error"); + static error = function error(value: unknown): value is Error { + return isObjectOfType("Error")(value); + }; -is.map = ( - value: unknown, -): value is Map => isObjectOfType>("Map")(value); + static mapIterator = function mapIterator( + value: unknown, + ): value is MapIterator { + return isObjectOfType>("Map Iterator")(value); + }; -is.set = (value: unknown): value is Set => - isObjectOfType>("Set")(value); + static setIterator = function setIterator( + value: unknown, + ): value is SetIterator { + return isObjectOfType>("Set Iterator")(value); + }; -is.weakMap = ( - value: unknown, -): value is WeakMap => - isObjectOfType>("WeakMap")(value); + /** + * Check if a value is a `Map` object. + * @param value The value to inspect. + * @returns `true` if the value is a `Map` object, `false` otherwise. + */ + // deno-fmt-ignore + static map: ((value: unknown) => value is Map) + & { + /** + * Check if a value is a `Map Iterator`. + * + * @param value The value to inspect. + * @returns `true` if the value is a `Map Iterator`, `false` otherwise. + * + * @see {@linkcode is.map} to check if a value is a `Map` object. + * @see {@linkcode is.mapIterator} for the standalone version. + */ + readonly iterator: ( + value: unknown, + ) => value is MapIterator; + readonly empty: (value: unknown) => value is Map; + readonly nonEmpty: (value: unknown) => value is Map; + }; -is.weakSet = (value: unknown): value is WeakSet => - isObjectOfType>("WeakSet")(value); + /** + * Check if a value is a `Set` object. + * + * @param value The value to inspect. + * @returns `true` if the value is a `Set` object, `false` otherwise. + */ + // deno-fmt-ignore + static set: ((value: unknown) => value is Set) + & { + /** + * Check if a value is a `Set Iterator`. + * + * @param value The value to inspect. + * @returns `true` if the value is a `Set Iterator`, `false` otherwise. + * + * @see {@linkcode is.set} to check if a value is a `Set` object. + * @see {@linkcode is.setIterator} for the standalone version. + */ + readonly iterator: (value: unknown) => value is SetIterator; + readonly empty: (value: unknown) => value is Set; + readonly nonEmpty: (value: unknown) => value is Set; + }; + + // ----------------------------------------------------------------- // + // Emptiness Checks + // ----------------------------------------------------------------- // + static emptyArray = function emptyArray(value: unknown): value is never[] { + return is.array(value) && value.length === 0; + }; + + static emptySet = function emptySet(value: unknown): value is Set { + return is.set(value) && value.size === 0; + }; + + static emptyMap = function emptyMap( + value: unknown, + ): value is Map { + return is.map(value) && value.size === 0; + }; -is.weakRef = (value: unknown): value is WeakRef => - isObjectOfType>("WeakRef")(value); + static emptyObject = function emptyObject( + value: unknown, + ): value is Record { + return is.object(value) && !is.map(value) && !is.set(value) && + Object.keys(value).length === 0; + }; -is.mapIterator = isObjectOfType("Map Iterator"); + static emptyString = function emptyString(value: unknown): value is "" { + return is.string(value) && value.length === 0; + }; -is.setIterator = isObjectOfType("Set Iterator"); + static whitespace = function whitespace(value: unknown): value is string { + return is.string(value) && !/\S/.test(value); + }; -is.namespaceModule = isObjectOfType("Module"); + static emptyStringOrWhitespace = function emptyStringOrWhitespace( + value: unknown, + ): value is string { + return is.emptyString(value) || is.whitespace(value); + }; -/** - * Check if a value is a plain object, with extensive checks to ensure we - * aren't actually dealing with an array, function, or other object type. - * - * @param value The value to inspect. - * @returns `boolean` - */ -is.plainObject = ( - value: unknown, -): value is Record => { /** - * @see https://github.com/sindresorhus/is-plain-obj/blob/main/index.js + * Collection of various emptiness checks. Each of the methods contained is + * really just a reference to a standalone static method of the `is` class. + * For your convenience they're also provided here as `is.empty.{method}`. + * + * @see {@linkcode is.emptyArray} - `is.empty.array` + * @see {@linkcode is.emptyMap} - `is.empty.map` + * @see {@linkcode is.emptyObject} - `is.empty.object` + * @see {@linkcode is.emptySet} - `is.empty.set` + * @see {@linkcode is.emptyString} - `is.empty.string` + * @see {@linkcode is.emptyStringOrWhitespace} - `is.empty.string.or.whitespace` or `is.empty.string.orWhitespace` */ - if (typeof value !== "object" || value === null) { - return false; - } + static empty = { + map: is.emptyMap, + set: is.emptySet, + array: is.emptyArray, + object: is.emptyObject, + string: assign({}, is.emptyString, { + or: { + whitespace: is.emptyStringOrWhitespace, + }, + orWhitespace: is.emptyStringOrWhitespace, + }), + stringOrWhitespace: is.emptyStringOrWhitespace, + }; + + // ----------------------------------------------------------------- // + // Non-Emptiness Checks + // ----------------------------------------------------------------- // + + static nonEmptyArray = function nonEmptyArray( + value: unknown, + ): value is [unknown, ...unknown[]] { + return is.array(value) && value.length > 0; + }; - const prototype = Object.getPrototypeOf(value); + static nonEmptySet = function nonEmptySet( + value: unknown, + ): value is Set { + return is.set(value) && value.size > 0; + }; - return (prototype === null || prototype === Object.prototype || - Object.getPrototypeOf(prototype) === null) && - !(Symbol.toStringTag in value) && !(Symbol.iterator in value); -}; + static nonEmptyMap = function nonEmptyMap( + value: unknown, + ): value is Map { + return is.map(value) && value.size > 0; + }; -// ----------------------------------------------------------------- // -// Properties and Property Descriptors -// ----------------------------------------------------------------- // + /* + * TODO: Use `not` operator here to remove `Map` and `Set` from type guard: + * https://github.com/Microsoft/TypeScript/pull/29317 + */ + static nonEmptyObject = function nonEmptyObject< + K extends keyof any = string, + V = unknown, + >( + value: unknown, + ): value is Record { + return is.object(value) && !is.map(value) && !is.set(value) && + Object.keys(value).length > 0; + }; -/** - * `PropertyKey` is any value that is valid as an object key. Equivalent to - * `string | number | symbol`. - */ -is.propertyKey = (value: unknown): value is PropertyKey => - is.any([is.string, is.number, is.symbol], value); + // TODO: Use `not ''` when the `not` operator is available. + static nonEmptyString = function nonEmptyString( + value: unknown, + ): value is string { + return is.string(value) && value.length > 0; + }; -/** - * Checks if a value is a Property Descriptor, without discerning between the - * subtypes of Accessor Properties or Data Properties. - * @see {@linkcode is.accessorDescriptor} for the Accessor Descriptor check - * @see {@linkcode is.dataDescriptor} for the Data Descriptor check - */ -is.propertyDescriptor = ( - value: unknown, -): value is TypedPropertyDescriptor => { - return is.nonEmptyObject(value) && - (is.accessorDescriptor(value) || is.dataDescriptor(value)); -}; + static nonEmptyStringAndNotWhitespace = + function nonEmptyStringAndNotWhitespace( + value: unknown, + ): value is string { + return is.string(value) && !is.emptyStringOrWhitespace(value); + }; -/** - * Checks if a value is a valid Accessor Descriptor, meaning it does not have - * `value` or `writable` properties, and **does** have a getter (`get`) and/or - * a setter (`set`) property. - * @param value The value to inspect. - * @returns `true` if the value is an accessor descriptor, else `false` - * @see {@linkcode is.dataDescriptor} for the Data Descriptor check - * @see {@linkcode is.propertyDescriptor} for the generic Descriptor check - */ -is.accessorDescriptor = ( - value: unknown, -): value is AccessorDescriptor => { - if (is.nonEmptyObject(value)) { - return is.any(is.function, value.get, value.set) && - is.all(is.undefined, value.writable, value.value); + /** + * Collection of negated-emptiness checks. Each of the methods contained is + * really just a reference to a standalone static method of the `is` class. + * For your convenience they're provided here as `is.nonEmpty.{method}`. + * + * @see {@linkcode is.nonEmptyArray} - `is.nonEmpty.array` + * @see {@linkcode is.nonEmptyMap} - `is.nonEmpty.map` + * @see {@linkcode is.nonEmptyObject} - `is.nonEmpty.object` + * @see {@linkcode is.nonEmptySet} - `is.nonEmpty.set` + * @see {@linkcode is.nonEmptyString} - `is.nonEmpty.string` + * @see {@linkcode is.nonEmptyStringAndNotWhitespace} - `is.nonEmpty.string.andNotWhitespace` or `is.nonEmpty.string.and.not.whitespace` + */ + static nonEmpty = { + array: is.nonEmptyArray, + map: is.nonEmptyMap, + object: is.nonEmptyObject, + set: is.nonEmptySet, + string: assign({}, is.nonEmptyString, { + or: { + whitespace: is.nonEmptyStringAndNotWhitespace, + }, + and: { + not: { + whitespace: is.nonEmptyStringAndNotWhitespace, + }, + }, + andNotWhitespace: is.nonEmptyStringAndNotWhitespace, + }), + nonEmptyStringAndNotWhitespace: is.nonEmptyStringAndNotWhitespace, + }; + + // ----------------------------------------------------------------- // + // Map + // ----------------------------------------------------------------- // + static { + const map = function map( + value: unknown, + ): value is Map { + return isObjectOfType>("Map")(value); + }; + map.iterator = is.mapIterator; + map.empty = is.emptyMap; + map.nonEmpty = is.nonEmptyMap; + is.map = map; } - return false; -}; -/** - * Checks if a value is a valid Data Property Descriptor, meaning it does not - * have a `get` or `set` property, but is defined with a `value` data property. - * @param value The value to inspect. - * @returns `true` if the value is a data property descriptor, else `false` - * @see {@linkcode is.accessorDescriptor} for the Accessor Descriptor check - * @see {@linkcode is.propertyDescriptor} for the generic Descriptor check - */ -is.dataDescriptor = ( - value: unknown, -): value is DataDescriptor => { - if (is.nonEmptyObject(value)) { - return !is.undefined(value.value) && - is.none(is.function, value.get, value.set); + // ----------------------------------------------------------------- // + // Set + // ----------------------------------------------------------------- // + static { + const set = function set(value: unknown): value is Set { + return isObjectOfType>("Set")(value); + }; + set.iterator = is.setIterator; + set.empty = is.emptySet; + set.nonEmpty = is.nonEmptySet; + is.set = set; } - return false; -}; - -/** - * Checks if a key is included in those defined on a target object or array.. - * @param value The value to inspect. - * @param target The object or array to check against. - * @returns `true` if the value is included in the target's keys, else `false`. - */ -is.key = = {}>( - value: unknown, - target: T, -): value is keyof T => Object.keys(target).includes(value as string); - -/** - * Checks if a value is included in a target object or array. - * @param value The value to inspect. - * @param target The object or array to check against. - * @returns `true` if the value is included in the target values, else `false`. - */ -is.value = = {}>( - value: unknown, - target: T, -): value is T[keyof T] => Object.values(target).includes(value as string); - -/** - * Checks if a key is included in a target enum's keys. - * @param value The value to inspect. - * @param targetEnum The enum to check against. - * @returns `true` if the key exists in the enum, else `false`. - */ -is.enumKey = ( - value: unknown, - targetEnum: T, -): value is keyof T => - Object.keys(targetEnum as object).includes(String(value)); - -/** - * Checks if a value is included in a target enum. - * @param value The value to inspect. - * @param targetEnum The enum to check against. - * @returns `true` if the value is included in the enum, else `false`. - */ -is.enumCase = ( - value: unknown, - targetEnum: T, -): value is T[keyof T] => Object.values(targetEnum as object).includes(value); -/** - * Checks if a value is `ArrayLike`. An "array-like" object is simply an - * object that has a numeric length property and 0-indexed numeric keys. - * - * @param value The value to inspect. - * @returns `boolean` - */ -is.arrayLike = (value: unknown): value is ArrayLike => - !is.nullOrUndefined(value) && !is.function(value) && - isValidLength((value as ArrayLike).length); + static weakMap = function weakMap< + Key extends object = object, + Value = unknown, + >( + value: unknown, + ): value is WeakMap { + return isObjectOfType>("WeakMap")(value); + }; -/** - * Checks if a value is a valid `[key, value]` entry in the form of a - * tuple pair with a fixed-length of 2. - * @see {@linkcode is.entries} - */ -is.entry = (value: unknown): value is [K, V] => - is.nonEmptyArray(value) && value.length === 2; + static weakSet = function weakSet(value: unknown): value is WeakSet { + return isObjectOfType>("WeakSet")(value); + }; -/** - * Checks if a value is a collection of valid `[key, value]` entries, each of - * which has the form of a tuple pair with a fixed-length of 2. - * @see {@linkcode is.entry} - */ -is.entries = (value: unknown): value is [K, V][] => - is.nonEmptyArray(value) && is.array(value, is.entry); + static weakRef = function weakRef(value: unknown): value is WeakRef { + return isObjectOfType>("WeakRef")(value); + }; -is.sparseArray = (value: unknown): value is unknown[] => - is.array(value) && (value.length !== value.filter((v) => v).length); + static module = function module(value: unknown): value is Module { + return isObjectOfType("Module")(value); + }; -/** - * Check if a value is a TypedArray. - * - * @param value The value to inspect. - * @returns `boolean` - */ -is.typedArray = (value: unknown): value is TypedArray => - isTypedArrayName(getObjectType(value)); - -is.int8Array = isObjectOfType("Int8Array"); -is.uint8Array = isObjectOfType("Uint8Array"); -is.uint8ClampedArray = isObjectOfType("Uint8ClampedArray"); -is.int16Array = isObjectOfType("Int16Array"); -is.uint16Array = isObjectOfType("Uint16Array"); -is.int32Array = isObjectOfType("Int32Array"); -is.uint32Array = isObjectOfType("Uint32Array"); -is.float32Array = isObjectOfType("Float32Array"); -is.float64Array = isObjectOfType("Float64Array"); -is.bigInt64Array = isObjectOfType("BigInt64Array"); -is.bigUint64Array = isObjectOfType("BigUint64Array"); -is.arrayBuffer = isObjectOfType("ArrayBuffer"); + static namespaceModule = function namespaceModule( + value: unknown, + ): value is Module { + return isObjectOfType("Module")(value); + }; -/** - * Checks if the given value is a valid `SharedArrayBuffer`. - * @param value The value to inspect. - * @returns `true` if the value is a valid `SharedArrayBuffer`, else `false`. - * @example ```ts - * import { is } from "is"; - * if (is.sharedArrayBuffer(new SharedArrayBuffer(1))) { - * console.log("SharedArrayBuffer"); - * } - */ -is.sharedArrayBuffer = isObjectOfType("SharedArrayBuffer"); + /** + * Check if a value is a plain object, with extensive checks to ensure we + * aren't actually dealing with an array, function, or other object type. + * + * @param value The value to inspect. + * @returns `boolean` + */ + static plainObject = function plainObject( + value: unknown, + ): value is Record { + /** + * @see https://github.com/sindresorhus/is-plain-obj/blob/main/index.js + */ + if (typeof value !== "object" || value === null) { + return false; + } -/** - * Checks if the given value is an instance of `DataView` or `ArrayBufferView`. - * @param value The value to inspect. - * @returns `true` if the value is an instance of `DataView` or `ArrayBufferView`, else `false`. - * @example ```ts - * import { is } from "is"; - * if (is.arrayBufferView(new DataView(new ArrayBuffer(1)))) { - * console.log("DataView"); - * } - */ -is.dataView = isObjectOfType("DataView"); + const prototype = Object.getPrototypeOf(value); -/** - * Checks if a given value is a valid Node.js Buffer, using the `.isBuffer()` - * method (static) from the Buffer constructor. This does not perform any other - * checks, and should not be relied upon for matching potential `ArrayBuffer` or - * `Deno.Buffer` instances. - */ -is.nodeBuffer = (value: unknown): value is NodeBuffer => - ((value as any)?.constructor as typeof NodeBuffer)?.isBuffer?.(value) ?? - false; + return (prototype === null || prototype === Object.prototype || + Object.getPrototypeOf(prototype) === null) && + !(Symbol.toStringTag in value) && !(Symbol.iterator in value); + }; -is.denoBuffer = (value: unknown): value is DenoBuffer => - DenoBuffer?.[Symbol.hasInstance]?.(value) || is.instanceOf(value, DenoBuffer); + // ----------------------------------------------------------------- // + // Properties and Property Descriptors + // ----------------------------------------------------------------- // -is.buffer = (value: unknown): value is Buffer => - is.arrayBuffer(value) || is.denoBuffer(value) || is.nodeBuffer(value); + /** + * `PropertyKey` is any value that is valid as an object key. Equivalent to + * `string | number | symbol`. + */ + static propertyKey = function propertyKey( + value: unknown, + ): value is PropertyKey { + return is.any([is.string, is.number, is.symbol], value); + }; -// ----------------------------------------------------------------- // -// Browser / Web API Related Objects -// ----------------------------------------------------------------- // + /** + * Checks if a value is a Property Descriptor, without discerning between the + * subtypes of Accessor Properties or Data Properties. + * @see {@linkcode is.accessorDescriptor} for the Accessor Descriptor check + * @see {@linkcode is.dataDescriptor} for the Data Descriptor check + */ + static propertyDescriptor = function propertyDescriptor( + value: unknown, + ): value is TypedPropertyDescriptor { + return is.nonEmptyObject(value) && + (is.accessorDescriptor(value) || is.dataDescriptor(value)); + }; -is.blob = (value: unknown): value is Blob => - isObjectOfType("Blob")(value); + /** + * Checks if a value is a valid Accessor Descriptor, meaning it does not have + * `value` or `writable` properties, and **does** have a getter (`get`) and/or + * a setter (`set`) property. + * @param value The value to inspect. + * @returns `true` if the value is an accessor descriptor, else `false` + * @see {@linkcode is.dataDescriptor} for the Data Descriptor check + * @see {@linkcode is.propertyDescriptor} for the generic Descriptor check + */ + static accessorDescriptor = function accessorDescriptor( + value: unknown, + ): value is AccessorDescriptor { + if (is.nonEmptyObject(value)) { + if (is.undefined(value.writable) && is.undefined(value.value)) { + if (is.any(is.function, value.get, value.set)) { + return true; + } + } + } + return false; + }; -is.formData = (value: unknown): value is FormData => - isObjectOfType("FormData")(value); + /** + * Checks if a value is a valid Data Property Descriptor, meaning it does not + * have a `get` or `set` property, but is defined with a `value` data property. + * @param value The value to inspect. + * @returns `true` if the value is a data property descriptor, else `false` + * @see {@linkcode is.accessorDescriptor} for the Accessor Descriptor check + * @see {@linkcode is.propertyDescriptor} for the generic Descriptor check + */ + static dataDescriptor = function dataDescriptor( + value: unknown, + ): value is DataDescriptor { + if (is.nonEmptyObject(value)) { + if (is.undefined(value.get) && is.undefined(value.set)) { + if (is.boolean(value.writable) || !is.undefined(value.value)) { + return true; + } + } + } + return false; + }; -is.headers = (value: unknown): value is Headers => - isObjectOfType("Headers")(value); + /** + * Checks if a key is included in those defined on a target object or array.. + * @param value The value to inspect. + * @param target The object or array to check against. + * @returns `true` if the value is included in the target's keys, else `false`. + */ + static key = function key< + T extends readonly unknown[] | Record = {}, + >(value: unknown, target: T): value is keyof T { + return Object.keys(target).includes(value as string); + }; -is.request = (value: unknown): value is Request => - isObjectOfType("Request")(value); + /** + * Checks if a value is included in a target object or array. + * @param value The value to inspect. + * @param target The object or array to check against. + * @returns `true` if the value is included in the target values, else `false`. + */ + static value = function value< + T extends readonly unknown[] | Record = {}, + >(value: unknown, target: T): value is T[keyof T] { + return Object.values(target).includes(value as string); + }; -is.response = (value: unknown): value is Response => - isObjectOfType("Response")(value); + /** + * Checks if a key is included in a target enum's keys. + * @param value The value to inspect. + * @param targetEnum The enum to check against. + * @returns `true` if the key exists in the enum, else `false`. + */ + static enumKey = function enumKey( + value: unknown, + targetEnum: T, + ): value is keyof T { + return Object.keys(targetEnum as object).includes(String(value)); + }; -is.urlSearchParams = (value: unknown): value is URLSearchParams => - isObjectOfType("URLSearchParams")(value); + /** + * Checks if a value is included in a target enum. + * @param value The value to inspect. + * @param targetEnum The enum to check against. + * @returns `true` if the value is included in the enum, else `false`. + */ + static enumCase = function enumCase( + value: unknown, + targetEnum: T, + ): value is T[keyof T] { + return Object.values(targetEnum as object).includes(value); + }; -/** - * Check if an value is a valid instance of the `URL` class. - * @param value The value to inspect. - * @returns `boolean` - * @see https://mdn.io/URL - */ -is.urlInstance = (value: unknown): value is URL => - isObjectOfType("URL")(value); + static exact = function exact( + shape: T, + value: unknown, + ): value is T { + // in case we try to compare something other than an object + if (!is.object(value) || !is.object(shape)) { + return value === shape; + } + // just to save time + if (Object.is(shape, value)) { + return true; + } + // forward comparison + if (is.subset(shape, value) && is.subset(value, shape)) { + if ( + Object.entries(shape).every(([key, val]) => + is.enumKey(key, value) && is.enumCase(val, value) + ) && + Object.entries(value).every(([key, val]) => + is.enumKey(key, shape) && is.enumCase(val, shape) + ) + ) { + return true; + } + // strict value type comparison + return (Object.entries(value).every(([key, val]) => { + if (is.object(val) && is.object(shape[key as keyof T])) { + // both values are objects, recurse their properties + return is.exact(val, shape[key as keyof T]); + } + // otherwise just check equality of types using getTypeName() + return getTypeName(val) === getTypeName(shape[key as keyof T]); + })); + } -/** - * Check if an arbitrary string is a valid URL. - * @param value The value to inspect. - * @returns `boolean` - */ -is.urlString = (value: unknown): value is string => { - if (!is.string(value)) return false; - try { - new URL(value); - return true; - } catch { return false; - } -}; - -is.url = (value: unknown): value is string | URL => - is.urlString(value) || is.urlInstance(value); + }; -/** - * Check if a value is a DOM element. - * - * @param value The value to inspect. - * @returns `true` if the value is a DOM node. - * - * @example ```ts - * const div = document.createElement("div"); - * is.domElement(div); // true - * ``` - * @example ```ts - * const myElement = document.querySelector("#my-element"); - * is.domElement(myElement); // true - * ``` - * - * @example ```ts - * const astNode = { tagName: "div", id: "my-element" }; - * is.domElement(astNode); // false - * ``` - */ -is.domElement = (value: unknown): value is HTMLElement => - is.object(value) && !is.plainObject(value) && isDomElement(value); + /** + * Determines if an object is a **subset** of another (the "shape" object). + * The target object may not have any keys that are not also in the shape, + * but it's okay if it only has _some_ of the shape's keys. + * Think of the **_shape_ as an _extension_ of the _target_**. + * + * @template T + * @param shape The object used as a schema shape to compare against + * @param value The target object to inspect + * @returns `true` if the target is a subset of the shape, `false` otherwise + * + * @see {@linkcode is.superset} for a typecheck that is the opposite of this. + * + * For example, consider the following shape and valid/invalid subsets: + * + * @example + * ```ts + * const shape = { name: "", age: 0, email: "" }; + * + * const valid = { name: "Nick", age: 29 }; + * is.subset(shape, valid) // => true + * + * const wrong = { name: "Nick", city: "Las Vegas" }; + * is.subset(shape, wrong) // => false + * ``` + * + * @example + * ```ts + * is.subset( + * { a: 0, b: 0 }, // the schema/shape object + * { a: 1, b: 2, c: 3 }, // the target object + * ) // => FALSE - target has keys not defined in shape! + * ``` + * + * @example + * ```ts + * is.subset( + * { a: 1, b: 2, c: 3 }, // shape + * { a: 1, c: 3 }, // target + * ) // => TRUE - target is missing `b`, but all other keys are in the shape! + * ``` + */ + static subset = function subset( + shape: T, + value: unknown, + ): value is { + [K in keyof T as (typeof value)[K] extends never ? never : K]: unknown; + } { + if (is.nonEmptyObject(shape) && is.nonEmptyObject(value)) { + if (Object.keys(value).every((key) => key in shape)) { + return true; + } + } + return false; + }; -/** - * Check if a value is `Observable` or `ObservableLike`. - * - * @note An "observable" is an object that has a `subscribe` method, and a `Symbol.observable` property (sometimes referred to as "@@observable"). - * - * @param value The value to inspect. - * @returns `true` if the value is an `Observable` or `ObservableLike`. - */ -is.observable = (value: unknown): value is ObservableLike => { - if (!value) { + /** + * Determines if an object is a **superset** of a given "shape". The object, + * unlike the {@link is.subset} typecheck, must have **all** of the keys in + * the shape. However, it **_can_ have keys that _are not_ in the shape**. + * + * Think of the **_target_ as an _extension_ of the _shape_**. + * + * @template T + * @param shape The object used as a "schema" to compare against + * @param value The target object to inspect + * @returns `true` if the value is a superset of the shape, `false` otherwise + * + * @see {@linkcode is.subset} for a typecheck that is the opposite of this. + * + * @example + * ```ts + * is.superset( + * { a: 0, b: 0 }, // the schema/shape object + * { a: 1, b: 2, c: 3 }, // the target object + * ) // => TRUE - target has all keys required by the shape + * ``` + * + * @example + * ```ts + * is.superset( + * { a: 1, b: 2, c: 3 }, // shape + * { a: 1, c: 3, d: 4 }, // target + * ) // => FALSE - target is missing `b` from the shape + * ``` + */ + static superset = function superset( + shape: T, + value: unknown, + ): value is { [K in keyof T]-?: any } & object { + if (is.nonEmptyObject(shape) && is.nonEmptyObject(value)) { + if (Object.keys(shape).every((key) => key in value)) { + return true; + } + } return false; - } + }; - if (value === (value as any)[Symbol.observable]?.()) { - return true; - } + /** + * Checks if a value is `ArrayLike`. An "array-like" object is simply an + * object that has a numeric length property and 0-indexed numeric keys. + * + * @param value The value to inspect. + * @returns `boolean` + */ + static arrayLike = function arrayLike( + value: unknown, + ): value is ArrayLike { + return !is.nullOrUndefined(value) && !is.function(value) && + isValidLength((value as ArrayLike).length); + }; - if (value === (value as any)["@@observable"]?.()) { - return true; - } + /** + * Checks if a value is a valid `[key, value]` entry in the form of a + * tuple pair with a fixed-length of 2. + * @see {@linkcode is.entries} + */ + static entry = function entry( + value: unknown, + ): value is [K, V] { + return is.nonEmptyArray(value) && value.length === 2; + }; - return false; -}; + /** + * Checks if a value is a collection of valid `[key, value]` entries, each of + * which has the form of a tuple pair with a fixed-length of 2. + * @see {@linkcode is.entry} + */ + static entries = function entries( + value: unknown, + ): value is [K, V][] { + return is.nonEmptyArray(value) && is.array(value, is.entry); + }; -is.nodeStream = (value: unknown): value is NodeJS.Stream => - is.object(value) && is.function((value as NodeJS.Stream).pipe) && - !is.observable(value); + static sparseArray = function sparseArray( + value: unknown, + ): value is unknown[] { + return is.array(value) && (value.length !== value.filter((v) => v).length); + }; -// ----------------------------------------------------------------- // -// Numbers / Numerics -// ----------------------------------------------------------------- // + /** + * Check if a value is a TypedArray. + * + * @param value The value to inspect. + * @returns `boolean` + */ + static typedArray = function typedArray(value: unknown): value is TypedArray { + return isTypedArrayName(getObjectType(value)); + }; -is.numericString = (value: unknown): value is string => - is.string(value) && - !is.emptyStringOrWhitespace(value) && - !Number.isNaN(Number(value)); + static int8Array = function int8Array(value: unknown): value is Int8Array { + return isObjectOfType("Int8Array")(value); + }; -/** - * Equivalent to the JavaScript builtin `Number.isNaN`. - * - * @param value The value to inspect. - * @returns `boolean` - */ -is.NaN = (value: unknown) => Number.isNaN(value as number); + static uint8Array = function uint8Array(value: unknown): value is Uint8Array { + return isObjectOfType("Uint8Array")(value); + }; -/** - * Alias for `is.NaN`. - * @see {@linkcode is.NaN} - */ -is.nan = (value: unknown) => Number.isNaN(value as number); + static uint8ClampedArray = function uint8ClampedArray( + value: unknown, + ): value is Uint8ClampedArray { + return isObjectOfType("Uint8ClampedArray")(value); + }; -/** - * Strong typed alias for the builtin `Number.isInteger`. - * - * @param value The value to inspect. - * @returns `boolean` - */ -is.integer = (value: unknown): value is number => - Number.isInteger(value as number); + static int16Array = function int16Array(value: unknown): value is Int16Array { + return isObjectOfType("Int16Array")(value); + }; -/** - * Strong-typed alias for the builtin `Number.isSafeInteger`. - * @param value The value to inspect. - * @returns `boolean` - */ -is.safeInteger = (value: unknown): value is number => - Number.isSafeInteger(value as number); + static uint16Array = function uint16Array( + value: unknown, + ): value is Uint16Array { + return isObjectOfType("Uint16Array")(value); + }; -/** - * Check if a value is of the valid length for its given type. - * @param value - * @returns `boolean` - */ -const isValidLength = (value: unknown): value is number => - is.safeInteger(value) && value >= 0; + static int32Array = function int32Array(value: unknown): value is Int32Array { + return isObjectOfType("Int32Array")(value); + }; -is.infinite = (value: unknown): value is number => - value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY; + static uint32Array = function uint32Array( + value: unknown, + ): value is Uint32Array { + return isObjectOfType("Uint32Array")(value); + }; -const isAbsoluteMod2 = - (remainder: number) => (value: number): value is number => - is.integer(value) && Math.abs(value % 2) === remainder; + static float32Array = function float32Array( + value: unknown, + ): value is Float32Array { + return isObjectOfType("Float32Array")(value); + }; -is.evenInteger = isAbsoluteMod2.apply(this, [0]); + static float64Array = function float64Array( + value: unknown, + ): value is Float64Array { + return isObjectOfType("Float64Array")(value); + }; -is.oddInteger = isAbsoluteMod2(1); + static bigInt64Array = function bigInt64Array( + value: unknown, + ): value is BigInt64Array { + return isObjectOfType("BigInt64Array")(value); + }; -/** - * Check if a numeric value conforms to a given range. - * @param value The value to inspect. - * @param range - the range to check against. - * @returns `true` if the value is within the legal bounds of the range. - * Otherwise, throws a `TypeError` similar to a `RangeError`. - * - * @example ```ts - * is.inRange = (value: number, range: [number, number]): boolean => { - * return value >= range[0] && value <= range[1]; - * } - * ``` - */ -is.inRange = (value: number, range: number | number[]): value is number => { - if (is.number(range)) { - return value >= Math.min(0, range) && value <= Math.max(range, 0); + static bigUint64Array = function bigUint64Array( + value: unknown, + ): value is BigUint64Array { + return isObjectOfType("BigUint64Array")(value); + }; + + static arrayBuffer = function arrayBuffer( + value: unknown, + ): value is ArrayBuffer { + return isObjectOfType("ArrayBuffer")(value); + }; + + /** + * Checks if the given value is an instance of `DataView` or `ArrayBufferView`. + * @param value The value to inspect. + * @returns `true` if the value is an instance of `DataView` or `ArrayBufferView`, else `false`. + * @example ```ts + * import { is } from "is"; + * if (is.arrayBufferView(new DataView(new ArrayBuffer(1)))) { + * console.log("DataView"); + * } + */ + static dataView = function dataView(value: unknown): value is DataView { + return isObjectOfType("DataView")(value); + }; + + /** + * Checks if the given value is a valid `SharedArrayBuffer`. + * @param value The value to inspect. + * @returns `true` if the value is a valid `SharedArrayBuffer`, else `false`. + * @example ```ts + * import { is } from "is"; + * if (is.sharedArrayBuffer(new SharedArrayBuffer(1))) { + * console.log("SharedArrayBuffer"); + * } + */ + static sharedArrayBuffer = function sharedArrayBuffer( + value: unknown, + ): value is SharedArrayBuffer { + return isObjectOfType("SharedArrayBuffer")(value); + }; + + /** + * Checks if a given value is a valid Node.js Buffer, using the `.isBuffer()` + * method (static) from the Buffer constructor. This does not perform any other + * checks, and should not be relied upon for matching potential `ArrayBuffer` or + * `Deno.Buffer` instances. + */ + static nodeBuffer = function nodeBuffer(value: unknown): value is NodeBuffer { + return ((value as any)?.constructor as typeof NodeBuffer)?.isBuffer?.( + value, + ) ?? false; + }; + + static denoBuffer = function denoBuffer(value: unknown): value is DenoBuffer { + return DenoBuffer?.[Symbol.hasInstance]?.(value) || + is.instanceOf(value, DenoBuffer); + }; + + static buffer = function buffer(value: unknown): value is Buffer { + return is.arrayBuffer(value) || is.denoBuffer(value) || + is.nodeBuffer(value); + }; + + // ----------------------------------------------------------------- // + // Browser / Web API Related Objects + // ----------------------------------------------------------------- // + + static blob = function blob(value: unknown): value is Blob { + return isObjectOfType("Blob")(value); + }; + + static formData = function formData(value: unknown): value is FormData { + return isObjectOfType("FormData")(value); + }; + static headers = function headers(value: unknown): value is Headers { + return isObjectOfType("Headers")(value); + }; + + static request = function request(value: unknown): value is Request { + return isObjectOfType("Request")(value); + }; + + static response = function response(value: unknown): value is Response { + return isObjectOfType("Response")(value); + }; + + static urlSearchParams = function urlSearchParams( + value: unknown, + ): value is URLSearchParams { + return isObjectOfType("URLSearchParams")(value); + }; + + /** + * Check if an value is a valid instance of the `URL` class. + * @param value The value to inspect. + * @returns `boolean` + * @see https://mdn.io/URL + */ + static urlInstance = function urlInstance(value: unknown): value is URL { + return isObjectOfType("URL")(value); + }; + + /** + * Check if an arbitrary string is a valid URL. + * @param value The value to inspect. + * @returns `boolean` + */ + public static urlString = (value: unknown): value is string => { + if (!is.string(value)) return false; + try { + new URL(value); + return true; + } catch { + return false; + } + }; + + static url = function url(value: unknown): value is string | URL { + return is.urlString(value) || is.urlInstance(value); + }; + + /** + * Check if a value is a DOM element. + * + * @param value The value to inspect. + * @returns `true` if the value is a DOM node. + * + * @example ```ts + * const div = document.createElement("div"); + * is.element(div); // true + * ``` + * @example ```ts + * const myElement = document.querySelector("#my-element"); + * is.element(myElement); // true + * ``` + * + * @example ```ts + * const astNode = { tagName: "div", id: "my-element" }; + * is.element(astNode); // false + * ``` + */ + static element = function element(value: unknown): value is Element { + return is.object(value) && !is.plainObject(value) && isElement(value); + }; + + /** + * Check if a value is a DOM element. + * + * @param value The value to inspect. + * @returns `true` if the value is a DOM node. + * + * @example ```ts + * const div = document.createElement("div"); + * is.domElement(div); // true + * ``` + * @example ```ts + * const myElement = document.querySelector("#my-element"); + * is.domElement(myElement); // true + * ``` + * + * @example ```ts + * const astNode = { tagName: "div", id: "my-element" }; + * is.domElement(astNode); // false + * ``` + */ + static domElement = function domElement( + value: unknown, + ): value is HTMLElement { + return is.object(value) && !is.plainObject(value) && isDomElement(value); + }; + + /** + * Check if a value is a DOM element. + * + * @param value The value to inspect. + * @returns `true` if the value is a DOM node. + * + * @example + * ```ts + * const div = document.createElement("div"); + * is.svgElement(div); // false + * ``` + * @example + * ```ts + * const svg = document.createElement("svg"); + * svg.id = "my-element"; + * is.svgElement(svg); // true + * + * const myElement = document.querySelector("#my-element"); + * is.svgElement(myElement); // true + * + * const astNode = { tagName: "svg", id: "my-element" }; + * is.svgElement(astNode); // false + * ``` + */ + static svgElement = function svgElement(value: unknown): value is SVGElement { + return is.object(value) && !is.plainObject(value) && isSvgElement(value); + }; + + /** + * Check if a value is `Observable` or `ObservableLike`. + * + * @note An "observable" is an object that has a `subscribe` method, and a `Symbol.observable` property (sometimes referred to as "@@observable"). + * + * @param value The value to inspect. + * @returns `true` if the value is an `Observable` or `ObservableLike`. + */ + static observable = function observable( + value: unknown, + ): value is ObservableLike { + if (!value) { + return false; + } + + if (value === (value as any)[Symbol.observable]?.()) { + return true; + } + + if (value === (value as any)["@@observable"]?.()) { + return true; + } + + return false; + }; + + static nodeStream = function nodeStream( + value: unknown, + ): value is NodeJS.Stream { + return is.object(value) && is.function((value as NodeJS.Stream).pipe) && + !is.observable(value); + }; + + // ----------------------------------------------------------------- // + // Numbers / Numerics + // ----------------------------------------------------------------- // + + static infinite = function infinite(value: unknown): value is number { + return value === Number.POSITIVE_INFINITY || + value === Number.NEGATIVE_INFINITY; + }; + + /** + * Strong typed alias for the builtin `Number.isInteger`. + * + * @param value The value to inspect. + * @returns `boolean` + */ + static integer = function integer(value: unknown): value is number { + return Number.isInteger(value as number); + }; + + static evenInteger = function evenInteger(value: unknown): value is number { + return is.integer(value) && isAbsoluteMod2(0)(value); + }; + + static oddInteger = function oddInteger(value: unknown): value is number { + return is.integer(value) && isAbsoluteMod2(1)(value); + }; + + /** + * Strong-typed alias for the builtin `Number.isSafeInteger`. + * @param value The value to inspect. + * @returns `boolean` + */ + static safeInteger = function safeInteger(value: unknown): value is number { + return Number.isSafeInteger(value as number); + }; + + /** + * Check if a numeric value conforms to a given range. + * @param value The value to inspect. + * @param range - the range to check against. + * @returns `true` if the value is within the legal bounds of the range. + * Otherwise, throws a `TypeError` similar to a `RangeError`. + * + * @example ```ts + * is.inRange = (value: number, range: [number, number]): boolean => { + * return value >= range[0] && value <= range[1]; + * } + * ``` + */ + static inRange = function inRange( + value: number, + range: number | number[], + ): value is number { + if (is.number(range)) { + return value >= Math.min(0, range) && value <= Math.max(range, 0); + } + + if (is.array(range) && range.length === 2) { + return value >= Math.min(...range) && value <= Math.max(...range); + } + + throw new TypeError(`Invalid range: ${JSON.stringify(range)}`); + }; + + /** + * Equivalent to the JavaScript builtin `Number.isNaN`. + * + * @param value The value to inspect. + * @returns `boolean` + */ + static NaN = function (value: unknown): value is unknown { + return Number.isNaN(value as number); + }; + + /** + * Alias for `is.NaN`. + * @see {@linkcode is.NaN} + */ + static nan = function nan(value: unknown): value is unknown { + return Number.isNaN(value as number); + }; + + static numeric = function numeric( + value: unknown, + ): value is bigint | number | `${number}` { + return is.any([is.number, is.numericString, is.bigint], value); + }; + + static numericString = function numericString( + value: unknown, + ): value is `${number}` { + return is.string(value) && !is.emptyStringOrWhitespace(value) && + !Number.isNaN(Number(value)); + }; + + // ----------------------------------------------------------------- // + // Emptiness Checks + // ----------------------------------------------------------------- // + static emptyArray = function emptyArray(value: unknown): value is never[] { + return is.array(value) && value.length === 0; + }; + + static emptySet = function emptySet(value: unknown): value is Set { + return is.set(value) && value.size === 0; + }; + + static emptyMap = function emptyMap( + value: unknown, + ): value is Map { + return is.map(value) && value.size === 0; + }; + + static emptyObject = function emptyObject( + value: unknown, + ): value is Record { + return is.object(value) && !is.map(value) && !is.set(value) && + Object.keys(value).length === 0; + }; + + static emptyString = function emptyString(value: unknown): value is "" { + return is.string(value) && value.length === 0; + }; + + static whitespace = function whitespace(value: unknown): value is string { + return is.string(value) && !/\S/.test(value); + }; + + static emptyStringOrWhitespace = function emptyStringOrWhitespace( + value: unknown, + ): value is string { + return is.emptyString(value) || is.whitespace(value); + }; + + // ----------------------------------------------------------------- // + // Non-Emptiness Checks + // ----------------------------------------------------------------- // + + static nonEmptyArray = function nonEmptyArray( + value: unknown, + ): value is [unknown, ...unknown[]] { + return is.array(value) && value.length > 0; + }; + + static nonEmptySet = function nonEmptySet( + value: unknown, + ): value is Set { + return is.set(value) && value.size > 0; + }; + + static nonEmptyMap = function nonEmptyMap( + value: unknown, + ): value is Map { + return is.map(value) && value.size > 0; + }; + + /** + * TODO: Use `not` operator here to remove `Map` and `Set` from type guard: + * https://github.com/Microsoft/TypeScript/pull/29317 + */ + static nonEmptyObject = function nonEmptyObject< + Key extends keyof any = string, + Value = unknown, + >( + value: unknown, + ): value is Record { + return is.object(value) && !is.map(value) && !is.set(value) && + Object.keys(value).length > 0; + }; + + // TODO: Use `not ''` when the `not` operator is available. + static nonEmptyString = function nonEmptyString( + value: unknown, + ): value is string { + return is.string(value) && value.length > 0; + }; + + static nonEmptyStringAndNotWhitespace = + function nonEmptyStringAndNotWhitespace( + value: unknown, + ): value is string { + return is.string(value) && !is.emptyStringOrWhitespace(value); + }; + + static any = function any( + predicate: Predicate | Predicate[], + ...values: unknown[] + ): boolean { + const predicates = is.array(predicate) ? predicate : [predicate]; + return predicates.some((singlePredicate) => + predicateOnArray(Array.prototype.some, singlePredicate, values) + ); + }; + + static some = function some( + predicate: Predicate | Predicate[], + ...values: unknown[] + ): boolean { + return is.any(predicate, ...values); + }; + + static all = function all( + predicate: Predicate | Predicate[], + ...values: unknown[] + ): boolean { + const predicates = is.array(predicate) ? predicate : [predicate]; + return predicates.every((singlePredicate) => + predicateOnArray(Array.prototype.every, singlePredicate, values) + ); + }; + + static every = function every( + predicate: Predicate | Predicate[], + ...values: unknown[] + ): boolean { + return is.all(predicate, ...values); + }; + + static none = function none( + predicate: Predicate | Predicate[], + ...values: unknown[] + ): boolean { + const predicates = is.array(predicate) ? predicate : [predicate]; + return predicates.some((singlePredicate) => + predicateOnArray(Array.prototype.some, singlePredicate, values) + ); + }; + + static typeName(value: unknown) { + return getTypeName(value) as TypeName; } - if (is.array(range) && range.length === 2) { - return value >= Math.min(...range) && value <= Math.max(...range); + /** @deprecated use {@linkcode is.function} instead */ + @deprecated({ since: "0.1.0", substitute: is.function }) + static function_(value: unknown): value is Function { + return isOfType("function")(value); } - throw new TypeError(`Invalid range: ${JSON.stringify(range)}`); -}; + /** @deprecated use {@linkcode is.class} instead */ + @deprecated({ since: "0.1.0", substitute: is.class }) + static class_(value: unknown): value is Class { + return is.class(value); + } -// ----------------------------------------------------------------- // -// Emptiness Checks -// ----------------------------------------------------------------- // -is.emptyArray = (value: unknown): value is never[] => - is.array(value) && value.length === 0; + /** @deprecated use {@linkcode is.null} instead */ + @deprecated({ since: "0.1.0", substitute: "is.null" }) + static null_(value: unknown): value is null { + return value === null; + } -is.emptySet = (value: unknown): value is Set => - is.set(value) && value.size === 0; + static [Symbol.for("Deno.customInspect")](inspect: typeof Deno.inspect) { + const options: Deno.InspectOptions = { + colors: true, + compact: true, + depth: 2, + getters: true, + showHidden: false, + showProxy: false, + sorted: false, + trailingComma: true, + }; + + return `is ${inspect({ ...this }, options)}`; + } +} -is.emptyMap = (value: unknown): value is Map => - is.map(value) && value.size === 0; +/* Type Assertions */ -is.emptyObject = ( - value: unknown, -): value is Record => - is.object(value) && !is.map(value) && !is.set(value) && - Object.keys(value).length === 0; +class Assertions { + // Unknowns. + static undefined = function ( + value: unknown, + ): asserts value is undefined { + return is.assertType(is.undefined(value), "undefined", value); + }; -is.emptyString = (value: unknown): value is "" => - is.string(value) && value.length === 0; + static null = function (value: unknown): asserts value is null { + return is.assertType(is.null(value), "null", value); + }; -is.whitespace = (value: unknown): value is string => - is.string(value) && !/\S/.test(value); + static nullOrUndefined = function nullOrUndefined( + value: unknown, + ): asserts value is null | undefined { + return is.assertType( + is.nullOrUndefined(value), + AssertionTypeDescription.nullOrUndefined, + value, + ); + }; -is.emptyStringOrWhitespace = (value: unknown): value is string => - is.emptyString(value) || is.whitespace(value); + static string = function string(value: unknown): asserts value is string { + return is.assertType(is.string(value), "string", value); + }; -// ----------------------------------------------------------------- // -// Non-Emptiness Checks -// ----------------------------------------------------------------- // + static number = function number(value: unknown): asserts value is number { + return is.assertType(is.number(value), "number", value); + }; -is.nonEmptyArray = (value: unknown): value is [unknown, ...unknown[]] => - is.array(value) && value.length > 0; + static bigint = function bigint(value: unknown): asserts value is bigint { + return is.assertType(is.bigint(value), "bigint", value); + }; -is.nonEmptySet = (value: unknown): value is Set => - is.set(value) && value.size > 0; + static function = function (value: unknown): asserts value is Function { + return is.assertType(is.function(value), "Function", value); + }; -is.nonEmptyMap = ( - value: unknown, -): value is Map => is.map(value) && value.size > 0; + static class = function

( + value: unknown, + ): asserts value is Class

{ + return is.assertType( + is.class

(value), + AssertionTypeDescription.class_, + value, + ); + }; -/** - * TODO: Use `not` operator here to remove `Map` and `Set` from type guard: - * https://github.com/Microsoft/TypeScript/pull/29317 - */ -is.nonEmptyObject = ( - value: unknown, -): value is Record => - is.object(value) && !is.map(value) && !is.set(value) && - Object.keys(value).length > 0; - -// TODO: Use `not ''` when the `not` operator is available. -is.nonEmptyString = (value: unknown): value is string => - is.string(value) && value.length > 0; - -// TODO: Use `not ''` when the `not` operator is available. -is.nonEmptyStringAndNotWhitespace = (value: unknown): value is string => - is.string(value) && !is.emptyStringOrWhitespace(value); - -is.any = ( - predicate: Predicate | Predicate[], - ...values: unknown[] -): boolean => { - const predicates = is.array(predicate) ? predicate : [predicate]; - return predicates.some((singlePredicate) => - predicateOnArray(Array.prototype.some, singlePredicate, values) - ); -}; - -is.some = ( - predicate: Predicate | Predicate[], - ...values: unknown[] -): boolean => { - return is.any(predicate, ...values); -}; - -is.all = ( - predicate: Predicate | Predicate[], - ...values: unknown[] -): boolean => { - const predicates = is.array(predicate) ? predicate : [predicate]; - return predicates.every((singlePredicate) => - predicateOnArray(Array.prototype.every, singlePredicate, values) - ); -}; - -is.every = ( - predicate: Predicate | Predicate[], - ...values: unknown[] -): boolean => { - return is.all(predicate, ...values); -}; - -is.none = ( - predicate: Predicate | Predicate[], - ...values: unknown[] -): boolean => { - const predicates = is.array(predicate) ? predicate : [predicate]; - return predicates.some((singlePredicate) => - predicateOnArray(Array.prototype.some, singlePredicate, values) - ); -}; + static nullish = function nullish( + value: unknown, + ): asserts value is null | undefined { + return is.assertType( + is.nullish(value), + AssertionTypeDescription.nullish, + value, + ); + }; -/** @deprecated use {@linkcode is.function} instead */ -is.function_ = isOfType("function"); + static object = function object(value: unknown): asserts value is object { + return is.assertType(is.object(value), "Object", value); + }; -/** @deprecated use {@linkcode is.class} instead */ -is.class_ = (value: unknown): value is Class => is.class(value); + static boolean = function boolean(value: unknown): asserts value is boolean { + return is.assertType(is.boolean(value), "boolean", value); + }; -/** - * @deprecated use {@linkcode is.null} instead - */ -is.null_ = (value: unknown): value is null => value === null; + static symbol = function symbol(value: unknown): asserts value is symbol { + return is.assertType(is.symbol(value), "symbol", value); + }; -/* Type Assertions */ -const assertType = ( - condition: boolean, - description: string, - value: unknown, - options: { multipleValues?: boolean } = {}, -): asserts condition => { - if (!condition) { - const { multipleValues } = options; - const valuesMessage = multipleValues - ? `received values of types ${ - [ - ...new Set( - (value as any[]).map((singleValue) => `\`${is(singleValue)}\``), - ), - ].join(", ") - }` - : `received value of type \`${is(value)}\``; - - throw new TypeError( - `Expected value which is \`${description}\`, ${valuesMessage}.`, + static truthy = function truthy(value: unknown): asserts value is unknown { + return is.assertType( + is.truthy(value), + AssertionTypeDescription.truthy, + value, + ); + }; + + static falsy = function falsy(value: unknown): asserts value is unknown { + return is.assertType( + is.falsy(value), + AssertionTypeDescription.falsy, + value, + ); + }; + + static primitive = function primitive( + value: unknown, + ): asserts value is Primitive { + return is.assertType( + is.primitive(value), + AssertionTypeDescription.primitive, + value, ); + }; + + static boxedPrimitive = function boxedPrimitive( + value: unknown, + ): asserts value is object { + return is.assertType(is.boxedPrimitive(value), "BoxedPrimitive", value); + }; + + static array = function array( + value: unknown, + assertion?: ArrayAssertion, + ): asserts value is T[] { + const assert: ( + condition: boolean, + description: string, + value: unknown, + ) => asserts condition = is.assertType; + + assert(is.array(value), "Array", value); + + if (assertion) { + value.forEach(assertion); + } + }; + + static iterable = function iterable( + value: unknown, + ): asserts value is Iterable { + return is.assertType( + is.iterable(value), + AssertionTypeDescription.iterable, + value, + ); + }; + + static asyncIterable = function asyncIterable( + value: unknown, + ): asserts value is AsyncIterable { + return is.assertType( + is.asyncIterable(value), + AssertionTypeDescription.asyncIterable, + value, + ); + }; + + static generator = function generator( + value: unknown, + ): asserts value is Generator { + return is.assertType(is.generator(value), "Generator", value); + }; + + static asyncGenerator = function asyncGenerator( + value: unknown, + ): asserts value is AsyncGenerator { + return is.assertType(is.asyncGenerator(value), "AsyncGenerator", value); + }; + + static nativePromise = function nativePromise( + value: unknown, + ): asserts value is Promise { + return is.assertType( + is.nativePromise(value), + AssertionTypeDescription.nativePromise, + value, + ); + }; + + static promise = function promise( + value: unknown, + ): asserts value is Promise { + return is.assertType(is.promise(value), "Promise", value); + }; + + static generatorFunction = function generatorFunction( + value: unknown, + ): asserts value is GeneratorFunction { + return is.assertType( + is.generatorFunction(value), + "GeneratorFunction", + value, + ); + }; + + static asyncGeneratorFunction = function asyncGeneratorFunction( + value: unknown, + ): asserts value is AsyncGeneratorFunction { + return is.assertType( + is.asyncGeneratorFunction(value), + "AsyncGeneratorFunction", + value, + ); + }; + + static asyncFunction = function asyncFunction( + value: unknown, + ): asserts value is Function { + return is.assertType(is.asyncFunction(value), "AsyncFunction", value); + }; + + static boundFunction = function boundFunction( + value: unknown, + ): asserts value is Function { + return is.assertType(is.boundFunction(value), "Function", value); + }; + + static directInstanceOf = function directInstanceOf( + instance: unknown, + class_: Class, + ): asserts instance is T { + return is.assertType( + is.directInstanceOf(instance, class_), + AssertionTypeDescription.directInstanceOf, + instance, + ); + }; + + static instanceOf = function instanceOf( + instance: unknown, + class_: Class, + ): asserts instance is T { + return is.assertType( + is.instanceOf(instance, class_), + AssertionTypeDescription.directInstanceOf, + instance, + ); + }; + + static regExp = function regExp(value: unknown): asserts value is RegExp { + return is.assertType(is.regExp(value), "RegExp", value); + }; + + static date = function date(value: unknown): asserts value is Date { + return is.assertType(is.date(value), "Date", value); + }; + + static error = function error(value: unknown): asserts value is Error { + return is.assertType(is.error(value), "Error", value); + }; + + static map = function map( + value: unknown, + ): asserts value is Map { + return is.assertType(is.map(value), "Map", value); + }; + + static set = function set( + value: unknown, + ): asserts value is Set { + return is.assertType(is.set(value), "Set", value); + }; + + static weakMap = function weakMap< + Key extends object = object, + Value = unknown, + >( + value: unknown, + ): asserts value is WeakMap { + return is.assertType(is.weakMap(value), "WeakMap", value); + }; + + static weakSet = function weakSet( + value: unknown, + ): asserts value is WeakSet { + return is.assertType(is.weakSet(value), "WeakSet", value); + }; + + static weakRef = function weakRef( + value: unknown, + ): asserts value is WeakRef { + return is.assertType(is.weakRef(value), "WeakRef", value); + }; + + static arrayLike = function arrayLike( + value: unknown, + ): asserts value is ArrayLike { + return is.assertType( + is.arrayLike(value), + AssertionTypeDescription.arrayLike, + value, + ); + }; + + static mapIterator = function mapIterator( + value: unknown, + ): asserts value is MapIterator { + return is.assertType(is.mapIterator(value), "Map Iterator", value); + }; + + static setIterator = function setIterator( + value: unknown, + ): asserts value is SetIterator { + return is.assertType(is.setIterator(value), "Set Iterator", value); + }; + + static namespaceModule = function namespaceModule( + value: unknown, + ): asserts value is Module { + return is.assertType(is.namespaceModule(value), "Module", value); + }; + + static plainObject = ( + value: unknown, + ): asserts value is Record => + is.assertType( + is.plainObject(value), + AssertionTypeDescription.plainObject, + value, + ); + + static propertyKey = function propertyKey( + value: unknown, + ): asserts value is number { + return is.assertType(is.propertyKey(value), "PropertyKey", value); + }; + + static propertyDescriptor = ( + value: unknown, + ): asserts value is TypedPropertyDescriptor => + is.assertType( + is.propertyDescriptor(value), + "PropertyDescriptor", + value, + ); + + static accessorDescriptor = ( + value: unknown, + ): asserts value is AccessorDescriptor => + is.assertType(is.accessorDescriptor(value), "AccessorDescriptor", value); + + static dataDescriptor = ( + value: unknown, + ): asserts value is DataDescriptor => + is.assertType(is.dataDescriptor(value), "DataDescriptor", value); + + static key = < + T extends readonly unknown[] | Record = {}, + >( + value: unknown, + target: T, + ): asserts value is keyof T => + is.assertType(is.key(value, target), "Key", value); + + static value = < + T extends readonly unknown[] | Record = {}, + >( + value: unknown, + target: T, + ): asserts value is T[keyof T] => + is.assertType(is.value(value, target), "Value", value); + + static enumKey = ( + value: unknown, + targetEnum: T, + ): asserts value is keyof T => + is.assertType(is.enumKey(value, targetEnum), "EnumKey", value); + + static enumCase = ( + value: unknown, + targetEnum: T, + ): asserts value is T[keyof T] => + is.assertType(is.enumCase(value, targetEnum), "EnumCase", value); + + static entry = ( + value: unknown, + ): asserts value is readonly [K, V] => + is.assertType( + is.entry(value), + AssertionTypeDescription.entry, + value, + ); + + static entries = ( + value: unknown, + ): asserts value is readonly (readonly [K, V])[] => + is.assertType( + is.entries(value), + AssertionTypeDescription.entries, + value, + ); + + static sparseArray = (value: unknown): asserts value is unknown[] => + is.assertType( + is.sparseArray(value), + AssertionTypeDescription.sparseArray, + value, + ); + + static typedArray = (value: unknown): asserts value is TypedArray => + is.assertType( + is.typedArray(value), + AssertionTypeDescription.typedArray, + value, + ); + + static int8Array = function int8Array( + value: unknown, + ): asserts value is Int8Array { + return is.assertType(is.int8Array(value), "Int8Array", value); + }; + + static uint8Array = function uint8Array( + value: unknown, + ): asserts value is Uint8Array { + return is.assertType(is.uint8Array(value), "Uint8Array", value); + }; + + static uint8ClampedArray = ( + value: unknown, + ): asserts value is Uint8ClampedArray => + is.assertType(is.uint8ClampedArray(value), "Uint8ClampedArray", value); + + static int16Array = function int16Array( + value: unknown, + ): asserts value is Int16Array { + return is.assertType(is.int16Array(value), "Int16Array", value); + }; + + static uint16Array = function uint16Array( + value: unknown, + ): asserts value is Uint16Array { + return is.assertType(is.uint16Array(value), "Uint16Array", value); + }; + + static int32Array = function int32Array( + value: unknown, + ): asserts value is Int32Array { + return is.assertType(is.int32Array(value), "Int32Array", value); + }; + + static uint32Array = function uint32Array( + value: unknown, + ): asserts value is Uint32Array { + return is.assertType(is.uint32Array(value), "Uint32Array", value); + }; + + static float32Array = function float32Array( + value: unknown, + ): asserts value is Float32Array { + return is.assertType(is.float32Array(value), "Float32Array", value); + }; + + static float64Array = function float64Array( + value: unknown, + ): asserts value is Float64Array { + return is.assertType(is.float64Array(value), "Float64Array", value); + }; + + static bigInt64Array = function bigInt64Array( + value: unknown, + ): asserts value is BigInt64Array { + return is.assertType(is.bigInt64Array(value), "BigInt64Array", value); + }; + + static bigUint64Array = function bigUint64Array( + value: unknown, + ): asserts value is BigUint64Array { + return is.assertType(is.bigUint64Array(value), "BigUint64Array", value); + }; + + static arrayBuffer = function arrayBuffer( + value: unknown, + ): asserts value is ArrayBuffer { + return is.assertType(is.arrayBuffer(value), "ArrayBuffer", value); + }; + + static dataView = function dataView( + value: unknown, + ): asserts value is DataView { + return is.assertType(is.dataView(value), "DataView", value); + }; + + static sharedArrayBuffer = ( + value: unknown, + ): asserts value is SharedArrayBuffer => + is.assertType(is.sharedArrayBuffer(value), "SharedArrayBuffer", value); + + static domElement = (value: unknown): asserts value is HTMLElement => + is.assertType( + is.domElement(value), + AssertionTypeDescription.domElement, + value, + ); + + static element = (value: unknown): asserts value is Element => + is.assertType( + is.element(value), + AssertionTypeDescription.domElement, + value, + ); + + static svgElement = (value: unknown): asserts value is SVGElement => + is.assertType( + is.svgElement(value), + AssertionTypeDescription.svgElement, + value, + ); + + static observable = function observable( + value: unknown, + ): asserts value is ObservableLike { + return is.assertType(is.observable(value), "Observable", value); + }; + + static nodeStream = (value: unknown): asserts value is NodeJS.Stream => + is.assertType( + is.nodeStream(value), + AssertionTypeDescription.nodeStream, + value, + ); + + static buffer = function buffer(value: unknown): asserts value is Buffer { + return is.assertType(is.buffer(value), "Buffer", value); + }; + + static blob = function blob(value: unknown): asserts value is Blob { + return is.assertType(is.blob(value), "Blob", value); + }; + + static formData = function formData( + value: unknown, + ): asserts value is FormData { + return is.assertType(is.formData(value), "FormData", value); + }; + + static headers = function headers(value: unknown): asserts value is Headers { + return is.assertType(is.headers(value), "Headers", value); + }; + + static request = function request(value: unknown): asserts value is Request { + return is.assertType(is.request(value), "Request", value); + }; + + static response = function response( + value: unknown, + ): asserts value is Response { + return is.assertType(is.response(value), "Response", value); + }; + + static urlSearchParams = ( + value: unknown, + ): asserts value is URLSearchParams => + is.assertType(is.urlSearchParams(value), "URLSearchParams", value); + + static urlInstance = function urlInstance( + value: unknown, + ): asserts value is URL { + return is.assertType(is.urlInstance(value), "URL", value); + }; + + static urlString = (value: unknown): asserts value is string => + is.assertType( + is.urlString(value), + AssertionTypeDescription.urlString, + value, + ); + + static url = function url(value: unknown): asserts value is string | URL { + return is.assertType(is.url(value), AssertionTypeDescription.url, value); + }; + + // Numbers. + static nan = function nan(value: unknown): asserts value is unknown { + return is.assertType(is.nan(value), AssertionTypeDescription.nan, value); + }; + + static integer = function integer(value: unknown): asserts value is number { + return is.assertType( + is.integer(value), + AssertionTypeDescription.integer, + value, + ); + }; + + static safeInteger = (value: unknown): asserts value is number => + is.assertType( + is.safeInteger(value), + AssertionTypeDescription.safeInteger, + value, + ); + + static evenInteger = (value: number): asserts value is number => + is.assertType( + is.evenInteger(value), + AssertionTypeDescription.evenInteger, + value, + ); + + static oddInteger = (value: number): asserts value is number => + is.assertType( + is.oddInteger(value), + AssertionTypeDescription.oddInteger, + value, + ); + + static infinite = function infinite(value: unknown): asserts value is number { + return is.assertType( + is.infinite(value), + AssertionTypeDescription.infinite, + value, + ); + }; + + static numericString = (value: unknown): asserts value is string => + is.assertType( + is.numericString(value), + AssertionTypeDescription.numericString, + value, + ); + + static inRange = ( + value: number, + range: number | number[], + ): asserts value is number => + is.assertType( + is.inRange(value, range), + AssertionTypeDescription.inRange, + value, + ); + + static emptyArray = (value: unknown): asserts value is never[] => + is.assertType( + is.emptyArray(value), + AssertionTypeDescription.emptyArray, + value, + ); + + static emptySet = function emptySet( + value: unknown, + ): asserts value is Set { + return is.assertType( + is.emptySet(value), + AssertionTypeDescription.emptySet, + value, + ); + }; + + static emptyMap = function emptyMap( + value: unknown, + ): asserts value is Map { + return is.assertType( + is.emptyMap(value), + AssertionTypeDescription.emptyMap, + value, + ); + }; + + static emptyObject = ( + value: unknown, + ): asserts value is Record => + is.assertType( + is.emptyObject(value), + AssertionTypeDescription.emptyObject, + value, + ); + + static emptyString = (value: unknown): asserts value is "" => + is.assertType( + is.emptyString(value), + AssertionTypeDescription.emptyString, + value, + ); + + static whitespace = (value: unknown): asserts value is string => + is.assertType( + is.whitespace(value), + AssertionTypeDescription.whitespace, + value, + ); + + static emptyStringOrWhitespace = ( + value: unknown, + ): asserts value is string => + is.assertType( + is.emptyStringOrWhitespace(value), + AssertionTypeDescription.emptyStringOrWhitespace, + value, + ); + + static nonEmptyArray = ( + value: unknown, + ): asserts value is [unknown, ...unknown[]] => + is.assertType( + is.nonEmptyArray(value), + AssertionTypeDescription.nonEmptyArray, + value, + ); + + static nonEmptySet = ( + value: unknown, + ): asserts value is Set => + is.assertType( + is.nonEmptySet(value), + AssertionTypeDescription.nonEmptySet, + value, + ); + + static nonEmptyMap = ( + value: unknown, + ): asserts value is Map => + is.assertType( + is.nonEmptyMap(value), + AssertionTypeDescription.nonEmptyMap, + value, + ); + + static nonEmptyObject = ( + value: unknown, + ): asserts value is Record => + is.assertType( + is.nonEmptyObject(value), + AssertionTypeDescription.nonEmptyObject, + value, + ); + + static nonEmptyString = (value: unknown): asserts value is string => + is.assertType( + is.nonEmptyString(value), + AssertionTypeDescription.nonEmptyString, + value, + ); + + static nonEmptyStringAndNotWhitespace = ( + value: unknown, + ): asserts value is string => + is.assertType( + is.nonEmptyStringAndNotWhitespace(value), + AssertionTypeDescription.nonEmptyStringAndNotWhitespace, + value, + ); + + // Variadic functions. + static any = ( + predicate: Predicate | Predicate[], + ...values: unknown[] + ): void | never => + is.assertType( + is.any(predicate, ...values), + AssertionTypeDescription.any, + values, + { multipleValues: true }, + ); + + static all = ( + predicate: Predicate | Predicate[], + ...values: unknown[] + ): void | never => + is.assertType( + is.all(predicate, ...values), + AssertionTypeDescription.all, + values, + { multipleValues: true }, + ); + + static every = ( + predicate: Predicate | Predicate[], + ...values: unknown[] + ): void | never => + is.assertType( + is.every(predicate, ...values), + AssertionTypeDescription.every, + values, + { multipleValues: true }, + ); + + static some = ( + predicate: Predicate | Predicate[], + ...values: unknown[] + ): void | never => + is.assertType( + is.some(predicate, ...values), + AssertionTypeDescription.some, + values, + { multipleValues: true }, + ); + + static none = ( + predicate: Predicate | Predicate[], + ...values: unknown[] + ): void | never => + is.assertType( + is.none(predicate, ...values), + AssertionTypeDescription.none, + values, + { multipleValues: true }, + ); + + /** @deprecated use {@linkcode assert.function} instead */ + @deprecated({ + since: "0.1.0", + substitute: Assertions.function, + hide: true, + seal: true, + }) + static function_ = function function_( + value: unknown, + ): asserts value is Function { + return is.assertType(is.function(value), "Function", value); + }; + + /** @deprecated use {@linkcode assert.class} instead */ + @deprecated({ + since: "0.1.0", + substitute: Assertions.class, + hide: true, + seal: true, + }) + static class_ = function class_

( + value: unknown, + ): asserts value is Class

{ + return is.assertType( + is.class

(value), + AssertionTypeDescription.class_, + value, + ); + }; + + /** @deprecated use {@linkcode assert.null} instead */ + @deprecated({ + since: "0.1.0", + substitute: Assertions.null, + hide: true, + seal: true, + }) + static null_ = function null_(value: unknown): asserts value is null { + return is.assertType(is.null(value), "null", value); + }; + + static [DenoCustomInspect](inspect: typeof Deno.inspect) { + const options: Deno.InspectOptions = { + colors: true, + compact: true, + depth: 1, + getters: true, + showHidden: false, + showProxy: false, + sorted: true, + trailingComma: true, + }; + + return `is.assert ${inspect({ ...this }, options)}`; } -}; +} /** * Type Assertions. If conditions are not as expected, throws a TypeError. */ const assert: Assert = Object.assign( - function assert( - condition: boolean, - value: unknown, - message?: unknown, - options?: AssertOptions, - ): asserts condition { - if (!condition) { - if (is.undefined(message) && is.undefined(options)) { - if (is.string(value)) { - throw new TypeError(`Assertion Failure: ${String(value)}`); + is.assertType as Partial, + Assertions as unknown as Assert, +) as Assert; + +interface is extends Id { + (value: T): GetTypeName; + (value: unknown): TypeName; + assert: Assert; +} + +type DeprecatedMethods = `${"null" | "function" | "class"}_`; +type ExcludedMethods = + | DeprecatedMethods + | "assert" + | "assertType" + | "prototype" + | "constructor" + | "arguments" + | "caller" + | "callee" + | "name" + | "typeName" + | "not" + | "negated" + | typeof Symbol.toStringTag + | typeof IsNegated + | typeof DenoCustomInspect; + +const _excludedMethods = [ + ...Reflect.ownKeys(Function.prototype), + "null_", + "function_", + "class_", + "not", + "typeName", + "assert", + "assertType", +] as const; + +type TypeChecks = NonNullable< + ConditionalExcept< + Except>, + never | null | undefined + > +>; + +type TypeCheckNames = keyof TypeChecks; + +// deno-fmt-ignore +type Args = ( + | T extends ((...a: infer P extends any[]) => any) ? P + : T extends (abstract new (...a: infer P extends any[]) => any) ? P + : never[] +); + +type NegatedMethods< + Base, + Keys extends keyof Base = keyof Base, +> = Flatten< + ConditionalExcept< + { + [K in Keys as Filter]: + ((...args: Args) => boolean); + }, + never + > +>; + +// deno-fmt-ignore +interface isnt extends NegatedMethods { + [IsNegated]: boolean; + /* negated interface */ + (value: unknown): boolean; +} + +type Id = U extends infer T extends Record | unknown[] ? { + [K in keyof T]: T[K] extends + Record | unknown[] ? Id : T[K]; + } + : U; + +// formatPropertyDescriptors(is as unknown as is, { sealed: false }); + +// function createNegated(this: T): { +// proxy: U; +// }; + +interface CreateNegatedOptions { + /** Names/Symbols of properties to exclude from the proxied object. */ + excluded?: (string | symbol)[]; + + /** + * Custom value to use for the `Symbol.toStringTag` property. + * @default "is.not" + */ + toStringTag?: string; + + /** + * Create a revocable negated object using `Proxy.revocable`. This returns an + * additional property alongside `proxy` (named `revoke`), which will destroy + * the proxy instance and free it for garbage collection once it is invoked. + * + * **Note**: Proxy revocation is a one-way operation. It cannot be undone. + * @see https://mdn.io/Proxy.revocable + * @default false + */ + revocable?: boolean; + + /** + * Rename proxied functions for methods and getters/setters. Helps the final + * object appear slightly more indistinguishable from the original target. + * @default true + */ + maskMethodNames?: boolean; + + /** + * Sort the proxied method names in ascending alphabetic order. + */ + sorted?: boolean; +} + +interface RevocableNegatedOptions extends CreateNegatedOptions { + revocable: true; +} + +function createNegated< + T extends is | Assert | typeof is | typeof Assertions, + U extends isnt, +>( + this: T, +): { proxy: U }; +function createNegated< + T extends is | Assert | typeof is | typeof Assertions, + U extends isnt, +>( + this: T, + options: RevocableNegatedOptions, +): { proxy: U; revoke: () => void }; +function createNegated< + T extends is | Assert | typeof is | typeof Assertions, + U extends isnt, +>( + this: T, + options?: CreateNegatedOptions, +): { proxy: U }; +function createNegated< + T extends is | Assert | typeof is | typeof Assertions, + U extends isnt, +>( + this: T, + options: CreateNegatedOptions = { + revocable: false, + maskMethodNames: true, + }, +): any { + options.sorted ??= true; + options.maskMethodNames ??= true; + options.toStringTag ??= "is.not"; + options.excluded ??= [ + // "assert", + // "assertType", + "typeName", + "negated", + "namespaceModule", + ]; + // properties to exclude from the returned object + const deprecatedProperties = Reflect.ownKeys( + Reflect.get(this, IsDeprecated, this), + ).filter(is.nonEmptyStringAndNotWhitespace); + + const excluded = Array.from( + new Set([ + "not", + ...options?.excluded, + IsDeprecated, + DenoCustomInspect, + ...deprecatedProperties, + ]), + ); + + /** + * Handle the results of one of the typecheck / assertion methods, applying + * the negation modifier whenever it seems appropriate. + */ + function handleResult( + this: any, + target: any, + args: unknown[], + result: unknown, + ) { + if (is.function(result) && !is.promise(result)) { + result = result.apply(target, args); + } + + if (is.promise(result)) { + return result.then((result) => { + if (is.boolean(result) && this[IsNegated]) { + this[IsNegated] = false; + return !result; } - } - const { multipleValues = false } = (options || {}); - const values = [...new Set([value as any].flat())]; - const msg = multipleValues - ? `values of types ${ - values.map((v, i) => - `${i === values.length - 1 ? "and " : ""}\`${is(v)}\`` - ).join(", ") - }` - : `value of type \`${is(value)}\``; + return result; + }); + } + if (is.boolean(result) && this[IsNegated]) { + this[IsNegated] = false; + return !result; + } + return result; + } - throw new TypeError( - `Assertion Failure: Expected value of \`${message}\` but received ${msg}.`, - ); + /** Check if a property is marked for exclusion from the proxy. */ + function isExcluded(property: string | symbol): boolean { + if (is.symbol(property)) { + return excluded.includes(property); } - }, - { - // Unknowns. - undefined: (value: unknown): asserts value is undefined => - assertType(is.undefined(value), "undefined", value), - null: (value: unknown): asserts value is null => - assertType(is.null(value), "null", value), - nullOrUndefined: (value: unknown): asserts value is null | undefined => - assertType( - is.nullOrUndefined(value), - AssertionTypeDescription.nullOrUndefined, - value, - ), - string: (value: unknown): asserts value is string => - assertType(is.string(value), "string", value), - number: (value: unknown): asserts value is number => - assertType(is.number(value), "number", value), - bigint: (value: unknown): asserts value is bigint => - assertType(is.bigint(value), "bigint", value), - function: (value: unknown): asserts value is Function => - assertType(is.function(value), "Function", value), - class:

(value: unknown): asserts value is Class

=> - assertType(is.class

(value), AssertionTypeDescription.class_, value), - nullish: (value: unknown): asserts value is null | undefined => - assertType( - is.nullOrUndefined(value), - AssertionTypeDescription.nullOrUndefined, - value, - ), - object: (value: unknown): asserts value is object => - assertType(is.object(value), "Object", value), - boolean: (value: unknown): asserts value is boolean => - assertType(is.boolean(value), "boolean", value), - symbol: (value: unknown): asserts value is symbol => - assertType(is.symbol(value), "symbol", value), - truthy: (value: unknown): asserts value is unknown => - assertType(is.truthy(value), AssertionTypeDescription.truthy, value), - falsy: (value: unknown): asserts value is unknown => - assertType(is.falsy(value), AssertionTypeDescription.falsy, value), - primitive: (value: unknown): asserts value is Primitive => - assertType( - is.primitive(value), - AssertionTypeDescription.primitive, - value, - ), - array: ( - value: unknown, - assertion?: (element: unknown) => asserts element is T, - ): asserts value is T[] => { - const assert: ( - condition: boolean, - description: string, - value: unknown, - ) => asserts condition = assertType; + return excluded.some((pattern) => + !is.symbol(pattern) && new RegExp(pattern).test(property) + ); + } - assert(is.array(value), "Array", value); + // return (revocable ? Proxy.revocable : new Proxy)(this as unknown as U, + const handler: ProxyHandler = { + get(target, p, receiver) { + if (isExcluded(p)) { + return undefined; + } - if (assertion) { - value.forEach(assertion); + if (p === Symbol.toStringTag) { + return options.toStringTag ?? "is.not"; } - }, - iterable: (value: unknown): asserts value is Iterable => - assertType(is.iterable(value), AssertionTypeDescription.iterable, value), - asyncIterable: ( - value: unknown, - ): asserts value is AsyncIterable => - assertType( - is.asyncIterable(value), - AssertionTypeDescription.asyncIterable, - value, - ), - generator: (value: unknown): asserts value is Generator => - assertType(is.generator(value), "Generator", value), - asyncGenerator: (value: unknown): asserts value is AsyncGenerator => - assertType(is.asyncGenerator(value), "AsyncGenerator", value), - nativePromise: (value: unknown): asserts value is Promise => - assertType( - is.nativePromise(value), - AssertionTypeDescription.nativePromise, - value, - ), - promise: (value: unknown): asserts value is Promise => - assertType(is.promise(value), "Promise", value), - generatorFunction: (value: unknown): asserts value is GeneratorFunction => - assertType(is.generatorFunction(value), "GeneratorFunction", value), - asyncGeneratorFunction: ( - value: unknown, - ): asserts value is AsyncGeneratorFunction => - assertType( - is.asyncGeneratorFunction(value), - "AsyncGeneratorFunction", - value, - ), + // equivalent to `value = target[p];` + let value = Reflect.get(target, p, target); + value ??= Reflect.get(target, p, receiver); + value ??= Reflect.getOwnPropertyDescriptor(target, p)?.value; + + if (is.function(value)) { + const proxiedMethod = function proxiedMethod( + this: any, + ...args: any[] + ) { + const result = (value as Function).apply( + this === receiver ? target : this, + args, + ); + return handleResult.call( + this === receiver ? target : this, + target, + args, + result, + ); + }; + + if (options?.maskMethodNames) { + renameFunction(proxiedMethod, p); + } + return proxiedMethod; + } + // otherwise... + return value; + }, + getOwnPropertyDescriptor(target, p) { + if (excluded.includes(p)) { + return undefined; + } + const desc = Reflect.getOwnPropertyDescriptor(target, p); + // sanity check + if (is.propertyDescriptor(desc)) { + if (is.dataDescriptor(desc)) { + if (is.function(desc.value)) { + const value = desc.value; + + const valueProxy = function valueProxy(this: any, ...args: any[]) { + const result = value.apply(target, args); + return handleResult.call(target, this, args, result); + }; + + if (options?.maskMethodNames) renameFunction(valueProxy, p); + desc.value = valueProxy; + } + } else if (is.accessorDescriptor(desc) && is.function(desc.get)) { + const get = desc.get; + + const getterProxy = function getterProxy() { + const result = get.apply(target); + return handleResult.call(target, target, [], result); + }; + + if (options?.maskMethodNames) { + renameFunction(getterProxy, p); + } + desc.get = getterProxy; + } + return desc; + } + }, + ownKeys(target) { + let keys: Set | (string | symbol)[] = new Set( + Reflect.ownKeys(target).filter((key) => !excluded.includes(key)), + ).add(Symbol.toStringTag); + + keys = [...keys] as (string | symbol)[]; + return options?.sorted + ? keys.sort((a, b) => + String((a as symbol)?.description ?? a).localeCompare( + String((b as symbol)?.description ?? b), + ) + ) + : keys; + }, + }; - asyncFunction: (value: unknown): asserts value is Function => - assertType(is.asyncFunction(value), "AsyncFunction", value), - boundFunction: (value: unknown): asserts value is Function => - assertType(is.boundFunction(value), "Function", value), - directInstanceOf: ( - instance: unknown, - class_: Class, - ): asserts instance is T => - assertType( - is.directInstanceOf(instance, class_), - AssertionTypeDescription.directInstanceOf, - instance, - ), - instanceOf: ( - instance: unknown, - class_: Class, - ): asserts instance is T => - assertType( - is.instanceOf(instance, class_), - AssertionTypeDescription.directInstanceOf, - instance, - ), + return options?.revocable + ? Proxy.revocable(this as unknown as U, handler) + : { proxy: new Proxy(this as unknown as U, handler) }; +} - regExp: (value: unknown): asserts value is RegExp => - assertType(is.regExp(value), "RegExp", value), - regex: (value: unknown): asserts value is RegExp => - assertType(is.regExp(value), "RegExp", value), - date: (value: unknown): asserts value is Date => - assertType(is.date(value), "Date", value), - error: (value: unknown): asserts value is Error => - assertType(is.error(value), "Error", value), +/** + * Determine the {@linkcode TypeName} of an arbitrary value of unknown type. + * + * @example + * ```ts + * import { is } from "https://deno.land/x/dis/mod.ts"; + * + * is("🦕") // => "string" + * is(100n) // => "bigint" + * is({ foo: "bar" }) // => "Object" + * is(new Uint8Array()) // => "Uint8Array" + * ``` + * + * @example + * ```ts + * import { is, assert } from "https://deno.land/x/dis/mod.ts"; + * + * let eitherStringOrNumber: string | number = "1"; + * + * eitherStringOrNumber = 1; + * // type is still `string | number` + * + * assert.number(eitherStringOrNumber); + * // type is now number + * + * is.assert.plainObject(new Map()) + * // => Unexpected TypeError: ... + * ``` + */ +// @ts-ignore janky re-assignment +is = new Proxy(is as unknown as is, { + apply( + _target: is, + _thisArg: any, + args: [value: T], + ): GetTypeName { + return getTypeName(args[0] as T) as GetTypeName; + }, + construct(target, args, newTarget) { + console.warn( + [ + `Warning: improper usage of the \`new\` operator. The \`is\` module is a static class and cannot be instantiated in prototype form.`, + " ", + `Instead, try one of these supported syntax examples:`, + ` • is(value: unknown) => TypeName`, + ` • is.assert(expr: boolean, msg?: string) => asserts expr`, + ` • is.string(value: unknown) => value is string`, + ` • is.not.string(value: unknown) => boolean`, + ].join("\n"), + ); - map: ( - value: unknown, - ): asserts value is Map => - assertType(is.map(value), "Map", value), - set: (value: unknown): asserts value is Set => - assertType(is.set(value), "Set", value), - weakMap: ( - value: unknown, - ): asserts value is WeakMap => - assertType(is.weakMap(value), "WeakMap", value), - weakSet: ( - value: unknown, - ): asserts value is WeakSet => - assertType(is.weakSet(value), "WeakSet", value), - weakRef: ( - value: unknown, - ): asserts value is WeakRef => - assertType(is.weakRef(value), "WeakRef", value), - - arrayLike: (value: unknown): asserts value is ArrayLike => - assertType( - is.arrayLike(value), - AssertionTypeDescription.arrayLike, - value, - ), - namespaceModule: (value: unknown): asserts value is Module => - assertType(is.namespaceModule(value), "Module", value), - plainObject: ( - value: unknown, - ): asserts value is Record => - assertType( - is.plainObject(value), - AssertionTypeDescription.plainObject, - value, - ), - propertyKey: (value: unknown): asserts value is number => - assertType(is.propertyKey(value), "PropertyKey", value), - propertyDescriptor: ( - value: unknown, - ): asserts value is TypedPropertyDescriptor => - assertType( - is.propertyDescriptor(value), - "PropertyDescriptor", - value, + return Reflect.apply(target, newTarget, args) as unknown as is; + }, + get(target, p, receiver) { + switch (p) { + case "name":/* fall through */ + case Symbol.toStringTag: + return "is"; + case "assert":/* fall through */ + case "asserts": { + // proxied to handle the .not operator + return new Proxy(assert, { + get(target, p, _receiver) { + if (p === "not") { + Reflect.set(target, IsNegated, true, target); + return createNegated.call(assert, { + revocable: false, + sorted: true, + excluded: ["assert"], + maskMethodNames: true, + }).proxy; + } + return Reflect.get(target, p, target); + }, + ownKeys(target) { + return [...Reflect.ownKeys(target), "not"]; + }, + }); + } + case "not": { + Reflect.set(target, IsNegated, true, target); + return createNegated.call(target, { + revocable: false, + sorted: true, + excluded: ["assert"], + maskMethodNames: true, + }).proxy; + } + default: + return Reflect.get(target, p, receiver); + } + }, + ownKeys(target) { + const excluded = [ + "assertType", + DenoCustomInspect, + ]; + + const keys = Array.from( + new Set( + Reflect.ownKeys(target).filter((key) => !excluded.includes(key)), ), - accessorDescriptor: ( - value: unknown, - ): asserts value is AccessorDescriptor => - assertType(is.accessorDescriptor(value), "AccessorDescriptor", value), - dataDescriptor: ( - value: unknown, - ): asserts value is DataDescriptor => - assertType(is.dataDescriptor(value), "DataDescriptor", value), - key: = {}>( - value: unknown, - target: T, - ): asserts value is keyof T => - assertType(is.key(value, target), "Key", value), - value: = {}>( - value: unknown, - target: T, - ): asserts value is T[keyof T] => - assertType(is.value(value, target), "Value", value), + ); - enumKey: ( - value: unknown, - targetEnum: T, - ): asserts value is keyof T => - assertType(is.enumKey(value, targetEnum), "EnumKey", value), - enumCase: ( - value: unknown, - targetEnum: T, - ): asserts value is T[keyof T] => - assertType(is.enumCase(value, targetEnum), "EnumCase", value), + return keys.toSorted((a, b) => + String((a as symbol)?.description ?? a).localeCompare( + String((b as symbol)?.description ?? b), + ) + ); + }, + getOwnPropertyDescriptor(target, p) { + const desc = Reflect.getOwnPropertyDescriptor(target, p) ?? {}; - entry: ( - value: unknown, - ): asserts value is readonly [K, V] => - assertType( - is.entry(value), - AssertionTypeDescription.entry, - value, - ), - entries: ( - value: unknown, - ): asserts value is readonly (readonly [K, V])[] => - assertType( - is.entries(value), - AssertionTypeDescription.entries, - value, - ), - sparseArray: (value: unknown): asserts value is unknown[] => - assertType( - is.sparseArray(value), - AssertionTypeDescription.sparseArray, - value, - ), - typedArray: (value: unknown): asserts value is TypedArray => - assertType( - is.typedArray(value), - AssertionTypeDescription.typedArray, - value, - ), - int8Array: (value: unknown): asserts value is Int8Array => - assertType(is.int8Array(value), "Int8Array", value), - uint8Array: (value: unknown): asserts value is Uint8Array => - assertType(is.uint8Array(value), "Uint8Array", value), - uint8ClampedArray: (value: unknown): asserts value is Uint8ClampedArray => - assertType(is.uint8ClampedArray(value), "Uint8ClampedArray", value), - int16Array: (value: unknown): asserts value is Int16Array => - assertType(is.int16Array(value), "Int16Array", value), - uint16Array: (value: unknown): asserts value is Uint16Array => - assertType(is.uint16Array(value), "Uint16Array", value), - int32Array: (value: unknown): asserts value is Int32Array => - assertType(is.int32Array(value), "Int32Array", value), - uint32Array: (value: unknown): asserts value is Uint32Array => - assertType(is.uint32Array(value), "Uint32Array", value), - float32Array: (value: unknown): asserts value is Float32Array => - assertType(is.float32Array(value), "Float32Array", value), - float64Array: (value: unknown): asserts value is Float64Array => - assertType(is.float64Array(value), "Float64Array", value), - bigInt64Array: (value: unknown): asserts value is BigInt64Array => - assertType(is.bigInt64Array(value), "BigInt64Array", value), - bigUint64Array: (value: unknown): asserts value is BigUint64Array => - assertType(is.bigUint64Array(value), "BigUint64Array", value), - arrayBuffer: (value: unknown): asserts value is ArrayBuffer => - assertType(is.arrayBuffer(value), "ArrayBuffer", value), - sharedArrayBuffer: (value: unknown): asserts value is SharedArrayBuffer => - assertType(is.sharedArrayBuffer(value), "SharedArrayBuffer", value), - dataView: (value: unknown): asserts value is DataView => - assertType(is.dataView(value), "DataView", value), - - domElement: (value: unknown): asserts value is HTMLElement => - assertType( - is.domElement(value), - AssertionTypeDescription.domElement, - value, - ), - observable: (value: unknown): asserts value is ObservableLike => - assertType(is.observable(value), "Observable", value), - nodeStream: (value: unknown): asserts value is NodeJS.Stream => - assertType( - is.nodeStream(value), - AssertionTypeDescription.nodeStream, - value, - ), - buffer: (value: unknown): asserts value is Buffer => - assertType(is.buffer(value), "Buffer", value), - blob: (value: unknown): asserts value is Blob => - assertType(is.blob(value), "Blob", value), - - formData: (value: unknown): asserts value is FormData => - assertType(is.formData(value), "FormData", value), - headers: (value: unknown): asserts value is Headers => - assertType(is.headers(value), "Headers", value), - request: (value: unknown): asserts value is Request => - assertType(is.request(value), "Request", value), - response: (value: unknown): asserts value is Response => - assertType(is.response(value), "Response", value), - urlSearchParams: (value: unknown): asserts value is URLSearchParams => - assertType(is.urlSearchParams(value), "URLSearchParams", value), - urlInstance: (value: unknown): asserts value is URL => - assertType(is.urlInstance(value), "URL", value), - urlString: (value: unknown): asserts value is string => - assertType( - is.urlString(value), - AssertionTypeDescription.urlString, - value, - ), - url: (value: unknown): asserts value is string | URL => - assertType(is.url(value), AssertionTypeDescription.url, value), - - // Numbers. - nan: (value: unknown): asserts value is unknown => - assertType(is.nan(value), AssertionTypeDescription.nan, value), - integer: (value: unknown): asserts value is number => - assertType(is.integer(value), AssertionTypeDescription.integer, value), - safeInteger: (value: unknown): asserts value is number => - assertType( - is.safeInteger(value), - AssertionTypeDescription.safeInteger, - value, - ), - evenInteger: (value: number): asserts value is number => - assertType( - is.evenInteger(value), - AssertionTypeDescription.evenInteger, - value, - ), - oddInteger: (value: number): asserts value is number => - assertType( - is.oddInteger(value), - AssertionTypeDescription.oddInteger, - value, - ), - infinite: (value: unknown): asserts value is number => - assertType(is.infinite(value), AssertionTypeDescription.infinite, value), - numericString: (value: unknown): asserts value is string => - assertType( - is.numericString(value), - AssertionTypeDescription.numericString, - value, - ), - inRange: ( - value: number, - range: number | number[], - ): asserts value is number => - assertType( - is.inRange(value, range), - AssertionTypeDescription.inRange, - value, - ), + if (isDeprecated(target, p) || is.symbol(p)) { + return { ...desc, enumerable: false }; + } + return desc; + }, +}) as is; - emptyArray: (value: unknown): asserts value is never[] => - assertType( - is.emptyArray(value), - AssertionTypeDescription.emptyArray, - value, - ), - emptySet: (value: unknown): asserts value is Set => - assertType(is.emptySet(value), AssertionTypeDescription.emptySet, value), - emptyMap: (value: unknown): asserts value is Map => - assertType(is.emptyMap(value), AssertionTypeDescription.emptyMap, value), - emptyObject: ( - value: unknown, - ): asserts value is Record => - assertType( - is.emptyObject(value), - AssertionTypeDescription.emptyObject, - value, - ), - emptyString: (value: unknown): asserts value is "" => - assertType( - is.emptyString(value), - AssertionTypeDescription.emptyString, - value, - ), - whitespace: (value: unknown): asserts value is string => - assertType( - is.whitespace(value), - AssertionTypeDescription.whitespace, - value, - ), - emptyStringOrWhitespace: (value: unknown): asserts value is string => - assertType( - is.emptyStringOrWhitespace(value), - AssertionTypeDescription.emptyStringOrWhitespace, - value, - ), +/** + * Determine the {@linkcode TypeName} of an arbitrary value of unknown type. + * + * @example + * ```ts + * import { is } from "https://deno.land/x/dis/mod.ts"; + * + * getTypeName("🦕") // => "string" + * getTypeName(100n) // => "bigint" + * getTypeName({ foo: "bar" }) // => "Object" + * getTypeName(new Uint8Array()) // => "Uint8Array" + * ``` + */ +function getTypeName(value: T) { + return ((value: unknown) => { + if (value === null) { + return "null"; + } - nonEmptyArray: (value: unknown): asserts value is [unknown, ...unknown[]] => - assertType( - is.nonEmptyArray(value), - AssertionTypeDescription.nonEmptyArray, - value, - ), - nonEmptySet: (value: unknown): asserts value is Set => - assertType( - is.nonEmptySet(value), - AssertionTypeDescription.nonEmptySet, - value, - ), - nonEmptyMap: ( - value: unknown, - ): asserts value is Map => - assertType( - is.nonEmptyMap(value), - AssertionTypeDescription.nonEmptyMap, - value, - ), - nonEmptyObject: ( - value: unknown, - ): asserts value is Record => - assertType( - is.nonEmptyObject(value), - AssertionTypeDescription.nonEmptyObject, - value, - ), - nonEmptyString: (value: unknown): asserts value is string => - assertType( - is.nonEmptyString(value), - AssertionTypeDescription.nonEmptyString, - value, - ), - nonEmptyStringAndNotWhitespace: (value: unknown): asserts value is string => - assertType( - is.nonEmptyStringAndNotWhitespace(value), - AssertionTypeDescription.nonEmptyStringAndNotWhitespace, - value, - ), + switch (typeof value) { + case "undefined": + return "undefined"; + case "string": + return "string"; + case "number": + return Number.isNaN(value) ? "NaN" : "number"; + case "boolean": + return "boolean"; + case "function": + return "Function"; + case "bigint": + return "bigint"; + case "symbol": + return "symbol"; + default: + } - // Variadic functions. - any: ( - predicate: Predicate | Predicate[], - ...values: unknown[] - ): void | never => - assertType( - is.any(predicate, ...values), - AssertionTypeDescription.any, - values, - { multipleValues: true }, - ), - all: ( - predicate: Predicate | Predicate[], - ...values: unknown[] - ): void | never => - assertType( - is.all(predicate, ...values), - AssertionTypeDescription.all, - values, - { multipleValues: true }, - ), - every: ( - predicate: Predicate | Predicate[], - ...values: unknown[] - ): void | never => - assertType( - is.every(predicate, ...values), - AssertionTypeDescription.every, - values, - { multipleValues: true }, - ), - some: ( - predicate: Predicate | Predicate[], - ...values: unknown[] - ): void | never => - assertType( - is.some(predicate, ...values), - AssertionTypeDescription.some, - values, - { multipleValues: true }, - ), - none: ( - predicate: Predicate | Predicate[], - ...values: unknown[] - ): void | never => - assertType( - is.none(predicate, ...values), - AssertionTypeDescription.none, - values, - { multipleValues: true }, - ), - // deprecated - function_: (value: unknown): asserts value is Function => - assertType(is.function(value), "Function", value), - class_:

(value: unknown): asserts value is Class

=> - assertType(is.class

(value), AssertionTypeDescription.class_, value), - null_: (value: unknown): asserts value is null => - assertType(is.null(value), "null", value), - } as Assertions, -) as Assert; + if (is.observable(value)) { + return "Observable"; + } -// is.assert = assert as Assert; + if (is.array(value)) { + return "Array"; + } -declare namespace is { - // deno-lint-ignore no-empty-interface - interface assert extends Assert {} - const assert: assert; -} + if (is.nodeBuffer(value)) { + return "Buffer"; + } -deprecate(assert, "function_", "null_", "class_", { seal: true }); -deprecate(is, "function_", "null_", "class_", "falsey", "NaN", { seal: true }); + if (is.denoBuffer(value)) { + return "Buffer"; + } -assign(is, { assert: assert as is.assert }); -freeze(assert, is.assert, is); + if (is.boxedPrimitive(value)) { + throw new TypeError( + "Please don't use object wrappers for primitive types", + ); + } + + return getObjectType(value) ?? "Object"; + })(value) as GetTypeName; +} -export { assert, is, is as default }; -export type { Assert, Assertions, TypeName }; +export default is as unknown as is; +export { type Assert, assert, type GetTypeName, is, type TypeName }; diff --git a/mod.ts b/mod.ts index 6623792..806777b 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,7 @@ -export { assert, is, is as default } from "./is.ts"; - -export type { Assert, Assertions, TypeName } from "./is.ts"; +/** + * @module 🦕 Deno typechecks and assertions with ease 💪 + * @see https://deno.land/x/dis@0.2.0/README.md for examples and feature docs + * @see https://deno.land/x/dis@0.2.0/mod.ts?doc for API docs (auto-generated) + */ +export { assert, default, default as is } from "./is.ts"; +export type { Assert, TypeName } from "./is.ts"; diff --git a/node.d.ts b/node.d.ts index e9baf4b..da57924 100644 --- a/node.d.ts +++ b/node.d.ts @@ -49,3 +49,41 @@ declare interface ObservableLike { } declare type Falsy = false | 0 | 0n | "" | null | undefined; + +interface SymbolConstructor { + /** + * A method that describes the object as `Observable` and returns an object + * with a `subscribe` method. That method accepts an `observer` for its + * single argument, which in turn accepts a arbitrary `value` argument. The + * subscribe method may return an `unsubscribe` method to run as a cleanup + * or teardown routine when the observable is disposed. + * + * @see https://github.com/tc39/proposal-observable for more details. + * @see {@linkcode ObservableLike} for the interface this matches. + */ + readonly observable: unique symbol; +} + +/** + * Matches a value that is like an Observable. + * @see https://github.com/tc39/proposal-observable + */ +interface ObservableLike { + subscribe(observer: (value: unknown) => void): void; + [Symbol.observable](): ObservableLike; +} + +/** + * The "Printable" Primitives - `string`, `number`, `boolean`, `bigint` - are + * the subset of the Primitive types that can be printed in Template Literal + * types (a feature of TypeScript 4.1+). + * + * _Technically_ `null` and `undefined` are also printable, but only as the + * literal strings `"null"` and `"undefined"`, respectively. As such, they + * are not included in this type. + * + * @see {@linkcode MaybePrintable} if you need to include `null` and `undefined` in the Printable type for your use case. + */ +declare type Printable = NonNullable>; + +declare type MaybePrintable = Exclude; diff --git a/types.ts b/types.ts index 12dfcbe..264f455 100644 --- a/types.ts +++ b/types.ts @@ -10,6 +10,9 @@ import { export { Buffer, DenoBuffer, NodeBuffer }; +export * from "./_util/types.d.ts"; +export * from "./_util/typenames.ts"; + export declare namespace NodeJS { export type Buffer = NodeBuffer; export type Stream = NodeStream; @@ -40,186 +43,16 @@ export declare namespace NodeJS { declare global { interface SymbolConstructor { - readonly observable: symbol; + /** + * A method that describes the object as `Observable` and returns an object + * with a `subscribe` method. That method accepts an `observer` for its + * single argument, which in turn accepts a arbitrary `value` argument. The + * subscribe method may return an `unsubscribe` method to run as a cleanup + * or teardown routine when the observable is disposed. + * + * @see https://github.com/tc39/proposal-observable for more details. + * @see {@linkcode ObservableLike} for the interface this matches. + */ + readonly observable: unique symbol; } } - -/** - * Matches a value that is like an Observable. - * @see https://github.com/tc39/proposal-observable - */ -export interface ObservableLike { - subscribe(observer: (value: unknown) => void): void; - [Symbol.observable](): ObservableLike; -} - -export type Falsy = false | 0 | 0n | "" | null | undefined; - -export interface WeakRef { - readonly [Symbol.toStringTag]: "WeakRef"; - deref(): T | undefined; -} - -/** - * Matches any primitive value. - * @see https://mdn.io/Primitive - */ -export type Primitive = - | null - | undefined - | string - | number - | boolean - | symbol - | bigint; - -export type Predicate = (value: unknown) => boolean; - -export type ArrayMethod = ( - fn: (value: unknown, index: number, array: unknown[]) => boolean, - thisArg?: unknown, -) => boolean; - -/** - * Matches a `class` constructor. - * @see https://mdn.io/Classes. - */ -export interface Constructor { - new (...args: Args): Proto; -} - -export interface Class< - Proto = unknown, - Args extends any[] = any[], -> extends Constructor { - readonly prototype: Proto; -} - -export type AccessorDescriptor = Omit< - TypedPropertyDescriptor, - "value" | "writable" ->; - -export type DataDescriptor = Pick< - TypedPropertyDescriptor, - "configurable" | "enumerable" | "writable" | "value" ->; - -export type MapIterator = ReturnType; - -export type SetIterator = ReturnType; - -export interface Module { - [property: string]: unknown; -} - -/** - * Matches any [typed array](https://mdn.io/TypedArray). - * @see https://mdn.io/TypedArray - */ -export type TypedArray = - | Int8Array - | Uint8Array - | Uint8ClampedArray - | Int16Array - | Uint16Array - | Int32Array - | Uint32Array - | Float32Array - | Float64Array - | BigInt64Array - | BigUint64Array; - -/** - * TypedArrays - */ -export const typedArrayTypeNames = [ - "Int8Array", - "Uint8Array", - "Uint8ClampedArray", - "Int16Array", - "Uint16Array", - "Int32Array", - "Uint32Array", - "Float32Array", - "Float64Array", - "BigInt64Array", - "BigUint64Array", -] as const; - -export type typedArrayTypeNames = typeof typedArrayTypeNames; -export type TypedArrayTypeName = typedArrayTypeNames[number]; - -export const objectTypeNames = [ - "Function", - "Generator", - "GeneratorFunction", - "AsyncFunction", - "AsyncGenerator", - "AsyncGeneratorFunction", - "Observable", - "Array", - "Buffer", - "Blob", - "Object", - "RegExp", - "Date", - "Error", - "Iterable", - "Iterator", - "IterableIterator", - "AsyncIterable", - "AsyncIterator", - "AsyncIterableIterator", - "Map Iterator", - "Set Iterator", - "Map", - "Set", - "WeakMap", - "WeakSet", - "WeakRef", - "ArrayBuffer", - "SharedArrayBuffer", - "DataView", - "Module", - "Promise", - "FormData", - "Headers", - "Request", - "Response", - "ReadableStream", - "WritableStream", - "TransformStream", - "Reader", - "Writer", - "FileInfo", - "Proxy", - "URLSearchParams", - "URLPattern", - "URL", - "HTMLElement", - "SVGElement", - "NaN", - ...typedArrayTypeNames, -] as const; - -export type objectTypeNames = typeof objectTypeNames; -export type ObjectTypeName = objectTypeNames[number]; - -/** - * Primitives - */ -export const primitiveTypeNames = [ - "null", - "undefined", - "string", - "number", - "bigint", - "boolean", - "symbol", - "object", - "function", -] as const; -export type primitiveTypeNames = typeof primitiveTypeNames; -export type PrimitiveTypeName = primitiveTypeNames[number]; -export type TypeName = ObjectTypeName | PrimitiveTypeName;