From 3ec97fd176e1d2481d6a80c10857f698e4e9a0da Mon Sep 17 00:00:00 2001 From: Nicholas Berlette Date: Sat, 6 Aug 2022 19:23:31 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20initial=20commit!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 16 + .vscode/settings.json | 16 + LICENSE | 22 + README.md | 1138 ++++++++++++++++++++++++++++++++++ deno.jsonc | 44 ++ mod.ts | 1345 +++++++++++++++++++++++++++++++++++++++++ types.d.ts | 86 +++ version.ts | 47 ++ 8 files changed, 2714 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 deno.jsonc create mode 100644 mod.ts create mode 100644 types.d.ts create mode 100644 version.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f8f613 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.*.log +*.log +.env* +!.env.example +!.env.vault +!.env.defaults +.*.swp +*.save +node_modules +Thumbs.db +.DS_Store +.SpotlightV100 +.hidden +.private +*.pem +node diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2aa77cc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "deno.enable": true, + "deno.lint": true, + "deno.unstable": true, + "deno.suggest.autoImports": true, + "deno.suggest.paths": false, + "deno.suggest.imports.autoDiscover": true, + "deno.suggest.imports.hosts": { + "https://deno.land": true, + "https://x.nest.land": true, + "https://crux.land": true, + "https://esm.sh": true + }, + "deno.suggest.names": true, + "deno.suggest.completeFunctionCalls": true +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f54f564 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +# The MIT License (MIT) + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Nicholas Berlette (https://berlette.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb7a661 --- /dev/null +++ b/README.md @@ -0,0 +1,1138 @@ +

🦕 dis

+ +[![](https://migo.deno.dev/width=1280;height=640;pxRatio=1;bgColor=10141a;icon=deno;iconColor=papayawhip;titleColor=white;subtitleColor=white;iconY=80;iconX=950;titleFontSize=120;titleX=175;titleY=450;subtitleX=420;subtitleY=550;subtitleFontSize=48;titleTextAnchor=left;subtitleTextAnchor=left/dis/https://deno.land/x/dis.png)](https://deno.land/x/dis) + +Originally by [**Sindre Sorhus**](https://github.com/sindresorhus/is) for Node, ported by [**Nicholas Berlette**](https://github.com/nberlette) for Deno. + +--- + +## Usage + +```ts +import * as is from "https://deno.land/x/dis@0.0.0/mod.ts"; +``` + +### Type Checking + +```ts +is('🦕'); +// 'string' + +is(new Map()); +// 'Map' + +is.number(6); +// true +``` + +### Assertions + +[Assertions](#type-assertions) perform the same type checks, but throw an error if the type does not match. + +```ts +import { assert } from "https://deno.land/x/dis@0.0.0/mod.ts"; + +assert.string(2); +// => Error: Expected value which is `string`, received value of type `number`. +``` + +### Assertions with TypeScript + +```ts +import { assert } from "https://deno.land/x/dis@0.0.0/mod.ts"; + +assert.string(foo); +// `foo` is now typed as a `string`. +``` + + +## Highlights + +- Written in TypeScript, for Deno and Deno Deploy +- [Extensive use of type guards](#type-guards) +- [Supports type assertions](#type-assertions) +- [Aware of generic type parameters](#generic-type-parameters) (use with caution) +- Actively maintained + +--- + +# API + +## is(value) + +Attempts to ascertain the type of the value it receives. Accepts only one argument. + +```ts +is('🦕'); +// 'string' + +is(new Map()); +// 'Map' +``` + +> **Returns**: the type of `value`. + +## `is.{method}` + +All the below methods accept a value and returns a boolean for whether the value is of the desired type. + +```ts +is.number(6); +// true + +is.undefined(true); +// false +``` + +--- + +## Primitives + +> **Note**: Primitives are **lowercase** and object types are **camelCase**. + +
Examples of Primitives + +--- + +- `'undefined'` +- `'null'` +- `'string'` +- `'symbol'` +- `'Array'` +- `'Function'` +- `'Object'` + +> **Note**: It will throw an error if you try to feed it object-wrapped primitives, as that's a bad practice (e.g. `new String('foo')`) + +
+ +
API Methods + +--- + +### undefined + +```ts +is.undefined(value) +``` + +### null + +```ts +is.null(value) +``` + +> **Note**: TypeScript users must use `.null_()` because of a TypeScript naming limitation. + +### string + +```ts +is.string(value) +``` + +### number + +```ts +is.number(value) +``` + +> **Note:** `is.number(NaN)` returns `false`. This intentionally deviates from `typeof` behavior to increase user-friendliness of `is` type checks. + +### boolean + +```ts +is.boolean(value) +``` + +### symbol + +```ts +is.symbol(value) +``` + +### bigint + +```ts +is.bigint(value) +``` + +
+ +--- + +## Builtins + +
API Methods + +--- + +### array + +```ts +is.array(value, assertion?) +``` + +> **Returns**: true if `value` is an array and all of its items match the assertion (if provided). + +#### Examples + +```ts +is.array(value); // Validate `value` is an array. +is.array(value, is.number); // Validate `value` is an array and all of its items are numbers. +``` + +### function + +```ts +is.function(value) +``` + +> **Note**: TypeScript users must use `.function_()` because of a TypeScript naming limitation. + +### buffer + +```ts +is.buffer(value) +``` + +### blob + +```ts +is.blob(value) +``` + +### object + +```ts +is.object(value) +``` + +> **Important**: Keep in mind that [functions are objects too](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions). + +### numericString + +```ts +is.numericString(value) +``` + +> **Returns**: `true` for a string that represents a number satisfying `is.number`, for example, `'42'` and `'-8.3'`. + +> **Important**: `'NaN'` returns `false`, but `'Infinity'` and `'-Infinity'` return `true`. + +### regExp + +```ts +is.regExp(value) +``` + +### date + +```ts +is.date(value) +``` + +### error + +```ts +is.error(value) +``` + +### nativePromise + +```ts +is.nativePromise(value) +``` + +### promise + +```ts +is.promise(value) +``` + +> **Returns**: `true` for any object with a `.then()` and `.catch()` method. Prefer this one over `.nativePromise()` as you usually want to allow userland promise implementations too. + +### generator + +```ts +is.generator(value) +``` + +> **Returns**: `true` for any object that implements its own `.next()` and `.throw()` methods and has a function definition for `Symbol.iterator`. + +### generatorFunction + +```ts +is.generatorFunction(value) +``` + +### asyncFunction + +```ts +is.asyncFunction(value) +``` + +> **Returns**: `true` for any `async` function that can be called with the `await` operator. + +#### Examples + +```ts +is.asyncFunction(async () => {}); +// true + +is.asyncFunction(() => {}); +// false +``` + +### asyncGenerator + +```ts +is.asyncGenerator(value) +``` + +#### Examples + +```ts +is.asyncGenerator( + (async function * () { + yield 4; + })() +); +// true + +is.asyncGenerator( + (function * () { + yield 4; + })() +); +// false +``` + +### asyncGeneratorFunction + +```ts +is.asyncGeneratorFunction(value) +``` + +#### Examples + +```ts +is.asyncGeneratorFunction(async function * () { + yield 4; +}); +// true + +is.asyncGeneratorFunction(function * () { + yield 4; +}); +// false +``` + +### boundFunction + +```ts +is.boundFunction(value) +``` + +> **Returns**: `true` for any `bound` function. + +```ts +is.boundFunction(() => {}); +// true + +is.boundFunction(function () {}.bind(null)); +// true + +is.boundFunction(function () {}); +// false +``` + +### map + +```ts +is.map(value) +``` + +### set + +```ts +is.set(value) +``` + +### weakMap + +```ts +is.weakMap(value) +``` + +### weakSet + +```ts +is.weakSet(value) +``` + +### weakRef + +```ts +is.weakRef(value) +``` + +
+ +--- + +## TypedArrays + +
API Methods + +--- + +### int8Array + +```ts +is.int8Array(value) +``` + +### uint8Array + +```ts +is.uint8Array(value) +``` + +### uint8ClampedArray + +```ts +is.uint8ClampedArray(value) +``` + +### int16Array + +```ts +is.int16Array(value) +``` + +### uint16Array + +```ts +is.uint16Array(value) +``` + +### int32Array + +```ts +is.int32Array(value) +``` + +### uint32Array + +```ts +is.uint32Array(value) +``` + +### float32Array + +```ts +is.float32Array(value) +``` + +### float64Array + +```ts +is.float64Array(value) +``` + +### bigInt64Array + +```ts +is.bigInt64Array(value) +``` + +### bigUint64Array + +```ts +is.bigUint64Array(value) +``` + +
+ +--- + +## Structured Data + +
API Methods + +--- + +### arrayBuffer + +```ts +is.arrayBuffer(value) +``` + +### sharedArrayBuffer + +```ts +is.sharedArrayBuffer(value) +``` + +### dataView + +```ts +is.dataView(value) +``` + + +### enumCase + +```ts +is.enumCase(value, enum) +``` + +> **Note**: **TypeScript-only**. Returns `true` if `value` is a member of `enum`. + +```ts +enum Direction { + Ascending = 'ascending', + Descending = 'descending' +} +is.enumCase('ascending', Direction); +// true +is.enumCase('other', Direction); +// false +``` + +
+ +--- + +## Emptiness + +
API Methods + +--- + +### emptyString + +```ts +is.emptyString(value) +``` + + +> **Returns**: `true` if the value is a `string` and the `.length` is 0. + +### emptyStringOrWhitespace + +```ts +is.emptyStringOrWhitespace(value) +``` + + +> **Returns**: `true` if `is.emptyString(value)` or if it's a `string` that is all whitespace. + +### nonEmptyString + +```ts +is.nonEmptyString(value) +``` + + +> **Returns**: `true` if the value is a `string` and the `.length` is more than 0. + +### nonEmptyStringAndNotWhitespace + +```ts +is.nonEmptyStringAndNotWhitespace(value) +``` + +> **Returns**: `true` if the value is a `string` that is not empty and not whitespace. + +```ts +const values = ['property1', '', null, 'property2', ' ', undefined]; +values.filter(is.nonEmptyStringAndNotWhitespace); +// ['property1', 'property2'] +``` + +### emptyArray + +```ts +is.emptyArray(value) +``` + +> **Returns**: `true` if the value is an `Array` and the `.length` is 0. + +### nonEmptyArray + +```ts +is.nonEmptyArray(value) +``` + +> **Returns**: `true` if the value is an `Array` and the `.length` is more than 0. + +### emptyObject + +```ts +is.emptyObject(value) +``` + +> **Returns**: `true` if the value is an `Object` and `Object.keys(value).length` is 0. + +> **Note**: `Object.keys` returns only own enumerable properties. Hence something like this can happen: + +```ts +const object1 = {}; +Object.defineProperty(object1, 'property1', { + value: 42, + writable: true, + enumerable: false, + configurable: true +}); +is.emptyObject(object1); +// true +``` + +### nonEmptyObject + +```ts +is.nonEmptyObject(value) +``` + + +> **Returns**: `true` if the value is an `Object` and `Object.keys(value).length` is more than 0. + +### emptySet + +```ts +is.emptySet(value) +``` + + +> **Returns**: `true` if the value is a `Set` and the `.size` is 0. + +### nonEmptySet + +```ts +is.nonEmptySet(Value) +``` + + +> **Returns**: `true` if the value is a `Set` and the `.size` is more than 0. + +### emptyMap + +```ts +is.emptyMap(value) +``` + + +> **Returns**: `true` if the value is a `Map` and the `.size` is 0. + +### nonEmptyMap + +```ts +is.nonEmptyMap(value) +``` + + +> **Returns**: `true` if the value is a `Map` and the `.size` is more than 0. + +
+ +--- + +## Everything Else + +
API Methods + +--- + +### directInstanceOf + +```ts +is.directInstanceOf(value, class) +``` + + +> **Returns**: `true` if `value` is a direct instance of `class`. + +```ts +is.directInstanceOf(new Error(), Error); +// true +class UnicornError extends Error {} +is.directInstanceOf(new UnicornError(), Error); +// false +``` + +### urlInstance + +```ts +is.urlInstance(value) +``` + + +> **Returns**: `true` if `value` is an instance of the [`URL` class](https://mdn.io/URL). + +```ts +const url = new URL('https://example.com'); +is.urlInstance(url); +// true +``` + +### urlString + +```ts +is.urlString(value) +``` + +> **Returns**: `true` if `value` is a URL string. + +Note: this only does basic checking using the [`URL` class](https://mdn.io/URL) constructor. + +```ts +const url = 'https://example.com'; +is.urlString(url); +// true +is.urlString(new URL(url)); +// false +``` + +### truthy + +```ts +is.truthy(value) +``` + + +> **Returns**: `true` for all values that evaluate to true in a boolean context: + +```ts +is.truthy('🦕'); +// true +is.truthy(undefined); +// false +``` + +### falsy + +```ts +is.falsy(value) +``` + + +> **Returns**: `true` if `value` is one of: `false`, `0`, `''`, `null`, `undefined`, `NaN`. + +### NaN + +```ts +is.nan(value) +``` + +### nullOrUndefined + +```ts +is.nullOrUndefined(value) +``` + +### primitive + +```ts +is.primitive(value) +``` + + +JavaScript primitives are as follows: `null`, `undefined`, `string`, `number`, `boolean`, `symbol`. + +### integer + +```ts +is.integer(value) +``` + + +### safeInteger + +```ts +is.safeInteger(value) +``` + + +> **Returns**: `true` if `value` is a [safe integer](https://mdn.io/isSafeInteger). + +### plainObject + +```ts +is.plainObject(value) +``` + + +An object is plain if it's created by either `{}`, `new Object()`, or `Object.create(null)`. + +### iterable + +```ts +is.iterable(value) +``` + +### asyncIterable + +```ts +is.asyncIterable(value) +``` + +### class + +```ts +is.class(value) +``` + +> **Returns**: `true` for instances created by a class. + +**Note:** TypeScript users must use `.class_()` because of a TypeScript naming limitation. + +### typedArray + +```ts +is.typedArray(value) +``` + +### arrayLike + +```ts +is.arrayLike(value) +``` + +A `value` is array-like if it is not a function and has a `value.length` that is a safe integer greater than or equal to 0. + +```ts +is.arrayLike(document.forms); +// true + +function foo() { + is.arrayLike(arguments); + // true +} +foo(); +``` + +### inRange + +```ts +is.inRange(value, range) +``` + +Check if `value` (number) is in the given `range`. The range is an array of two values, lower bound and upper bound, in no specific order. + +```ts +is.inRange(3, [0, 5]); +is.inRange(3, [5, 0]); +is.inRange(0, [-2, 2]); +``` + +### inRange + +```ts +is.inRange(value, upperBound) +``` + + +Check if `value` (number) is in the range of `0` to `upperBound`. + +```ts +is.inRange(3, 10); +``` + +### domElement + +```ts +is.domElement(value) +``` + + +> **Returns**: `true` if `value` is a DOM Element. + +### nodeStream + +```ts +is.nodeStream(value) +``` + +> **Returns**: `true` if `value` is a Node.js [stream](https://nodejs.org/api/stream.html). + +```ts +import fs from 'node:fs'; +is.nodeStream(fs.createReadStream('unicorn.png')); +// true +``` + +### observable + +```ts +is.observable(value) +``` + + +> **Returns**: `true` if `value` is an `Observable`. + +```ts +import {Observable} from 'rxjs'; +is.observable(new Observable()); +// true +``` + +### infinite + +```ts +is.infinite(value) +``` + + +Check if `value` is `Infinity` or `-Infinity`. + +### evenInteger + +```ts +is.evenInteger(value) +``` + + +> **Returns**: `true` if `value` is an even integer. + +### oddInteger + +```ts +is.oddInteger(value) +``` + + +> **Returns**: `true` if `value` is an odd integer. + +### propertyKey + +```ts +is.propertyKey(value) +``` + + +> **Returns**: `true` if `value` can be used as an object property key (either `string`, `number`, or `symbol`). + +### formData + +```ts +is.formData(value) +``` + + +> **Returns**: `true` if `value` is an instance of the [`FormData` class](https://developer.mozilla.org/en-US/docs/Web/API/FormData). + +```ts +const data = new FormData(); +is.formData(data); +// true +``` + +### urlSearchParams + +```ts +is.urlSearchParams(value) +``` + +> **Returns**: `true` if `value` is an instance of the [`URLSearchParams` class](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams). + +```ts +const searchParams = new URLSearchParams(); +is.urlSearchParams(searchParams); +// true +``` + +### any + +```ts +is.any(predicate | predicate[], ...values) +``` + + +Using a single `predicate` argument, returns `true` if **any** of the input `values` returns true in the `predicate`: + +```ts +is.any(is.string, {}, true, '🦕'); +// true +is.any(is.boolean, 'denosaurs', [], new Map()); +// false +``` + +Using an array of `predicate[]`, returns `true` if **any** of the input `values` returns true for **any** of the `predicates` provided in an array: + +```ts +is.any([is.string, is.number], {}, true, '🦕'); +// true +is.any([is.boolean, is.number], 'denosaurs', [], new Map()); +// false +``` + +### all + +```ts +is.all(predicate, ...values) +``` + + +> **Returns**: `true` if **all** of the input `values` returns true in the `predicate`: + +```ts +is.all(is.object, {}, new Map(), new Set()); +// true +is.all(is.string, '🦕', [], 'denosaurs'); +// false +``` + +
+ +--- + +## Type Guards + +When using `is` together with TypeScript, [type guards](http://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types) are being used extensively to infer the correct type inside if-else statements. + +
Examples + +--- + +```ts +import is from "https://deno.land/x/dis@0.0.0/mod.ts"; +const padLeft = (value: string, padding: string | number) => { + + if (is.number(padding)) { + // `padding` is typed as `number` + return Array(padding + 1).join(' ') + value; + } + + if (is.string(padding)) { + // `padding` is typed as `string` + return padding + value; + } + + throw new TypeError(`Expected 'padding' to be of type 'string' or 'number', got '${is(padding)}'.`); +} +padLeft('🦕', 3); +// ' 🦕' + +padLeft('🦕', '🌈'); +// '🌈🦕' +``` + +
+ +--- + +## Type Assertions + +The type guards are also available as [type assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions), which throw an error for unexpected types. It is a convenient one-line version of the often repetitive `"if-not-expected-type-throw"` pattern. + +
Examples + +--- + +```ts +import { assert } from "https://deno.land/x/dis@0.0.0/mod.ts"; + +const handleMovieRatingApiResponse = (response: unknown) => { + assert.plainObject(response); + // `response` is now typed as a plain `object` with `unknown` properties. + + assert.number(response.rating); + // `response.rating` is now typed as a `number`. + + assert.string(response.title); + // `response.title` is now typed as a `string`. + + return `${response.title} (${response.rating * 10})`; +}; +handleMovieRatingApiResponse({rating: 0.87, title: 'The Matrix'}); +// 'The Matrix (8.7)' + +// This throws an error. +handleMovieRatingApiResponse({rating: '🦕'}); +``` + +
+ +--- + +## Generic type parameters + +
More Information + +--- + +--- + +The type guards and type assertions are aware of [generic type parameters](https://www.typescriptlang.org/docs/handbook/generics.html), such as `Promise` and `Map`. The default is `unknown` for most cases, since `is` cannot check them at runtime. If the generic type is known at compile-time, either implicitly (inferred) or explicitly (provided), `is` propagates the type so it can be used later. + +Use generic type parameters with caution. They are only checked by the TypeScript compiler, and not checked by `is` at runtime. This can lead to unexpected behavior, where the generic type is _assumed_ at compile-time, but actually is something completely different at runtime. It is best to use `unknown` (default) and type-check the value of the generic type parameter at runtime with `is` or `assert`. + +
+
Examples + +--- + +```ts +import { assert } from "https://deno.land/x/dis@0.0.0/mod.ts"; +async function badNumberAssumption(input: unknown) { + // Bad assumption about the generic type parameter fools the compile-time type system. + assert.promise(input); + // `input` is a `Promise` but only assumed to be `Promise`. + const resolved = await input; + // `resolved` is typed as `number` but was not actually checked at runtime. + // Multiplication will return NaN if the input promise did not actually contain a number. + return 2 * resolved; +} +async function goodNumberAssertion(input: unknown) { + assert.promise(input); + // `input` is typed as `Promise` + const resolved = await input; + // `resolved` is typed as `unknown` + assert.number(resolved); + // `resolved` is typed as `number` + // Uses runtime checks so only numbers will reach the multiplication. + return 2 * resolved; +} +badNumberAssumption(Promise.resolve('An unexpected string')); +// NaN +// This correctly throws an error because of the unexpected string value. +goodNumberAssertion(Promise.resolve('An unexpected string')); +``` + +
+ +--- + +## Frequently Asked Questions + +
+Why yet another type checking module? + +--- + +There are hundreds of type checking modules on npm, unfortunately, I couldn't find any that fit my needs: + +- Includes both type methods and ability to get the type +- Types of primitives returned as lowercase and object types as camelcase +- Covers all built-ins +- Unsurprising behavior +- Well-maintained +- Comprehensive test suite + +For the ones I found, pick 3 of these. + +The most common mistakes I noticed in these modules was using `instanceof` for type checking, forgetting that functions are objects, and omitting `symbol` as a primitive. +
+ +
Why not just use `instanceof` instead of this package? + +--- + +`instanceof` does not work correctly for all types and it does not work across [realms](https://stackoverflow.com/a/49832343/64949). Examples of realms are iframes, windows, web workers, and the `vm` module in Node.js. + +
+ +--- + +**MIT License**. Originally by [**Sindre Sorhus**](https://github.com/sindresorhus/is). Ported by [**Nicholas Berlette**](https://github.com/nberlette) for Deno. diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..2ff6502 --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,44 @@ +{ + "compilerOptions": { + "lib": [ + "deno.ns", + "deno.window", + "dom", + "dom.iterable", + "esnext" + ], + "types": [ + "./types.d.ts" + ], + "strict": true, + "strictNullChecks": true, + "keyofStringsOnly": true + }, + "fmt": { + "files": { + "exclude": [ + ".vscode", + ".git*", + ".node/*" + ] + } + }, + "lint": { + "files": { + "exclude": [ + ".vscode", + ".git*", + ".node" + ] + }, + "rules": { + "exclude": [ + "no-explicit-any", + "no-bad-types" + ] + } + }, + "tasks": { + "build:docs": "deno doc --json ./mod.ts > docs.json" + } +} diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..b087c10 --- /dev/null +++ b/mod.ts @@ -0,0 +1,1345 @@ +/** + * The following file was extracted from @sindresorhus/is, and ported to Deno + * by Nicholas Berlette. + * + * @copyright Sindre Sorhus + * @copyright Nicholas Berlette + * @see {@link https://github.com/sindresorhus/is} + * @see {@link https://github.com/deno911/x} + */ + +// deno-lint-ignore-file ban-types +import type { Buffer } from "https://deno.land/std@0.151.0/io/buffer.ts"; + +export declare namespace NodeJS { + export type Buffer = + import("https://deno.land/std@0.151.0/node/buffer.ts").Buffer; + export type EventEmitter = + import("https://deno.land/std@0.151.0/node/events.ts").EventEmitter; + export type Readable = + import("https://deno.land/std@0.151.0/node/stream.ts").Readable; + export type Writable = + import("https://deno.land/std@0.151.0/node/stream.ts").Writable; + export type Stream = + import("https://deno.land/std@0.151.0/node/stream.ts").Stream; + export type ReadableStream = Readable & { + readonly readable: true; + }; + export type WritableStream = Writable & { + readonly writable: true; + }; + export interface Streams extends EventEmitter { + pipe( + destination: T, + options?: { end?: boolean }, + ): T; + } +} + +/** + * Matches any primitive value. + * @see https://mdn.io/Primitive + */ +export type Primitive = + | null + | undefined + | string + | number + | boolean + | symbol + | bigint; + +/** + * Matches a `class` constructor. + * @see https://mdn.io/Classes. + */ +export type Class = new ( + ...arguments_: Arguments +) => T; + +/** + * 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; + +declare global { + interface SymbolConstructor { + readonly observable: 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 Falsey = false | 0 | 0n | "" | null | undefined; + +export interface WeakRef { + readonly [Symbol.toStringTag]: "WeakRef"; + deref(): T | undefined; +} + +/** + * Type assertions have to be declared with an explicit type. + */ +export interface Assert { + // Unknowns. + undefined: (value: unknown) => asserts value is undefined; + string: (value: unknown) => asserts value is string; + number: (value: unknown) => asserts value is number; + bigint: (value: unknown) => asserts value is bigint; + function_: (value: unknown) => asserts value is Function; + null_: (value: unknown) => asserts value is null; + class_: (value: unknown) => asserts value is Class; + 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[]; + buffer: (value: unknown) => asserts value is Buffer; + blob: (value: unknown) => asserts value is Blob; + nullOrUndefined: (value: unknown) => asserts value is null | undefined; + object: ( + value: unknown, + ) => asserts value is Record; + iterable: (value: unknown) => asserts value is Iterable; + asyncIterable: ( + value: unknown, + ) => asserts value is AsyncIterable; + generator: (value: unknown) => asserts value is Generator; + asyncGenerator: (value: unknown) => asserts value is AsyncGenerator; + nativePromise: (value: unknown) => asserts value is Promise; + promise: (value: unknown) => asserts value is Promise; + generatorFunction: (value: unknown) => asserts value is GeneratorFunction; + asyncGeneratorFunction: ( + value: unknown, + ) => asserts value is AsyncGeneratorFunction; + asyncFunction: (value: unknown) => asserts value is Function; + boundFunction: (value: unknown) => asserts value is Function; + regExp: (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; + set: (value: unknown) => asserts value is Set; + weakMap: ( + value: unknown, + ) => asserts value is WeakMap; + weakSet: ( + value: unknown, + ) => asserts value is WeakSet; + weakRef: ( + value: unknown, + ) => asserts value is WeakRef; + int8Array: (value: unknown) => asserts value is Int8Array; + uint8Array: (value: unknown) => asserts value is Uint8Array; + uint8ClampedArray: (value: unknown) => asserts value is Uint8ClampedArray; + int16Array: (value: unknown) => asserts value is Int16Array; + uint16Array: (value: unknown) => asserts value is Uint16Array; + int32Array: (value: unknown) => asserts value is Int32Array; + uint32Array: (value: unknown) => asserts value is Uint32Array; + float32Array: (value: unknown) => asserts value is Float32Array; + 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; + enumCase: ( + value: unknown, + targetEnum: T, + ) => asserts value is T[keyof T]; + urlInstance: (value: unknown) => asserts value is URL; + urlString: (value: unknown) => asserts value is string; + truthy: (value: unknown) => asserts value is unknown; + falsy: (value: unknown) => asserts value is unknown; + nan: (value: unknown) => asserts value is unknown; + primitive: (value: unknown) => asserts value is Primitive; + integer: (value: unknown) => asserts value is number; + safeInteger: (value: unknown) => asserts value is number; + plainObject: ( + value: unknown, + ) => asserts value is Record; + typedArray: (value: unknown) => asserts value is TypedArray; + arrayLike: (value: unknown) => asserts value is ArrayLike; + domElement: (value: unknown) => asserts value is HTMLElement; + observable: (value: unknown) => asserts value is ObservableLike; + nodeStream: (value: unknown) => asserts value is NodeJS.Stream; + infinite: (value: unknown) => asserts value is number; + emptyArray: (value: unknown) => asserts value is never[]; + nonEmptyArray: (value: unknown) => asserts value is [unknown, ...unknown[]]; + emptyString: (value: unknown) => asserts value is ""; + emptyStringOrWhitespace: (value: unknown) => asserts value is string; + nonEmptyString: (value: unknown) => asserts value is string; + nonEmptyStringAndNotWhitespace: (value: unknown) => asserts value is string; + emptyObject: ( + value: unknown, + ) => asserts value is Record; + nonEmptyObject: ( + value: unknown, + ) => asserts value is Record; + emptySet: (value: unknown) => asserts value is Set; + nonEmptySet: (value: unknown) => asserts value is Set; + emptyMap: (value: unknown) => asserts value is Map; + nonEmptyMap: ( + value: unknown, + ) => asserts value is Map; + propertyKey: (value: unknown) => asserts value is PropertyKey; + formData: (value: unknown) => asserts value is FormData; + urlSearchParams: (value: unknown) => asserts value is URLSearchParams; + + // Numbers. + evenInteger: (value: number) => asserts value is number; + oddInteger: (value: number) => asserts value is number; + + // Two arguments. + directInstanceOf: ( + instance: unknown, + class_: Class, + ) => asserts instance is T; + inRange: (value: number, range: number | number[]) => asserts value is number; + + // Variadic functions. + any: ( + predicate: Predicate | Predicate[], + ...values: unknown[] + ) => void | never; + all: (predicate: Predicate, ...values: unknown[]) => void | never; +} + +/** + * TypedArrays + */ +const typedArrayTypeNames = [ + "Int8Array", + "Uint8Array", + "Uint8ClampedArray", + "Int16Array", + "Uint16Array", + "Int32Array", + "Uint32Array", + "Float32Array", + "Float64Array", + "BigInt64Array", + "BigUint64Array", +] as const; + +type TypedArrayTypeName = typeof typedArrayTypeNames[number]; + +function isTypedArrayName(name: unknown): name is TypedArrayTypeName { + return typedArrayTypeNames.includes(name as TypedArrayTypeName); +} + +/** + * Objects + */ + +const objectTypeNames = [ + "Function", + "Generator", + "AsyncGenerator", + "GeneratorFunction", + "AsyncGeneratorFunction", + "AsyncFunction", + "Observable", + "Array", + "Buffer", + "Blob", + "Object", + "RegExp", + "Date", + "Error", + "Map", + "Set", + "WeakMap", + "WeakSet", + "WeakRef", + "ArrayBuffer", + "SharedArrayBuffer", + "DataView", + "Promise", + "URL", + "FormData", + "URLSearchParams", + "HTMLElement", + "NaN", + ...typedArrayTypeNames, +] as const; + +type ObjectTypeName = typeof objectTypeNames[number]; + +function isObjectTypeName(name: unknown): name is ObjectTypeName { + return objectTypeNames.includes(name as ObjectTypeName); +} + +/** + * Primitives + */ + +const primitiveTypeNames = [ + "null", + "undefined", + "string", + "number", + "bigint", + "boolean", + "symbol", + "object", + "function", +] as const; + +type PrimitiveTypeName = typeof primitiveTypeNames[number]; + +function isPrimitiveTypeName(name: unknown): name is PrimitiveTypeName { + return primitiveTypeNames.includes(name as PrimitiveTypeName); +} + +export type TypeName = ObjectTypeName | PrimitiveTypeName; + +function isOfType( + type: PrimitiveTypeName | "function", +) { + // deno-lint-ignore valid-typeof + return (value: unknown): value is T => typeof value === type; +} + +const { toString } = Object.prototype; +const getObjectType = (value: unknown): ObjectTypeName | undefined => { + const objectTypeName = toString.call(value).slice(8, -1); + + if (/HTML\w+Element/.test(objectTypeName) && is.domElement(value)) { + return "HTMLElement"; + } + + if (isObjectTypeName(objectTypeName)) { + return objectTypeName; + } + + return undefined; +}; + +const isObjectOfType = (type: ObjectTypeName) => + (value: unknown): value is T => getObjectType(value) === type; + +function is(value: unknown): TypeName { + if (value === null) { + return "null"; + } + + 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: + } + + if (is.observable(value)) { + return "Observable"; + } + + if (is.array(value)) { + return "Array"; + } + + if (is.buffer(value)) { + return "Buffer"; + } + + const tagType = getObjectType(value); + if (tagType) { + return tagType; + } + + if ( + value instanceof String || value instanceof Boolean || + value instanceof Number + ) { + throw new TypeError("Please don't use object wrappers for primitive types"); + } + + return "Object"; +} + +is.undefined = isOfType("undefined"); + +is.string = isOfType("string"); + +const isNumberType = isOfType("number"); +is.number = (value: unknown): value is number => + isNumberType(value) && !is.nan(value); + +is.bigint = isOfType("bigint"); + +is.function_ = isOfType("function"); + +is.null_ = (value: unknown): value is null => value === null; + +is.class_ = (value: unknown): value is Class => + is.function_(value) && value.toString().startsWith("class "); + +is.boolean = (value: unknown): value is boolean => + value === true || value === false; + +is.symbol = isOfType("symbol"); + +is.numericString = (value: unknown): value is string => + is.string(value) && !is.emptyStringOrWhitespace(value) && + !Number.isNaN(Number(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((element) => assertion(element)); +}; + +is.buffer = (value: unknown): value is Buffer => + (value as any)?.constructor?.isBuffer?.(value) ?? false; + +is.blob = (value: unknown): value is Blob => + isObjectOfType("Blob")(value); + +is.nullOrUndefined = (value: unknown): value is null | undefined => + is.null_(value) || is.undefined(value); + +is.object = (value: unknown): value is object => + !is.null_(value) && (typeof value === "object" || is.function_(value)); + +is.iterable = (value: unknown): value is Iterable => + is.function_((value as Iterable)?.[Symbol.iterator]); + +is.asyncIterable = (value: unknown): value is AsyncIterable => + is.function_((value as AsyncIterable)?.[Symbol.asyncIterator]); + +is.generator = (value: unknown): value is Generator => + is.iterable(value) && is.function_((value as Generator)?.next) && + is.function_((value as Generator)?.throw); + +is.asyncGenerator = (value: unknown): value is AsyncGenerator => + is.asyncIterable(value) && is.function_((value as AsyncGenerator).next) && + is.function_((value as AsyncGenerator).throw); + +is.nativePromise = (value: unknown): value is Promise => + isObjectOfType>("Promise")(value); + +const hasPromiseApi = (value: unknown): value is Promise => + is.function_((value as Promise)?.then) && + is.function_((value as Promise)?.catch); + +is.promise = (value: unknown): value is Promise => + is.nativePromise(value) || hasPromiseApi(value); + +is.generatorFunction = isObjectOfType("GeneratorFunction"); + +is.asyncGeneratorFunction = ( + value: unknown, +): value is (...args: any[]) => Promise => + getObjectType(value) === "AsyncGeneratorFunction"; + +is.asyncFunction = ( + value: unknown, +): value is (...args: any[]) => Promise => + getObjectType(value) === "AsyncFunction"; + +is.boundFunction = (value: unknown): value is Function => + // deno-lint-ignore no-prototype-builtins + is.function_(value) && !value.hasOwnProperty("prototype"); + +is.regExp = isObjectOfType("RegExp"); + +is.date = isObjectOfType("Date"); + +is.error = isObjectOfType("Error"); + +is.map = ( + value: unknown, +): value is Map => isObjectOfType>("Map")(value); + +is.set = (value: unknown): value is Set => + isObjectOfType>("Set")(value); + +is.weakMap = ( + value: unknown, +): value is WeakMap => + isObjectOfType>("WeakMap")(value); + +is.weakSet = (value: unknown): value is WeakSet => + isObjectOfType>("WeakSet")(value); + +is.weakRef = (value: unknown): value is WeakRef => + isObjectOfType>("WeakRef")(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"); + +/** + * 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"); + +/** + * 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"); + +/** + * 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) => + Object.values(targetEnum).includes(value as string); + +/** + * 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 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); + +/** + * 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; + } +}; + +/** + * Check if a value is truthy, meaning it cannot be falsey (`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 | Falsey): value is T => Boolean(value); + +/** + * Check if a value is falsey: `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.falsey = (value: T | Falsey): value is Falsey => !value; + +/** + * Alias for `is.falsey`. + * @see {@link is.falsey} + */ +is.falsy = is.falsey; + +/** + * Equivalent to the JavaScript builtin `Number.isNaN`. + * + * @param value The value to inspect. + * @returns `boolean` + */ +is.NaN = (value: unknown) => Number.isNaN(value as number); + +/** + * Alias for `is.NaN`. + * @see {@link is.NaN} + */ +is.nan = is.NaN; + +/** + * 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); + +/** + * 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); + +/** + * 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); + +/** + * 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 + */ + if (typeof value !== "object" || value === null) { + return false; + } + + const prototype = Object.getPrototypeOf(value); + + return (prototype === null || prototype === Object.prototype || + Object.getPrototypeOf(prototype) === null) && + !(Symbol.toStringTag in value) && !(Symbol.iterator in 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)); + +/** + * 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; + +/** + * 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); + +/** + * 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); + } + + if (is.array(range) && range.length === 2) { + return value >= Math.min(...range) && value <= Math.max(...range); + } + + throw new TypeError(`Invalid range: ${JSON.stringify(range)}`); +}; + +const NODE_TYPE_ELEMENT = 1; + +const DOM_PROPERTIES_TO_CHECK: Array<(keyof HTMLElement)> = [ + "innerHTML", + "ownerDocument", + "style", + "attributes", + "nodeValue", +]; + +/** + * 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) && + (value as HTMLElement).nodeType === NODE_TYPE_ELEMENT && + is.string((value as HTMLElement).nodeName) && + !is.plainObject(value) && + DOM_PROPERTIES_TO_CHECK.every((property) => property in 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`. + */ +is.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; +}; + +is.nodeStream = (value: unknown): value is NodeJS.Stream => + is.object(value) && is.function_((value as NodeJS.Stream).pipe) && + !is.observable(value); + +is.infinite = (value: unknown): value is number => + value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY; + +const isAbsoluteMod2 = (remainder: number) => + (value: number): value is number => + is.integer(value) && Math.abs(value % 2) === remainder; +is.evenInteger = isAbsoluteMod2(0); +is.oddInteger = isAbsoluteMod2(1); + +is.emptyArray = (value: unknown): value is never[] => + is.array(value) && value.length === 0; + +is.nonEmptyArray = (value: unknown): value is [unknown, ...unknown[]] => + is.array(value) && value.length > 0; + +is.emptyString = (value: unknown): value is "" => + is.string(value) && value.length === 0; + +const isWhiteSpaceString = (value: unknown): value is string => + is.string(value) && !/\S/.test(value); +is.emptyStringOrWhitespace = (value: unknown): value is string => + is.emptyString(value) || isWhiteSpaceString(value); + +// 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); + +// eslint-disable-next-line unicorn/no-array-callback-reference +is.emptyObject = ( + value: unknown, +): value is Record => + is.object(value) && !is.map(value) && !is.set(value) && + Object.keys(value).length === 0; + +/** + * 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; + +is.emptySet = (value: unknown): value is Set => + is.set(value) && value.size === 0; + +is.nonEmptySet = (value: unknown): value is Set => + is.set(value) && value.size > 0; + +is.emptyMap = (value: unknown): value is Map => + is.map(value) && value.size === 0; + +is.nonEmptyMap = ( + value: unknown, +): value is Map => is.map(value) && value.size > 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); + +is.formData = (value: unknown): value is FormData => + isObjectOfType("FormData")(value); + +is.urlSearchParams = (value: unknown): value is URLSearchParams => + isObjectOfType("URLSearchParams")(value); + +export type Predicate = (value: unknown) => boolean; + +type ArrayMethod = ( + fn: (value: unknown, index: number, array: unknown[]) => boolean, + thisArg?: unknown, +) => boolean; + +const predicateOnArray = ( + method: ArrayMethod, + predicate: Predicate, + values: unknown[], +) => { + if (!is.function_(predicate)) { + throw new TypeError(`Invalid predicate: ${JSON.stringify(predicate)}`); + } + + if (values.length === 0) { + throw new TypeError("Invalid number of values"); + } + + return method.call(values, predicate); +}; + +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.all = (predicate: Predicate, ...values: unknown[]): boolean => + predicateOnArray(Array.prototype.every, predicate, values); + +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}.`, + ); + } +}; + +export enum AssertionTypeDescription { + class_ = "Class", + numericString = "string with a number", + nullOrUndefined = "null or undefined", + iterable = "Iterable", + asyncIterable = "AsyncIterable", + nativePromise = "native Promise", + urlString = "string with a URL", + truthy = "truthy", + falsy = "falsy", + nan = "NaN", + primitive = "primitive", + integer = "integer", + safeInteger = "integer", + plainObject = "plain object", + arrayLike = "array-like", + typedArray = "TypedArray", + domElement = "HTMLElement", + nodeStream = "Node.js Stream", + infinite = "infinite number", + emptyArray = "empty array", + nonEmptyArray = "non-empty array", + emptyString = "empty string", + emptyStringOrWhitespace = "empty string or whitespace", + 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", + + evenInteger = "even integer", + oddInteger = "odd integer", + + directInstanceOf = "T", + inRange = "in range", + + any = "predicate returns truthy for any value", + all = "predicate returns truthy for all values", +} + +export const assert: Assert = { + // Unknowns. + undefined: (value: unknown): asserts value is undefined => + assertType(is.undefined(value), "undefined", 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), + null_: (value: unknown): asserts value is null => + assertType(is.null_(value), "null", value), + class_: (value: unknown): asserts value is Class => + assertType(is.class_(value), AssertionTypeDescription.class_, 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), + numericString: (value: unknown): asserts value is string => + assertType( + is.numericString(value), + AssertionTypeDescription.numericString, + 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; + assert(is.array(value), "Array", value); + + if (assertion) { + value.forEach(assertion); + } + }, + 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), + nullOrUndefined: (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), + 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, + ), + + 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), + regExp: (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), + 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), + 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), + enumCase: ( + value: unknown, + targetEnum: T, + ): asserts value is T[keyof T] => + assertType(is.enumCase(value, targetEnum), "EnumCase", 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), + 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), + nan: (value: unknown): asserts value is unknown => + assertType(is.nan(value), AssertionTypeDescription.nan, value), + primitive: (value: unknown): asserts value is Primitive => + assertType(is.primitive(value), AssertionTypeDescription.primitive, 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, + ), + plainObject: ( + value: unknown, + ): asserts value is Record => + assertType( + is.plainObject(value), + AssertionTypeDescription.plainObject, + value, + ), + typedArray: (value: unknown): asserts value is TypedArray => + assertType( + is.typedArray(value), + AssertionTypeDescription.typedArray, + value, + ), + arrayLike: (value: unknown): asserts value is ArrayLike => + assertType(is.arrayLike(value), AssertionTypeDescription.arrayLike, 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, + ), + infinite: (value: unknown): asserts value is number => + assertType(is.infinite(value), AssertionTypeDescription.infinite, value), + emptyArray: (value: unknown): asserts value is never[] => + assertType( + is.emptyArray(value), + AssertionTypeDescription.emptyArray, + value, + ), + nonEmptyArray: (value: unknown): asserts value is [unknown, ...unknown[]] => + assertType( + is.nonEmptyArray(value), + AssertionTypeDescription.nonEmptyArray, + value, + ), + emptyString: (value: unknown): asserts value is "" => + assertType( + is.emptyString(value), + AssertionTypeDescription.emptyString, + value, + ), + emptyStringOrWhitespace: (value: unknown): asserts value is string => + assertType( + is.emptyStringOrWhitespace(value), + AssertionTypeDescription.emptyStringOrWhitespace, + 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, + ), + emptyObject: ( + value: unknown, + ): asserts value is Record => + assertType( + is.emptyObject(value), + AssertionTypeDescription.emptyObject, + value, + ), + nonEmptyObject: ( + value: unknown, + ): asserts value is Record => + assertType( + is.nonEmptyObject(value), + AssertionTypeDescription.nonEmptyObject, + value, + ), + emptySet: (value: unknown): asserts value is Set => + assertType(is.emptySet(value), AssertionTypeDescription.emptySet, value), + nonEmptySet: (value: unknown): asserts value is Set => + assertType( + is.nonEmptySet(value), + AssertionTypeDescription.nonEmptySet, + value, + ), + emptyMap: (value: unknown): asserts value is Map => + assertType(is.emptyMap(value), AssertionTypeDescription.emptyMap, value), + nonEmptyMap: ( + value: unknown, + ): asserts value is Map => + assertType( + is.nonEmptyMap(value), + AssertionTypeDescription.nonEmptyMap, + value, + ), + propertyKey: (value: unknown): asserts value is number => + assertType(is.propertyKey(value), "PropertyKey", value), + formData: (value: unknown): asserts value is FormData => + assertType(is.formData(value), "FormData", value), + urlSearchParams: (value: unknown): asserts value is URLSearchParams => + assertType(is.urlSearchParams(value), "URLSearchParams", value), + + // Numbers. + 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, + ), + + // Two arguments. + directInstanceOf: ( + instance: unknown, + class_: Class, + ): asserts instance is T => + assertType( + is.directInstanceOf(instance, class_), + AssertionTypeDescription.directInstanceOf, + instance, + ), + inRange: (value: number, range: number | number[]): asserts value is number => + assertType( + is.inRange(value, range), + AssertionTypeDescription.inRange, + value, + ), + + // Variadic functions. + any: ( + predicate: Predicate | Predicate[], + ...values: unknown[] + ): void | never => + assertType( + is.any(predicate, ...values), + AssertionTypeDescription.any, + values, + { multipleValues: true }, + ), + all: (predicate: Predicate, ...values: unknown[]): void | never => + assertType( + is.all(predicate, ...values), + AssertionTypeDescription.all, + values, + { multipleValues: true }, + ), +}; + +/** + * Some few keywords are reserved, but we'll populate them for Node.js users. + * @see https://github.com/Microsoft/TypeScript/issues/2536 + */ +Object.defineProperties(is, { + "class": { + value: is.class_, + }, + "function": { + value: is.function_, + }, + "null": { + value: is.null_, + }, +}); +Object.defineProperties(assert, { + "class": { + value: assert.class_, + }, + "function": { + value: assert.function_, + }, + "null": { + value: assert.null_, + }, +}); + +export default is; diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..f6b724e --- /dev/null +++ b/types.d.ts @@ -0,0 +1,86 @@ +import type { Buffer } from "https://deno.land/std@0.151.0/io/buffer.ts"; + +export declare namespace NodeJS { + export type Buffer = + import("https://deno.land/std@0.151.0/node/buffer.ts").Buffer; + export type EventEmitter = + import("https://deno.land/std@0.151.0/node/events.ts").EventEmitter; + export type Readable = + import("https://deno.land/std@0.151.0/node/stream.ts").Readable; + export type Writable = + import("https://deno.land/std@0.151.0/node/stream.ts").Writable; + export type Stream = + import("https://deno.land/std@0.151.0/node/stream.ts").Stream; + export type ReadableStream = Readable & { + readonly readable: true; + }; + export type WritableStream = Writable & { + readonly writable: true; + }; + export interface Streams extends EventEmitter { + pipe( + destination: T, + options?: { end?: boolean }, + ): T; + } +} + +/** + * Matches any primitive value. + * @see https://mdn.io/Primitive + */ +export type Primitive = + | null + | undefined + | string + | number + | boolean + | symbol + | bigint; + +/** + * Matches a `class` constructor. + * @see https://mdn.io/Classes. + */ +export type Class = new ( + ...arguments_: Arguments +) => T; + +/** + * 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; + +declare global { + interface SymbolConstructor { + readonly observable: 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 Falsey = false | 0 | 0n | "" | null | undefined; + +export interface WeakRef { + readonly [Symbol.toStringTag]: "WeakRef"; + deref(): T | undefined; +} diff --git a/version.ts b/version.ts new file mode 100644 index 0000000..ba1b261 --- /dev/null +++ b/version.ts @@ -0,0 +1,47 @@ +import * as ansi from "https://deno.land/std@0.151.0/fmt/colors.ts"; + +/** `VERSION` managed by https://deno.land/x/publish */ +export const VERSION = "0.0.1" +export const MODULE = "dis"; + +/** `prepublish` will be invoked before publish */ +export async function prepublish(version: string) { + for (const filename of [ + "README.md", + "mod.ts", + ]) { + await bump(filename, version) + } + + return false; +} + +/** `prepublish` will be invoked after publish */ +export function postpublish(version: string) { + console.log( + ansi.bgGreen(" SUCCESS "), + ` ✓ published ${ansi.bold(ansi.underline(MODULE + "@" + version))}`, + ); +} + +async function bump(filename: string, version: string) { + try { + const module_regex = new RegExp( + `(?<=[/"'\s](${MODULE})[@]([{]{1,2}VERSION[}]{1,2}|\$VERSION|[^/"'\s]+)(?=[/"'\s])`, + "ig", + ); + const content = await Deno.readTextFile(filename); + + await Deno.writeTextFile( + filename, + content.replace(module_regex, `$1@${version}`), + ); + } catch (e) { + console.error( + ansi.bgRed(" FAILED "), + `⚠︎ could not update ${ansi.underline(ansi.italic(filename))} to ${ + ansi.bold(version) + }!\n\n${e}`, + ); + } +}