diff --git a/__snapshots__/inspect_test.ts.snap b/__snapshots__/inspect_test.ts.snap new file mode 100644 index 0000000..7b658a4 --- /dev/null +++ b/__snapshots__/inspect_test.ts.snap @@ -0,0 +1,45 @@ +export const snapshot = {}; + +snapshot[`inspect > string 1`] = `'"hello world"'`; + +snapshot[`inspect > number 1`] = `"100"`; + +snapshot[`inspect > bigint 1`] = `"100n"`; + +snapshot[`inspect > boolean 1`] = `"true"`; + +snapshot[`inspect > array 1`] = `"[]"`; + +snapshot[`inspect > array 2`] = `"[0, 1, 2]"`; + +snapshot[`inspect > array 3`] = `'[0, "a", true]'`; + +snapshot[`inspect > array 4`] = `"[0, [1, [2]]]"`; + +snapshot[`inspect > record 1`] = `"{}"`; + +snapshot[`inspect > record 2`] = `"{a: 0, b: 1, c: 2}"`; + +snapshot[`inspect > record 3`] = ` +'{ + a: "a", + b: 1, + c: true +}' +`; + +snapshot[`inspect > record 4`] = `"{a: {b: {c: 0}}}"`; + +snapshot[`inspect > function 1`] = `"inspect"`; + +snapshot[`inspect > function 2`] = `"(anonymous)"`; + +snapshot[`inspect > null 1`] = `"null"`; + +snapshot[`inspect > undefined 1`] = `"undefined"`; + +snapshot[`inspect > symbol 1`] = `"Symbol(a)"`; + +snapshot[`inspect > date 1`] = `"Date"`; + +snapshot[`inspect > promise 1`] = `"Promise"`; diff --git a/__snapshots__/is_test.ts.snap b/__snapshots__/is_test.ts.snap new file mode 100644 index 0000000..da7559a --- /dev/null +++ b/__snapshots__/is_test.ts.snap @@ -0,0 +1,92 @@ +export const snapshot = {}; + +snapshot[`isArrayOf > returns properly named function 1`] = `"isArrayOf(isNumber)"`; + +snapshot[`isArrayOf > returns properly named function 2`] = `"isArrayOf((anonymous))"`; + +snapshot[`isTupleOf > returns properly named function 1`] = ` +"isTupleOf([ + isNumber, + isString, + isBoolean +])" +`; + +snapshot[`isTupleOf > returns properly named function 2`] = `"isTupleOf([(anonymous)])"`; + +snapshot[`isTupleOf > returns properly named function 3`] = ` +"isTupleOf([ + isTupleOf([ + isTupleOf([ + isNumber, + isString, + isBoolean + ]) + ]) +])" +`; + +snapshot[`isUniformTupleOf > returns properly named function 1`] = `"isUniformTupleOf(3, isAny)"`; + +snapshot[`isUniformTupleOf > returns properly named function 2`] = `"isUniformTupleOf(3, isNumber)"`; + +snapshot[`isUniformTupleOf > returns properly named function 3`] = `"isUniformTupleOf(3, (anonymous))"`; + +snapshot[`isRecordOf > returns properly named function 1`] = `"isRecordOf(isNumber)"`; + +snapshot[`isRecordOf > returns properly named function 2`] = `"isRecordOf((anonymous))"`; + +snapshot[`isObjectOf > returns properly named function 1`] = ` +"isObjectOf({ + a: isNumber, + b: isString, + c: isBoolean +})" +`; + +snapshot[`isObjectOf > returns properly named function 2`] = `"isObjectOf({a: a})"`; + +snapshot[`isObjectOf > returns properly named function 3`] = ` +"isObjectOf({ + a: isObjectOf({ + b: isObjectOf({c: isBoolean}) + }) +})" +`; + +snapshot[`isInstanceOf > returns properly named function 1`] = `"isInstanceOf(Date)"`; + +snapshot[`isInstanceOf > returns properly named function 2`] = `"isInstanceOf((anonymous))"`; + +snapshot[`isLiteralOf > returns properly named function 1`] = `'isLiteralOf("hello")'`; + +snapshot[`isLiteralOf > returns properly named function 2`] = `"isLiteralOf(100)"`; + +snapshot[`isLiteralOf > returns properly named function 3`] = `"isLiteralOf(100n)"`; + +snapshot[`isLiteralOf > returns properly named function 4`] = `"isLiteralOf(true)"`; + +snapshot[`isLiteralOf > returns properly named function 5`] = `"isLiteralOf(null)"`; + +snapshot[`isLiteralOf > returns properly named function 6`] = `"isLiteralOf(undefined)"`; + +snapshot[`isLiteralOf > returns properly named function 7`] = `"isLiteralOf(Symbol(asdf))"`; + +snapshot[`isLiteralOneOf > returns properly named function 1`] = `'isLiteralOneOf(["hello", "world"])'`; + +snapshot[`isOneOf > returns properly named function 1`] = ` +"isOneOf([ + isNumber, + isString, + isBoolean +])" +`; + +snapshot[`isAllOf > returns properly named function 1`] = ` +"isAllOf([ + isObjectOf({a: isNumber}), + isObjectOf({b: isString}) +])" +`; + +snapshot[`isOptionalOf > returns properly named function 1`] = `"isOptionalOf(isNumber)"`; diff --git a/inspect.ts b/inspect.ts new file mode 100644 index 0000000..010f31e --- /dev/null +++ b/inspect.ts @@ -0,0 +1,59 @@ +const defaultThreshold = 20; + +export type InspectOptions = { + // The maximum number of characters of a single attribute + threshold?: number; +}; + +/** + * Inspect a value + */ +export function inspect(value: unknown, options: InspectOptions = {}): string { + if (value === null) { + return "null"; + } else if (Array.isArray(value)) { + return inspectArray(value, options); + } + switch (typeof value) { + case "string": + return JSON.stringify(value); + case "bigint": + return `${value}n`; + case "object": + if (value.constructor?.name !== "Object") { + return value.constructor?.name; + } + return inspectRecord(value as Record, options); + case "function": + return value.name || "(anonymous)"; + } + return value?.toString() ?? "undefined"; +} + +function inspectArray(value: unknown[], options: InspectOptions): string { + const { threshold = defaultThreshold } = options; + const vs = value.map((v) => inspect(v, options)); + const s = vs.join(", "); + if (s.length <= threshold) return `[${s}]`; + const m = vs.join(",\n"); + return `[\n${indent(2, m)}\n]`; +} + +function inspectRecord( + value: Record, + options: InspectOptions, +): string { + const { threshold = defaultThreshold } = options; + const vs = Object.entries(value).map(([k, v]) => + `${k}: ${inspect(v, options)}` + ); + const s = vs.join(", "); + if (s.length <= threshold) return `{${s}}`; + const m = vs.join(",\n"); + return `{\n${indent(2, m)}\n}`; +} + +function indent(level: number, text: string): string { + const prefix = " ".repeat(level); + return text.split("\n").map((line) => `${prefix}${line}`).join("\n"); +} diff --git a/inspect_test.ts b/inspect_test.ts new file mode 100644 index 0000000..b28d965 --- /dev/null +++ b/inspect_test.ts @@ -0,0 +1,50 @@ +import { + assertSnapshot, +} from "https://deno.land/std@0.202.0/testing/snapshot.ts"; +import { inspect } from "./inspect.ts"; + +Deno.test("inspect", async (t) => { + await t.step("string", async (t) => { + await assertSnapshot(t, inspect("hello world")); + }); + await t.step("number", async (t) => { + await assertSnapshot(t, inspect(100)); + }); + await t.step("bigint", async (t) => { + await assertSnapshot(t, inspect(100n)); + }); + await t.step("boolean", async (t) => { + await assertSnapshot(t, inspect(true)); + }); + await t.step("array", async (t) => { + await assertSnapshot(t, inspect([])); + await assertSnapshot(t, inspect([0, 1, 2])); + await assertSnapshot(t, inspect([0, "a", true])); + await assertSnapshot(t, inspect([0, [1, [2]]])); + }); + await t.step("record", async (t) => { + await assertSnapshot(t, inspect({})); + await assertSnapshot(t, inspect({ a: 0, b: 1, c: 2 })); + await assertSnapshot(t, inspect({ a: "a", b: 1, c: true })); + await assertSnapshot(t, inspect({ a: { b: { c: 0 } } })); + }); + await t.step("function", async (t) => { + await assertSnapshot(t, inspect(inspect)); + await assertSnapshot(t, inspect(() => {})); + }); + await t.step("null", async (t) => { + await assertSnapshot(t, inspect(null)); + }); + await t.step("undefined", async (t) => { + await assertSnapshot(t, inspect(undefined)); + }); + await t.step("symbol", async (t) => { + await assertSnapshot(t, inspect(Symbol("a"))); + }); + await t.step("date", async (t) => { + await assertSnapshot(t, inspect(new Date())); + }); + await t.step("promise", async (t) => { + await assertSnapshot(t, inspect(new Promise(() => {}))); + }); +}); diff --git a/is.ts b/is.ts index de06d53..9c89699 100644 --- a/is.ts +++ b/is.ts @@ -1,3 +1,5 @@ +import { inspect } from "./inspect.ts"; + /** * A type predicate function */ @@ -8,6 +10,14 @@ export type Predicate = (x: unknown) => x is T; */ export type PredicateType

= P extends Predicate ? T : never; +/** + * Always return `true` regardless of the type of `x`. + */ +// deno-lint-ignore no-explicit-any +export function isAny(_x: unknown): _x is any { + return true; +} + /** * Return `true` if the type of `x` is `string`. */ @@ -51,7 +61,14 @@ export function isArray( export function isArrayOf( pred: Predicate, ): Predicate { - return (x: unknown): x is T[] => isArray(x) && x.every(pred); + return Object.defineProperties( + (x: unknown): x is T[] => isArray(x) && x.every(pred), + { + name: { + get: () => `isArrayOf(${inspect(pred)})`, + }, + }, + ); } export type TupleOf[]> = { @@ -79,12 +96,19 @@ export type TupleOf[]> = { export function isTupleOf[]>( predTup: T, ): Predicate> { - return (x: unknown): x is TupleOf => { - if (!isArray(x) || x.length !== predTup.length) { - return false; - } - return predTup.every((pred, i) => pred(x[i])); - }; + return Object.defineProperties( + (x: unknown): x is TupleOf => { + if (!isArray(x) || x.length !== predTup.length) { + return false; + } + return predTup.every((pred, i) => pred(x[i])); + }, + { + name: { + get: () => `isTupleOf(${inspect(predTup)})`, + }, + }, + ); } // https://stackoverflow.com/a/71700658/1273406 @@ -114,10 +138,17 @@ export type UniformTupleOf< */ export function isUniformTupleOf( n: N, - pred: Predicate = (_x: unknown): _x is T => true, + pred: Predicate = isAny, ): Predicate> { - const predTup = Array(n).fill(pred); - return isTupleOf(predTup) as Predicate>; + const predInner = isTupleOf(Array(n).fill(pred)); + return Object.defineProperties( + (x: unknown): x is UniformTupleOf => predInner(x), + { + name: { + get: () => `isUniformTupleOf(${n}, ${inspect(pred)})`, + }, + }, + ); } /** @@ -143,13 +174,20 @@ export function isRecord( export function isRecordOf( pred: Predicate, ): Predicate> { - return (x: unknown): x is RecordOf => { - if (!isRecord(x)) return false; - for (const k in x) { - if (!pred(x[k])) return false; - } - return true; - }; + return Object.defineProperties( + (x: unknown): x is RecordOf => { + if (!isRecord(x)) return false; + for (const k in x) { + if (!pred(x[k])) return false; + } + return true; + }, + { + name: { + get: () => `isRecordOf(${inspect(pred)})`, + }, + }, + ); } type FlatType = T extends RecordOf @@ -196,9 +234,16 @@ export function isObjectOf< T extends RecordOf>, >( predObj: T, - options: { strict?: boolean } = {}, + { strict }: { strict?: boolean } = {}, ): Predicate> { - return options.strict ? isObjectOfStrict(predObj) : isObjectOfLoose(predObj); + return Object.defineProperties( + strict ? isObjectOfStrict(predObj) : isObjectOfLoose(predObj), + { + name: { + get: () => `isObjectOf(${inspect(predObj)})`, + }, + }, + ); } function isObjectOfLoose< @@ -253,7 +298,14 @@ export function isFunction(x: unknown): x is (...args: unknown[]) => unknown { export function isInstanceOf unknown>( ctor: T, ): Predicate> { - return (x: unknown): x is InstanceType => x instanceof ctor; + return Object.defineProperties( + (x: unknown): x is InstanceType => x instanceof ctor, + { + name: { + get: () => `isInstanceOf(${inspect(ctor)})`, + }, + }, + ); } /** @@ -304,8 +356,15 @@ export function isPrimitive(x: unknown): x is Primitive { /** * Return a type predicate function that returns `true` if the type of `x` is a literal type of `pred`. */ -export function isLiteralOf(pred: T): Predicate { - return (x: unknown): x is T => x === pred; +export function isLiteralOf(literal: T): Predicate { + return Object.defineProperties( + (x: unknown): x is T => x === literal, + { + name: { + get: () => `isLiteralOf(${inspect(literal)})`, + }, + }, + ); } /** @@ -321,10 +380,17 @@ export function isLiteralOf(pred: T): Predicate { * ``` */ export function isLiteralOneOf( - preds: T, + literals: T, ): Predicate { - return (x: unknown): x is T[number] => - preds.includes(x as unknown as T[number]); + return Object.defineProperties( + (x: unknown): x is T[number] => + literals.includes(x as unknown as T[number]), + { + name: { + get: () => `isLiteralOneOf(${inspect(literals)})`, + }, + }, + ); } export type OneOf = T extends Predicate[] ? U : never; @@ -346,7 +412,14 @@ export type OneOf = T extends Predicate[] ? U : never; export function isOneOf[]>( preds: T, ): Predicate> { - return (x: unknown): x is OneOf => preds.some((pred) => pred(x)); + return Object.defineProperties( + (x: unknown): x is OneOf => preds.some((pred) => pred(x)), + { + name: { + get: () => `isOneOf(${inspect(preds)})`, + }, + }, + ); } type UnionToIntersection = @@ -372,7 +445,14 @@ export type AllOf = UnionToIntersection>; export function isAllOf[]>( preds: T, ): Predicate> { - return (x: unknown): x is AllOf => preds.every((pred) => pred(x)); + return Object.defineProperties( + (x: unknown): x is AllOf => preds.every((pred) => pred(x)), + { + name: { + get: () => `isAllOf(${inspect(preds)})`, + }, + }, + ); } export type OptionalPredicate = Predicate & { @@ -395,15 +475,21 @@ export type OptionalPredicate = Predicate & { export function isOptionalOf( pred: Predicate, ): OptionalPredicate { - return Object.assign( + return Object.defineProperties( (x: unknown): x is Predicate => isUndefined(x) || pred(x), { - optional: true as const, + optional: { + value: true as const, + }, + name: { + get: () => `isOptionalOf(${inspect(pred)})`, + }, }, ) as OptionalPredicate; } export default { + Any: isAny, String: isString, Number: isNumber, BigInt: isBigInt, diff --git a/is_test.ts b/is_test.ts index ca6af53..310c636 100644 --- a/is_test.ts +++ b/is_test.ts @@ -2,12 +2,16 @@ import { assertEquals, assertStrictEquals, } from "https://deno.land/std@0.202.0/assert/mod.ts"; +import { + assertSnapshot, +} from "https://deno.land/std@0.202.0/testing/snapshot.ts"; import type { AssertTrue, IsExact, } from "https://deno.land/std@0.202.0/testing/types.ts"; import is, { isAllOf, + isAny, isArray, isArrayOf, isBigInt, @@ -108,6 +112,25 @@ Deno.test("PredicateType", () => { >; }); +Deno.test("isAny", async (t) => { + await testWithExamples(t, isAny, { + validExamples: [ + "string", + "number", + "bigint", + "boolean", + "array", + "record", + "function", + "null", + "undefined", + "symbol", + "date", + "promise", + ], + }); +}); + Deno.test("isString", async (t) => { await testWithExamples(t, isString, { validExamples: ["string"] }); }); @@ -129,6 +152,10 @@ Deno.test("isArray", async (t) => { }); Deno.test("isArrayOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isArrayOf(isNumber).name); + await assertSnapshot(t, isArrayOf((_x): _x is string => false).name); + }); await t.step("returns proper type predicate", () => { const a: unknown = [0, 1, 2]; if (isArrayOf(isNumber)(a)) { @@ -151,6 +178,15 @@ Deno.test("isArrayOf", async (t) => { }); Deno.test("isTupleOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isTupleOf([isNumber, isString, isBoolean]).name); + await assertSnapshot(t, isTupleOf([(_x): _x is string => false]).name); + // Nested + await assertSnapshot( + t, + isTupleOf([isTupleOf([isTupleOf([isNumber, isString, isBoolean])])]).name, + ); + }); await t.step("returns proper type predicate", () => { const predTup = [isNumber, isString, isBoolean] as const; const a: unknown = [0, "a", true]; @@ -182,6 +218,14 @@ Deno.test("isTupleOf", async (t) => { }); Deno.test("isUniformTupleOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isUniformTupleOf(3).name); + await assertSnapshot(t, isUniformTupleOf(3, isNumber).name); + await assertSnapshot( + t, + isUniformTupleOf(3, (_x): _x is string => false).name, + ); + }); await t.step("returns proper type predicate", () => { const a: unknown = [0, 1, 2, 3, 4]; if (isUniformTupleOf(5)(a)) { @@ -220,6 +264,10 @@ Deno.test("isRecord", async (t) => { }); Deno.test("isRecordOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isRecordOf(isNumber).name); + await assertSnapshot(t, isRecordOf((_x): _x is string => false).name); + }); await t.step("returns proper type predicate", () => { const a: unknown = { a: 0 }; if (isRecordOf(isNumber)(a)) { @@ -244,6 +292,21 @@ Deno.test("isRecordOf", async (t) => { }); Deno.test("isObjectOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot( + t, + isObjectOf({ a: isNumber, b: isString, c: isBoolean }).name, + ); + await assertSnapshot( + t, + isObjectOf({ a: (_x): _x is string => false }).name, + ); + // Nested + await assertSnapshot( + t, + isObjectOf({ a: isObjectOf({ b: isObjectOf({ c: isBoolean }) }) }).name, + ); + }); await t.step("returns proper type predicate", () => { const predObj = { a: isNumber, @@ -394,6 +457,10 @@ Deno.test("isFunction", async (t) => { }); Deno.test("isInstanceOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isInstanceOf(Date).name); + await assertSnapshot(t, isInstanceOf(class {}).name); + }); await t.step("returns true on T instance", () => { class Cls {} assertEquals(isInstanceOf(Cls)(new Cls()), true); @@ -464,6 +531,15 @@ Deno.test("isPrimitive", async (t) => { }); Deno.test("isLiteralOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isLiteralOf("hello").name); + await assertSnapshot(t, isLiteralOf(100).name); + await assertSnapshot(t, isLiteralOf(100n).name); + await assertSnapshot(t, isLiteralOf(true).name); + await assertSnapshot(t, isLiteralOf(null).name); + await assertSnapshot(t, isLiteralOf(undefined).name); + await assertSnapshot(t, isLiteralOf(Symbol("asdf")).name); + }); await t.step("returns proper type predicate", () => { const pred = "hello"; const a: unknown = "hello"; @@ -482,6 +558,9 @@ Deno.test("isLiteralOf", async (t) => { }); Deno.test("isLiteralOneOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isLiteralOneOf(["hello", "world"]).name); + }); await t.step("returns proper type predicate", () => { const preds = ["hello", "world"] as const; const a: unknown = "hello"; @@ -501,6 +580,9 @@ Deno.test("isLiteralOneOf", async (t) => { }); Deno.test("isOneOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isOneOf([isNumber, isString, isBoolean]).name); + }); await t.step("returns proper type predicate", () => { const preds = [isNumber, isString, isBoolean]; const a: unknown = [0, "a", true]; @@ -523,6 +605,15 @@ Deno.test("isOneOf", async (t) => { }); Deno.test("isAllOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot( + t, + isAllOf([ + is.ObjectOf({ a: is.Number }), + is.ObjectOf({ b: is.String }), + ]).name, + ); + }); await t.step("returns proper type predicate", () => { const preds = [ is.ObjectOf({ a: is.Number }), @@ -562,6 +653,9 @@ Deno.test("isAllOf", async (t) => { }); Deno.test("isOptionalOf", async (t) => { + await t.step("returns properly named function", async (t) => { + await assertSnapshot(t, isOptionalOf(isNumber).name); + }); await t.step("returns proper type predicate", () => { const a: unknown = undefined; if (isOptionalOf(isNumber)(a)) { diff --git a/scripts/build_npm.ts b/scripts/build_npm.ts index b2d8231..2f498cf 100644 --- a/scripts/build_npm.ts +++ b/scripts/build_npm.ts @@ -13,6 +13,10 @@ await emptyDir("./npm"); await build({ typeCheck: false, + // XXX: + // snapshot tests doesn't work with dnt so we disable tests for now + // https://github.com/denoland/dnt/issues/254 + test: false, entryPoints: ["./mod.ts"], outDir: "./npm", shims: {