@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
494 lines (490 loc) • 15.5 kB
JavaScript
;
var _class = require('./class.js');
/**
* Functions for dealing with objects.
* @module
*/
/**
* Returns `true` if the specified object has the indicated property as its own property.
* If the property is inherited, or does not exist, the function returns `false`.
*
* @example
* ```ts
* import { hasOwn } from "@ayonli/jsext/object";
*
* const obj = { foo: "hello" };
*
* console.log(hasOwn(obj, "foo")); // true
* console.log(hasOwn(obj, "toString")); // false
* ```
*/
function hasOwn(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
/**
* Returns `true` if the specified object has the indicated method as its own method (in its own
* prototype). If the method is inherited, or is not in the prototype, or does not exist, this
* function returns `false`.
*
* @example
* ```ts
* import { hasOwnMethod } from "@ayonli/jsext/object";
*
* class MyClass {
* foo() {
* return "Hello";
* }
*
* bar = () => "World";
* }
*
* const obj = new MyClass();
*
* console.log(hasOwnMethod(obj, "foo")); // true
* console.log(hasOwnMethod(obj, "bar")); // false
* console.log(hasOwnMethod(obj, "toString")); // false
* ```
*/
function hasOwnMethod(obj, method) {
var _a;
const proto = Object.getPrototypeOf(obj);
if (!proto || !hasOwn(proto, method)) {
return false;
}
return typeof ((_a = Object.getOwnPropertyDescriptor(proto, method)) === null || _a === void 0 ? void 0 : _a.value) === "function";
}
function patch(target, ...sources) {
for (const source of sources) {
for (const key of Reflect.ownKeys(source)) {
if (!hasOwn(target, key) || target[key] === undefined) {
target[key] = source[key];
}
}
}
return target;
}
function pick(obj, keys) {
return keys.reduce((result, key) => {
if (key in obj && obj[key] !== undefined) {
result[key] = obj[key];
}
return result;
}, {});
}
function omit(obj, keys) {
const allKeys = Reflect.ownKeys(obj);
const keptKeys = allKeys.filter(key => !keys.includes(key));
const result = pick(obj, keptKeys);
// special treatment for Error types
if (obj instanceof Error) {
["name", "message", "stack", "cause"].forEach(key => {
if (!keys.includes(key) &&
obj[key] !== undefined &&
!hasOwn(result, key)) {
result[key] = obj[key];
}
});
}
return result;
}
function as(value, type) {
if (typeof type !== "function") {
throw new TypeError("type must be a valid constructor");
}
let _type;
const primitiveMap = {
"string": String,
"number": Number,
"bigint": BigInt,
"boolean": Boolean,
"symbol": Symbol
};
if (value instanceof type) {
if ([String, Number, Boolean].includes(type)) {
return value.valueOf(); // make sure the primitives are returned.
}
else {
return value;
}
}
else if ((_type = typeof value) && primitiveMap[_type] === type) {
return value;
}
return null;
}
/**
* Returns a string representation or the constructor of the value's type.
*
* **NOTE:** This function returns `"class"` for ES6 classes.
*
* **NOTE:** This function returns `"null"` for `null`.
*
* **NOTE:** This function returns `Object` for `Object.create(null)`.
*
* @example
* ```ts
* import { typeOf } from "@ayonli/jsext/object";
*
* console.log(typeOf("Hello")); // string
* console.log(typeOf(42)); // number
* console.log(typeOf(42n)); // bigint
* console.log(typeOf(true)); // boolean
* console.log(typeOf(Symbol("foo"))); // symbol
* console.log(typeOf(() => {})); // function
* console.log(typeOf(class Foo {})); // class
* console.log(typeOf(undefined)); // undefined
* console.log(typeOf(null)); // null
* console.log(typeOf({ foo: "bar" })); // [Function: Object]
* console.log(typeOf(Object.create(null))); // [Function: Object]
* console.log(typeOf([1, 2, 3])); // [Function: Array]
* console.log(typeOf(new Date())); // [Function: Date]
* ```
*/
function typeOf(value) {
var _a, _b;
if (value === undefined) {
return "undefined";
}
else if (value === null) {
return "null";
}
const type = typeof value;
if (type === "function") {
return _class.isClass(value) ? "class" : "function";
}
else if (type === "object") {
return (_b = (_a = Object.getPrototypeOf(value)) === null || _a === void 0 ? void 0 : _a.constructor) !== null && _b !== void 0 ? _b : Object;
}
else {
return type;
}
}
/**
* Returns `true` if the given value is valid. The following values are considered invalid:
*
* - `undefined`
* - `null`
* - `NaN`
* - `Invalid Date`
*/
function isValid(value) {
return value !== undefined
&& value !== null
&& !Object.is(value, NaN)
&& !(value instanceof Date && value.toString() === "Invalid Date");
}
/**
* Returns `true` is the given value is a plain object, that is, an object created by
* the `Object` constructor or one with a `[[Prototype]]` of `null`.
*
* @example
* ```ts
* import { isPlainObject } from "@ayonli/jsext/object";
*
* console.log(isPlainObject({ foo: "bar" })); // true
* console.log(isPlainObject(Object.create(null))); // true
* console.log(isPlainObject(new Map([["foo", "bar"]]))); // false
* ```
*/
function isPlainObject(value) {
if (typeof value !== "object" || value === null)
return false;
const proto = Object.getPrototypeOf(value);
return proto === null || proto.constructor === Object;
}
/**
* Creates an object base on the original object but without any invalid values
* (except for `null`), and trims the value if it's a string.
*
* **NOTE:** This function only operates on plain objects and arrays.
*
* @example
* ```ts
* import { sanitize } from "@ayonli/jsext/object";
*
* const obj = sanitize({
* foo: "Hello",
* bar: " World ",
* baz: undefined,
* num: NaN,
* });
*
* console.log(obj); // { foo: "Hello", bar: "World" }
* ```
*/
function sanitize(obj, deep = false, options = {}) {
const { removeNulls, removeEmptyStrings, removeEmptyObjects, removeArrayItems } = options;
return (function process(target, depth) {
if (typeof target === "string") {
return target.trim();
}
else if (Array.isArray(target)) {
const arr = !depth || deep ? target.map(item => process(item, depth + 1)) : target;
if (removeArrayItems) {
return arr.filter(value => {
if (value === null) {
return !removeNulls;
}
else if (value === "") {
return !removeEmptyStrings;
}
else if (isValid(value)) {
if (typeof value !== "object") {
return true;
}
else if (Array.isArray(value)) {
return value.length > 0 || !removeEmptyObjects;
}
else if (isPlainObject(value)) {
return Reflect.ownKeys(value).length > 0 || !removeEmptyObjects;
}
else {
return true;
}
}
else {
return false;
}
});
}
else {
return arr;
}
}
else if (isPlainObject(target)) {
return !depth || deep ? Reflect.ownKeys(target).reduce((result, key) => {
const value = process(target[key], depth + 1);
if (value === null) {
if (!removeNulls) {
result[key] = value;
}
}
else if (value === "") {
if (!removeEmptyStrings) {
result[key] = value;
}
}
else if (isValid(value)) {
if (typeof value !== "object") {
result[key] = value;
}
else if (Array.isArray(value)) {
if (value.length > 0 || !removeEmptyObjects) {
result[key] = value;
}
}
else if (isPlainObject(value)) {
if (Reflect.ownKeys(value).length > 0 || !removeEmptyObjects) {
result[key] = value;
}
}
else {
result[key] = value;
}
}
return result;
}, target.constructor ? {} : Object.create(null)) : target;
}
else {
return target;
}
})(obj, 0);
}
/**
* Creates an object with sorted keys (in ascending order) of the original object.
*
* **NOTE:** Symbol keys are not sorted and remain their original order.
*
* **NOTE:** This function only operates on plain objects and arrays.
*
* @example
* ```ts
* import { sortKeys } from "@ayonli/jsext/object";
*
* const obj = sortKeys({ foo: "Hello", bar: "World" });
*
* console.log(JSON.stringify(obj)); // { "bar": "World", "foo": "Hello" }
* ```
*/
function sortKeys(obj, deep = false) {
return (function process(target, depth) {
if (isPlainObject(target)) {
return !depth || deep ? [
...Object.getOwnPropertyNames(target).sort(), // sort the string keys
...Object.getOwnPropertySymbols(target)
].reduce((result, key) => {
result[key] = process(target[key], depth + 1);
return result;
}, target.constructor ? {} : Object.create(null)) : target;
}
else if (Array.isArray(target)) {
return !depth || deep ? target.map(item => process(item, depth + 1)) : target;
}
else {
return target;
}
})(obj, 0);
}
/**
* Create an object with flatted keys of the original object, the children
* nodes' properties will be transformed to a string-represented path.
*
* **NOTE:** This function only operates on plain objects and arrays.
*
* @param depth Default value: `1`.
* @example
* ```ts
* import { flatKeys } from "@ayonli/jsext/object";
*
* const obj = flatKeys({ foo: { bar: "Hello", baz: "World" } });
*
* console.log(obj); // { "foo.bar": "Hello", "foo.baz": "World" }
* ```
*/
function flatKeys(obj, depth = 1, options = {}) {
var _a;
const maxDepth = depth;
const carrier = obj.constructor ? {} : Object.create(null);
const flatArrayIndices = (_a = options === null || options === void 0 ? void 0 : options.flatArrayIndices) !== null && _a !== void 0 ? _a : false;
if (!isPlainObject(obj) && (!Array.isArray(obj) || !flatArrayIndices)) {
return obj;
}
(function process(target, path, depth) {
if (depth === maxDepth) {
carrier[path] = target;
}
else if (Array.isArray(target) && depth) {
if (!flatArrayIndices) {
carrier[path] = target;
}
else {
target.forEach((value, i) => {
process(value, path ? `${path}.${i}` : String(i), path ? depth + 1 : depth);
});
}
}
else if (isPlainObject(target) || (Array.isArray(target) && !depth)) {
Reflect.ownKeys(target).forEach(key => {
const value = target[key];
if (typeof key === "symbol") {
if (depth === 0) { // only allow top-level symbol properties
carrier[key] = value;
}
}
else {
process(value, path ? `${path}.${key}` : key, path ? depth + 1 : depth);
}
});
}
else {
carrier[path] = target;
}
})(obj, "", 0);
return carrier;
}
/**
* Returns a new record with all entries of the given record except the ones
* that do not match the given predicate.
*
* This function is effectively as
* `Object.fromEntries(Object.entries(obj).filter(predicate))`.
*
* @example
* ```ts
* import { filterEntries } from "@ayonli/jsext/object";
*
* const obj = { foo: "Hello", bar: "World" };
* const result = filterEntries(obj, ([key]) => key === "foo");
*
* console.log(result); // { foo: "Hello" }
* ```
*/
function filterEntries(obj, predicate) {
return Object.fromEntries(Object.entries(obj).filter(predicate));
}
/**
* Applies the given transformer to all entries in the given record and returns
* a new record containing the results.
*
* This function is effectively as
* `Object.fromEntries(Object.entries(obj).map(transformer))`.
*
* @example
* ```ts
* import { mapEntries } from "@ayonli/jsext/object";
*
* const obj = { foo: "Hello", bar: "World" };
* const result = mapEntries(obj, ([key, value]) => [key, value.toUpperCase()]);
*
* console.log(result); // { foo: "HELLO", bar: "WORLD" }
* ```
*/
function mapEntries(obj, transformer) {
return Object.fromEntries(Object.entries(obj).map(transformer));
}
/**
* Returns a tuple of two records with the first one containing all entries of
* the given record that match the given predicate and the second one containing
* all that do not.
*
* @example
* ```ts
* import { partitionEntries } from "@ayonli/jsext/object";
*
* const obj = { foo: "Hello", bar: "World" };
* const [match, rest] = partitionEntries(obj, ([key]) => key === "foo");
*
* console.log(match); // { foo: "Hello" }
* console.log(rest); // { bar: "World" }
* ```
*/
function partitionEntries(record, predicate) {
const match = {};
const rest = {};
const entries = Object.entries(record);
for (const [key, value] of entries) {
if (predicate([key, value])) {
match[key] = value;
}
else {
rest[key] = value;
}
}
return [match, rest];
}
/**
* Composes a new record with all keys and values inverted.
*
* This function is effectively as
* `Object.fromEntries(Object.entries(record).map(([key, value]) => [value, key]))`.
*
* @example
* ```ts
* import { invert } from "@ayonli/jsext/object";
*
* const obj = { foo: "Hello", bar: "World" };
* const result = invert(obj);
*
* console.log(result); // { Hello: "foo", World: "bar" }
* ```
*/
function invert(record) {
return Object.fromEntries(Object.entries(record).map(([key, value]) => [value, key]));
}
exports.as = as;
exports.filterEntries = filterEntries;
exports.flatKeys = flatKeys;
exports.hasOwn = hasOwn;
exports.hasOwnMethod = hasOwnMethod;
exports.invert = invert;
exports.isPlainObject = isPlainObject;
exports.isValid = isValid;
exports.mapEntries = mapEntries;
exports.omit = omit;
exports.partitionEntries = partitionEntries;
exports.patch = patch;
exports.pick = pick;
exports.sanitize = sanitize;
exports.sortKeys = sortKeys;
exports.typeOf = typeOf;
//# sourceMappingURL=object.js.map