diff --git a/README.md b/README.md index d56c8a9..22df36f 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The `unknownutil` provides the following predicate functions - `isNull(x: unknown): x is null` - `isUndefined(x: unknown): x is undefined` - `isNone(x: unknown): x is null | undefined` +- `isLike(ref: R, x: unknown, pred?: Predicate): x is R` For example: @@ -52,6 +53,39 @@ if (isArray(a, isString)) { } ``` +Use `isLike` if you need some complicated types like tuple or struct like: + +```typescript +import { isLike } from "https://deno.land/x/unknownutil/mod.ts"; + +const a: unknown = ["a", 0, "b"]; +const b: unknown = ["a", 0, "b", "c"]; + +if (isLike(["", 0, ""], a)) { + // 'a' is [string, number, string] thus this block is called +} + +if (isLike(["", 0, ""], b)) { + // 'b' is [string, number, string, string] thus this block is NOT called +} + +const c: unknown = { foo: "foo", bar: 100 }; +const d: unknown = { foo: "foo", bar: 100, hoge: "hoge" }; +const e: unknown = { foo: "foo", hoge: "hoge" }; + +if (isLike({ foo: "", bar: 0 }, c)) { + // 'c' is {foo: string, bar: number} thus this block is called +} + +if (isLike({ foo: "", bar: 0 }, d)) { + // 'd' contains {foo: string, bar: number} thus this block is called +} + +if (isLike({ foo: "", bar: 0 }, e)) { + // 'e' does not contain {foo: '', bar: 0} thus this block is NOT called +} +``` + ### ensureXXXXX The `unknownutil` provides the following ensure functions which will raise @@ -93,6 +127,20 @@ ensureArray(b); // Now 'b' is 'unknown[]' ensureArray(b, isString); // Raise EnsureError on above while 'b' is not string array ``` +Use `ensureLike` if you need some complicated types like tuple or struct like: + +```typescript +import { ensureLike } from "https://deno.land/x/unknownutil/mod.ts"; + +const a: unknown = ["a", "b", "c"]; +ensureLike([], a); // Now 'a' is 'unknown[]' +ensureLike(["", "", ""], a); // Now 'a' is '[string, string, string]' + +const b: unknown = { foo: "foo", bar: 0 }; +ensureLike({}, b); // Now 'b' is 'Record' +ensureLike({ foo: "", bar: 0 }, b); // Now 'b' is '{foo: string, bar: number}' +``` + ## License The code follows MIT license written in [LICENSE](./LICENSE). Contributors need diff --git a/ensure.ts b/ensure.ts index 019497d..b4202b4 100644 --- a/ensure.ts +++ b/ensure.ts @@ -1,6 +1,7 @@ import { isArray, isFunction, + isLike, isNone, isNull, isNumber, @@ -100,3 +101,15 @@ export function ensureUndefined(x: unknown): asserts x is undefined { export function ensureNone(x: unknown): asserts x is null | undefined { return ensure(x, isNone, "The value must be null or undefined"); } + +/** + * Ensure if `x` follows the reference by raising an `EnsureError` when it doesn't. + */ +export function ensureLike( + ref: R, + x: unknown, + ipred?: Predicate, +): asserts x is R { + const pred = (x: unknown): x is T[] => isLike(ref, x, ipred); + return ensure(x, pred, "The value must follow the reference"); +} diff --git a/ensure_test.ts b/ensure_test.ts index 8e8b5ea..52380cc 100644 --- a/ensure_test.ts +++ b/ensure_test.ts @@ -3,6 +3,7 @@ import { ensure, ensureArray, ensureFunction, + ensureLike, ensureNone, ensureNull, ensureNumber, @@ -141,3 +142,125 @@ Deno.test("ensureNone throws error on non null nor undefined", () => { assertThrows(() => ensureNone({})); assertThrows(() => ensureNone(function () {})); }); + +Deno.test("ensureLike does it's job on string", () => { + const ref = ""; + ensureLike(ref, "Hello"); + + assertThrows(() => ensureLike(ref, 0)); + assertThrows(() => ensureLike(ref, [])); + assertThrows(() => ensureLike(ref, {})); + assertThrows(() => ensureLike(ref, function () {})); + assertThrows(() => ensureLike(ref, undefined)); + assertThrows(() => ensureLike(ref, null)); +}); +Deno.test("ensureLike does it's job on number", () => { + const ref = 0; + ensureLike(ref, 0); + ensureLike(ref, 1); + ensureLike(ref, 0.1); + + assertThrows(() => ensureLike(ref, "a")); + assertThrows(() => ensureLike(ref, [])); + assertThrows(() => ensureLike(ref, {})); + assertThrows(() => ensureLike(ref, function () {})); + assertThrows(() => ensureLike(ref, undefined)); + assertThrows(() => ensureLike(ref, null)); +}); +Deno.test("ensureLike does it's job on array", () => { + const ref: unknown[] = []; + ensureLike(ref, []); + ensureLike(ref, [0, 1, 2]); + ensureLike(ref, ["a", "b", "c"]); + + assertThrows(() => ensureLike(ref, "a")); + assertThrows(() => ensureLike(ref, 0)); + assertThrows(() => ensureLike(ref, {})); + assertThrows(() => ensureLike(ref, function () {})); + assertThrows(() => ensureLike(ref, undefined)); + assertThrows(() => ensureLike(ref, null)); +}); +Deno.test("ensureLike does it's job on T array", () => { + const ref: unknown[] = []; + ensureLike(ref, [0, 1, 2], isNumber); + ensureLike(ref, ["a", "b", "c"], isString); + + assertThrows(() => ensureLike(ref, [0, 1, 2], isString)); + assertThrows(() => ensureLike(ref, ["a", "b", "c"], isNumber)); +}); +Deno.test("ensureLike does it's job on tuple", () => { + const ref = ["", 0, ""]; + ensureLike(ref, ["", 0, ""]); + ensureLike(ref, ["Hello", 100, "World"]); + + assertThrows(() => ensureLike(ref, ["Hello", 100, "World", "foo"])); + assertThrows(() => ensureLike(ref, [0, 0, 0])); + assertThrows(() => ensureLike(ref, ["", "", ""])); + assertThrows(() => ensureLike(ref, [0, "", 0])); +}); +Deno.test("ensureLike does it's job on object", () => { + const ref = {}; + ensureLike(ref, {}); + ensureLike(ref, { a: 0 }); + ensureLike(ref, { a: "a" }); + + assertThrows(() => ensureLike(ref, "a")); + assertThrows(() => ensureLike(ref, 0)); + assertThrows(() => ensureLike(ref, [])); + assertThrows(() => ensureLike(ref, function () {})); + assertThrows(() => ensureLike(ref, undefined)); + assertThrows(() => ensureLike(ref, null)); +}); +Deno.test("ensureLike does it's job on T object", () => { + const ref = {}; + ensureLike(ref, { a: 0 }, isNumber); + ensureLike(ref, { a: "a" }, isString); + + assertThrows(() => ensureLike(ref, { a: 0 }, isString)); + assertThrows(() => ensureLike(ref, { a: "a" }, isNumber)); +}); +Deno.test("ensureLike does it's job on struct", () => { + const ref = { foo: "", bar: 0 }; + ensureLike(ref, { foo: "", bar: 0 }); + ensureLike(ref, { foo: "", bar: 0, hoge: "" }); + + assertThrows(() => ensureLike(ref, {})); + assertThrows(() => ensureLike(ref, { foo: "" })); + assertThrows(() => ensureLike(ref, { bar: 0 })); +}); +Deno.test("ensureLike does it's job on function", () => { + const ref = () => {}; + ensureLike(ref, ensureFunction); + ensureLike(ref, function () {}); + ensureLike(ref, () => {}); + ensureLike(ref, setTimeout); + + assertThrows(() => ensureLike(ref, "a")); + assertThrows(() => ensureLike(ref, 0)); + assertThrows(() => ensureLike(ref, [])); + assertThrows(() => ensureLike(ref, {})); + assertThrows(() => ensureLike(ref, undefined)); + assertThrows(() => ensureLike(ref, null)); +}); +Deno.test("ensureLike does it's job on null", () => { + const ref = null; + ensureLike(ref, null); + + assertThrows(() => ensureLike(ref, "a")); + assertThrows(() => ensureLike(ref, 0)); + assertThrows(() => ensureLike(ref, [])); + assertThrows(() => ensureLike(ref, {})); + assertThrows(() => ensureLike(ref, function () {})); + assertThrows(() => ensureLike(ref, undefined)); +}); +Deno.test("ensureLike does it's job on undefined", () => { + const ref = undefined; + ensureLike(ref, undefined); + + assertThrows(() => ensureLike(ref, "a")); + assertThrows(() => ensureLike(ref, 0)); + assertThrows(() => ensureLike(ref, [])); + assertThrows(() => ensureLike(ref, {})); + assertThrows(() => ensureLike(ref, function () {})); + assertThrows(() => ensureLike(ref, null)); +}); diff --git a/is.ts b/is.ts index 95f7535..d8c1315 100644 --- a/is.ts +++ b/is.ts @@ -66,3 +66,39 @@ export function isUndefined(x: unknown): x is undefined { export function isNone(x: unknown): x is null | undefined { return x == null; } + +/** + * Return true if a type of value is like a type of reference. + */ +export function isLike( + ref: R, + x: unknown, + pred?: Predicate, +): x is R { + if (isString(ref) && isString(x)) { + return true; + } + if (isNumber(ref) && isNumber(x)) { + return true; + } + if (isArray(ref, pred) && isArray(x, pred)) { + return ref.length === 0 || ( + ref.length === x.length && + ref.every((r, i) => isLike(r, x[i])) + ); + } + if (isObject(ref, pred) && isObject(x, pred)) { + const es = Object.entries(ref); + return es.length === 0 || es.every(([k, v]) => isLike(v, x[k])); + } + if (isFunction(ref) && isFunction(x)) { + return true; + } + if (isNull(ref) && isNull(x)) { + return true; + } + if (isUndefined(ref) && isUndefined(x)) { + return true; + } + return false; +} diff --git a/is_test.ts b/is_test.ts index 14b37ff..11cb59a 100644 --- a/is_test.ts +++ b/is_test.ts @@ -2,6 +2,7 @@ import { assertEquals } from "./deps_test.ts"; import { isArray, isFunction, + isLike, isNone, isNull, isNumber, @@ -81,13 +82,13 @@ Deno.test("isObject returns false on non T object", () => { assertEquals(isObject({ a: "a" }, isNumber), false); }); -Deno.test("isFunction returns true on array", () => { +Deno.test("isFunction returns true on function", () => { assertEquals(isFunction(isFunction), true); assertEquals(isFunction(function () {}), true); assertEquals(isFunction(() => {}), true); assertEquals(isFunction(setTimeout), true); }); -Deno.test("isFunction returns false on non array", () => { +Deno.test("isFunction returns false on non function", () => { assertEquals(isFunction(""), false); assertEquals(isFunction(0), false); assertEquals(isFunction([]), false); @@ -131,3 +132,130 @@ Deno.test("isNone returns false on non null/undefined", () => { assertEquals(isNone({}), false); assertEquals(isNone(function () {}), false); }); + +Deno.test("isLike returns true/false on string", () => { + const ref = ""; + assertEquals(isLike(ref, ""), true); + assertEquals(isLike(ref, "Hello World"), true); + + assertEquals(isLike(ref, 0), false); + assertEquals(isLike(ref, []), false); + assertEquals(isLike(ref, {}), false); + assertEquals(isLike(ref, function () {}), false); + assertEquals(isLike(ref, null), false); + assertEquals(isLike(ref, undefined), false); +}); +Deno.test("isLike returns true/false on number", () => { + const ref = 0; + assertEquals(isLike(ref, 0), true); + assertEquals(isLike(ref, 1234567890), true); + + assertEquals(isLike(ref, ""), false); + assertEquals(isLike(ref, []), false); + assertEquals(isLike(ref, {}), false); + assertEquals(isLike(ref, function () {}), false); + assertEquals(isLike(ref, null), false); + assertEquals(isLike(ref, undefined), false); +}); +Deno.test("isLike returns true/false on array", () => { + const ref: unknown[] = []; + assertEquals(isLike(ref, []), true); + assertEquals(isLike(ref, [0, 1, 2]), true); + assertEquals(isLike(ref, ["a", "b", "c"]), true); + assertEquals(isLike(ref, [0, "a", 1]), true); + + assertEquals(isLike(ref, ""), false); + assertEquals(isLike(ref, 0), false); + assertEquals(isLike(ref, {}), false); + assertEquals(isLike(ref, function () {}), false); + assertEquals(isLike(ref, null), false); + assertEquals(isLike(ref, undefined), false); +}); +Deno.test("isLike returns true/false on T array", () => { + const ref: unknown[] = []; + assertEquals(isLike(ref, [0, 1, 2], isNumber), true); + assertEquals(isLike(ref, ["a", "b", "c"], isString), true); + + assertEquals(isLike(ref, [0, 1, 2], isString), false); + assertEquals(isLike(ref, ["a", "b", "c"], isNumber), false); +}); +Deno.test("isLike returns true/false on tuple", () => { + const ref = ["", 0, ""]; + assertEquals(isLike(ref, ["", 0, ""]), true); + assertEquals(isLike(ref, ["Hello", 100, "World"]), true); + + assertEquals(isLike(ref, ["Hello", 100, "World", "foo"]), false); + assertEquals(isLike(ref, [0, 0, 0]), false); + assertEquals(isLike(ref, ["", "", ""]), false); + assertEquals(isLike(ref, [0, "", 0]), false); +}); +Deno.test("isLike returns true/false on object", () => { + const ref = {}; + assertEquals(isLike(ref, {}), true); + assertEquals(isLike(ref, { a: 0 }), true); + assertEquals(isLike(ref, { a: "a" }), true); + + assertEquals(isLike(ref, ""), false); + assertEquals(isLike(ref, 0), false); + assertEquals(isLike(ref, []), false); + assertEquals(isLike(ref, function () {}), false); + assertEquals(isLike(ref, null), false); + assertEquals(isLike(ref, undefined), false); +}); +Deno.test("isLike returns true/false on T object", () => { + const ref = {}; + assertEquals(isLike(ref, { a: 0 }, isNumber), true); + assertEquals(isLike(ref, { a: "a" }, isString), true); + + assertEquals(isLike(ref, { a: 0 }, isString), false); + assertEquals(isLike(ref, { a: "a" }, isNumber), false); +}); +Deno.test("isLike returns true/false on struct", () => { + const ref = { foo: "", bar: 0 }; + assertEquals(isLike(ref, { foo: "", bar: 0 }), true); + assertEquals(isLike(ref, { foo: "Hello", bar: 100 }), true); + assertEquals( + isLike(ref, { foo: "", bar: 0, hoge: "" }), + true, + ); + + assertEquals(isLike(ref, {}), false); + assertEquals(isLike(ref, { foo: "" }), false); + assertEquals(isLike(ref, { bar: 0 }), false); +}); +Deno.test("isLike returns true/false on function", () => { + const ref = () => {}; + assertEquals(isLike(ref, isFunction), true); + assertEquals(isLike(ref, function () {}), true); + assertEquals(isLike(ref, () => {}), true); + assertEquals(isLike(ref, setTimeout), true); + + assertEquals(isLike(ref, ""), false); + assertEquals(isLike(ref, 0), false); + assertEquals(isLike(ref, []), false); + assertEquals(isLike(ref, {}), false); + assertEquals(isLike(ref, null), false); + assertEquals(isLike(ref, undefined), false); +}); +Deno.test("isLike returns true/false on null", () => { + const ref = null; + assertEquals(isLike(ref, null), true); + + assertEquals(isLike(ref, ""), false); + assertEquals(isLike(ref, 0), false); + assertEquals(isLike(ref, []), false); + assertEquals(isLike(ref, {}), false); + assertEquals(isLike(ref, function () {}), false); + assertEquals(isLike(ref, undefined), false); +}); +Deno.test("isLike returns true/false on undefined", () => { + const ref = undefined; + assertEquals(isLike(ref, undefined), true); + + assertEquals(isLike(ref, ""), false); + assertEquals(isLike(ref, 0), false); + assertEquals(isLike(ref, []), false); + assertEquals(isLike(ref, {}), false); + assertEquals(isLike(ref, function () {}), false); + assertEquals(isLike(ref, null), false); +});