feat(orderBy, isSymbol): add compatibility with lodash (#357)

* Add internal functions

* add orderBy with compat

* Add docs

* Fix typo

* Split the code for readability

* Change convert to path logic and variable name for readability

* Add test cases and js doc in isKey and isSymbol

* Add testcase and change logic of isKey

* change bench category name

* split converToPropertyName for testing

* Add case for coverage

* fix type

* Change using getTage to instanceof

* move to predicate

* Add doc

* Add bench and description in isSymbol

* Fix type and test case

* Simplify function names

* Feat: handle the deep path like keys
This commit is contained in:
Dayong Lee 2024-08-10 16:30:32 +09:00 committed by GitHub
parent 4af0662066
commit bf33cfbca5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 427 additions and 20 deletions

View File

@ -1,5 +1,6 @@
import { bench, describe } from 'vitest'; import { bench, describe } from 'vitest';
import { isSymbol as isSymbolToolkit } from 'es-toolkit'; import { isSymbol as isSymbolToolkit } from 'es-toolkit';
import { isSymbol as isSymbolToolkitCompat } from 'es-toolkit/compat';
import { isSymbol as isSymbolLodash } from 'lodash'; import { isSymbol as isSymbolLodash } from 'lodash';
describe('isSymbol', () => { describe('isSymbol', () => {
@ -11,6 +12,16 @@ describe('isSymbol', () => {
isSymbolToolkit({}); isSymbolToolkit({});
isSymbolToolkit(123); isSymbolToolkit(123);
}); });
bench('es-toolkit/compat/isSymbol', () => {
isSymbolToolkitCompat(Symbol('a'));
isSymbolToolkitCompat(Symbol.for('a'));
isSymbolToolkitCompat(Symbol.iterator);
isSymbolToolkitCompat('');
isSymbolToolkitCompat({});
isSymbolToolkitCompat(123);
});
bench('lodash/isSymbol', () => { bench('lodash/isSymbol', () => {
isSymbolLodash(Symbol('a')); isSymbolLodash(Symbol('a'));
isSymbolLodash(Symbol.for('a')); isSymbolLodash(Symbol.for('a'));

View File

@ -1,25 +1,46 @@
import { bench, describe } from 'vitest'; import { bench, describe } from 'vitest';
import { orderBy as orderByToolkit } from 'es-toolkit'; import { orderBy as orderByToolkit } from 'es-toolkit';
import { orderBy as orderByToolkitCompat } from 'es-toolkit/compat';
import { orderBy as orderByLodash } from 'lodash'; import { orderBy as orderByLodash } from 'lodash';
const users = [
{ user: 'fred', age: 48, nested: { user: 'fred' } },
{ user: 'barney', age: 34, nested: { user: 'barney' } },
{ user: 'fred', age: 40, nested: { user: 'fred' } },
{ user: 'barney', age: 36, nested: { user: 'bar' } },
];
const keys: Array<keyof (typeof users)[0]> = ['user', 'age'];
const orders: Array<'asc' | 'desc'> = ['asc', 'desc'];
describe('orderBy', () => { describe('orderBy', () => {
bench('es-toolkit/orderBy', () => { bench('es-toolkit/orderBy', () => {
const users = [ orderByToolkit(users, keys, orders);
{ user: 'fred', age: 48 }, });
{ user: 'barney', age: 34 },
{ user: 'fred', age: 40 }, bench('es-toolkit/compat/orderBy', () => {
{ user: 'barney', age: 36 }, orderByToolkitCompat(users, keys, orders);
];
orderByToolkit(users, ['user', 'age'], ['asc', 'asc']);
}); });
bench('lodash/orderBy', () => { bench('lodash/orderBy', () => {
const users = [ orderByLodash(users, keys, orders);
{ user: 'fred', age: 48 }, });
{ user: 'barney', age: 34 }, });
{ user: 'fred', age: 40 },
{ user: 'barney', age: 36 }, describe('orderBy (nested property names)', () => {
]; bench('es-toolkit/compat/orderBy', () => {
orderByLodash(users, ['user', 'age'], ['asc', 'asc']); orderByToolkitCompat(users, [['nested', 'user'], ['age']], orders);
});
bench('lodash/orderBy', () => {
orderByLodash(users, [['nested', 'user'], ['age']], orders);
});
});
describe('orderBy (property path)', () => {
bench('es-toolkit/compat/orderBy', () => {
orderByToolkitCompat(users, ['nested.user', 'age'], orders);
});
bench('lodash/orderBy', () => {
orderByLodash(users, ['nested.user', 'age'], orders);
}); });
}); });

View File

@ -135,7 +135,7 @@ Even if a feature is marked "in review," it might already be under review to ens
| [invokeMap](https://lodash.com/docs/4.17.15#invokeMap) | ❌ | | [invokeMap](https://lodash.com/docs/4.17.15#invokeMap) | ❌ |
| [keyBy](https://lodash.com/docs/4.17.15#keyBy) | 📝 | | [keyBy](https://lodash.com/docs/4.17.15#keyBy) | 📝 |
| [map](https://lodash.com/docs/4.17.15#map) | ❌ | | [map](https://lodash.com/docs/4.17.15#map) | ❌ |
| [orderBy](https://lodash.com/docs/4.17.15#orderBy) | 📝 | | [orderBy](https://lodash.com/docs/4.17.15#orderBy) | |
| [partition](https://lodash.com/docs/4.17.15#partition) | 📝 | | [partition](https://lodash.com/docs/4.17.15#partition) | 📝 |
| [reduce](https://lodash.com/docs/4.17.15#reduce) | ❌ | | [reduce](https://lodash.com/docs/4.17.15#reduce) | ❌ |
| [reduceRight](https://lodash.com/docs/4.17.15#reduceRight) | ❌ | | [reduceRight](https://lodash.com/docs/4.17.15#reduceRight) | ❌ |
@ -226,7 +226,7 @@ Even if a feature is marked "in review," it might already be under review to ens
| [isSafeInteger](https://lodash.com/docs/4.17.15#isSafeInteger) | ❌ | | [isSafeInteger](https://lodash.com/docs/4.17.15#isSafeInteger) | ❌ |
| [isSet](https://lodash.com/docs/4.17.15#isSet) | ❌ | | [isSet](https://lodash.com/docs/4.17.15#isSet) | ❌ |
| [isString](https://lodash.com/docs/4.17.15#isString) | ❌ | | [isString](https://lodash.com/docs/4.17.15#isString) | ❌ |
| [isSymbol](https://lodash.com/docs/4.17.15#isSymbol) | | | [isSymbol](https://lodash.com/docs/4.17.15#isSymbol) | |
| [isTypedArray](https://lodash.com/docs/4.17.15#isTypedArray) | ✅ | | [isTypedArray](https://lodash.com/docs/4.17.15#isTypedArray) | ✅ |
| [isUndefined](https://lodash.com/docs/4.17.15#isUndefined) | ✅ | | [isUndefined](https://lodash.com/docs/4.17.15#isUndefined) | ✅ |
| [isWeakMap](https://lodash.com/docs/4.17.15#isWeakMap) | ❌ | | [isWeakMap](https://lodash.com/docs/4.17.15#isWeakMap) | ❌ |

View File

@ -136,7 +136,7 @@ chunk([1, 2, 3, 4], 0);
| [invokeMap](https://lodash.com/docs/4.17.15#invokeMap) | ❌ | | [invokeMap](https://lodash.com/docs/4.17.15#invokeMap) | ❌ |
| [keyBy](https://lodash.com/docs/4.17.15#keyBy) | 📝 | | [keyBy](https://lodash.com/docs/4.17.15#keyBy) | 📝 |
| [map](https://lodash.com/docs/4.17.15#map) | ❌ | | [map](https://lodash.com/docs/4.17.15#map) | ❌ |
| [orderBy](https://lodash.com/docs/4.17.15#orderBy) | 📝 | | [orderBy](https://lodash.com/docs/4.17.15#orderBy) | |
| [partition](https://lodash.com/docs/4.17.15#partition) | 📝 | | [partition](https://lodash.com/docs/4.17.15#partition) | 📝 |
| [reduce](https://lodash.com/docs/4.17.15#reduce) | ❌ | | [reduce](https://lodash.com/docs/4.17.15#reduce) | ❌ |
| [reduceRight](https://lodash.com/docs/4.17.15#reduceRight) | ❌ | | [reduceRight](https://lodash.com/docs/4.17.15#reduceRight) | ❌ |
@ -227,7 +227,7 @@ chunk([1, 2, 3, 4], 0);
| [isSafeInteger](https://lodash.com/docs/4.17.15#isSafeInteger) | ❌ | | [isSafeInteger](https://lodash.com/docs/4.17.15#isSafeInteger) | ❌ |
| [isSet](https://lodash.com/docs/4.17.15#isSet) | ❌ | | [isSet](https://lodash.com/docs/4.17.15#isSet) | ❌ |
| [isString](https://lodash.com/docs/4.17.15#isString) | ❌ | | [isString](https://lodash.com/docs/4.17.15#isString) | ❌ |
| [isSymbol](https://lodash.com/docs/4.17.15#isSymbol) | | | [isSymbol](https://lodash.com/docs/4.17.15#isSymbol) | |
| [isTypedArray](https://lodash.com/docs/4.17.15#isTypedArray) | ✅ | | [isTypedArray](https://lodash.com/docs/4.17.15#isTypedArray) | ✅ |
| [isUndefined](https://lodash.com/docs/4.17.15#isUndefined) | ✅ | | [isUndefined](https://lodash.com/docs/4.17.15#isUndefined) | ✅ |
| [isWeakMap](https://lodash.com/docs/4.17.15#isWeakMap) | ❌ | | [isWeakMap](https://lodash.com/docs/4.17.15#isWeakMap) | ❌ |

View File

@ -135,7 +135,7 @@ chunk([1, 2, 3, 4], 0);
| [invokeMap](https://lodash.com/docs/4.17.15#invokeMap) | ❌ | | [invokeMap](https://lodash.com/docs/4.17.15#invokeMap) | ❌ |
| [keyBy](https://lodash.com/docs/4.17.15#keyBy) | 📝 | | [keyBy](https://lodash.com/docs/4.17.15#keyBy) | 📝 |
| [map](https://lodash.com/docs/4.17.15#map) | ❌ | | [map](https://lodash.com/docs/4.17.15#map) | ❌ |
| [orderBy](https://lodash.com/docs/4.17.15#orderBy) | 📝 | | [orderBy](https://lodash.com/docs/4.17.15#orderBy) | |
| [partition](https://lodash.com/docs/4.17.15#partition) | 📝 | | [partition](https://lodash.com/docs/4.17.15#partition) | 📝 |
| [reduce](https://lodash.com/docs/4.17.15#reduce) | ❌ | | [reduce](https://lodash.com/docs/4.17.15#reduce) | ❌ |
| [reduceRight](https://lodash.com/docs/4.17.15#reduceRight) | ❌ | | [reduceRight](https://lodash.com/docs/4.17.15#reduceRight) | ❌ |
@ -226,7 +226,7 @@ chunk([1, 2, 3, 4], 0);
| [isSafeInteger](https://lodash.com/docs/4.17.15#isSafeInteger) | ❌ | | [isSafeInteger](https://lodash.com/docs/4.17.15#isSafeInteger) | ❌ |
| [isSet](https://lodash.com/docs/4.17.15#isSet) | ❌ | | [isSet](https://lodash.com/docs/4.17.15#isSet) | ❌ |
| [isString](https://lodash.com/docs/4.17.15#isString) | ❌ | | [isString](https://lodash.com/docs/4.17.15#isString) | ❌ |
| [isSymbol](https://lodash.com/docs/4.17.15#isSymbol) | | | [isSymbol](https://lodash.com/docs/4.17.15#isSymbol) | |
| [isTypedArray](https://lodash.com/docs/4.17.15#isTypedArray) | ✅ | | [isTypedArray](https://lodash.com/docs/4.17.15#isTypedArray) | ✅ |
| [isUndefined](https://lodash.com/docs/4.17.15#isUndefined) | ✅ | | [isUndefined](https://lodash.com/docs/4.17.15#isUndefined) | ✅ |
| [isWeakMap](https://lodash.com/docs/4.17.15#isWeakMap) | ❌ | | [isWeakMap](https://lodash.com/docs/4.17.15#isWeakMap) | ❌ |

View File

@ -0,0 +1,24 @@
import { describe, it, expect } from 'vitest';
import { getPath } from './getPath';
describe('getPath function', () => {
it('should return a property name from a string', () => {
expect(getPath('a', { a: 1 })).toBe('a');
});
it('should return an array of property names from a deep path string', () => {
expect(getPath('a.b.c', { a: { b: { c: 1 } } })).toEqual(['a', 'b', 'c']);
});
it('should return an array of property names from a string array', () => {
expect(getPath(['a', 'b', 'c'], { a: { b: { c: 1 } } })).toEqual(['a', 'b', 'c']);
});
it('should return an array of property names from a string array with a nested string', () => {
expect(getPath(['a', 'b.c', 'd'], { a: { b: { c: { d: 1 } } } })).toEqual(['a', 'b', 'c', 'd']);
});
it('should return an array of property names from a deep path like string array', () => {
expect(getPath(['a.b.c', 'd'], { 'a.b.c': { d: 1 } })).toEqual(['a.b.c', 'd']);
});
});

View File

@ -0,0 +1,35 @@
import { isKey } from './isKey';
import { toPath } from './toPath';
/**
* Get the `path` (property name) from the `key` (property name or property path).
*
* @param {string | string[]} key - The `key` (property name or property path) to convert.
* @param {object} object - The object to query.
* @returns {string | string[]} The converted key (only property name).
*/
export function getPath(key: string | string[], object: object): string | string[] {
if (Array.isArray(key)) {
const path = [];
for (let i = 0; i < key.length; i++) {
const k = key[i];
if (isKey(k, object)) {
object = object[k as keyof typeof object];
path.push(k);
} else {
const keys = toPath(k);
for (let i = 0; i < keys.length; i++) {
object = object[keys[i] as keyof typeof object];
path.push(keys[i]);
}
}
}
return path;
}
return isKey(key, object) ? key : toPath(key);
}

View File

@ -0,0 +1,50 @@
import { describe, it, expect } from 'vitest';
import { isKey } from './isKey';
describe('isKey', () => {
it('should return `true` for property names', () => {
expect(isKey('a')).toBe(true);
expect(isKey(false)).toBe(true);
expect(isKey(1)).toBe(true);
expect(isKey(null)).toBe(true);
expect(isKey(undefined)).toBe(true);
expect(isKey(-1.1)).toBe(true);
expect(isKey(Symbol.iterator)).toBe(true);
});
it('should return `false` for property paths', () => {
expect(isKey('a.b')).toBe(false);
expect(isKey('a[0]')).toBe(false);
expect(isKey('a["b"]')).toBe(false);
expect(isKey("a['b']")).toBe(false);
expect(isKey('a[0].b')).toBe(false);
expect(isKey('a[0]["b"]')).toBe(false);
expect(isKey("a[0]['b']")).toBe(false);
});
it('should return `true` for property paths that are in the object', () => {
expect(isKey('a', { a: 1 })).toBe(true);
expect(isKey('a.b', { 'a.b': 2 })).toBe(true);
expect(isKey('a[0]', { 'a[0]': 3 })).toBe(true);
expect(isKey('a["b"]', { 'a["b"]': 4 })).toBe(true);
expect(isKey('a[0].b', { 'a[0].b': 5 })).toBe(true);
expect(isKey("a[0]['b']", { "a[0]['b']": 6 })).toBe(true);
});
it('should return `false` for arrays', () => {
expect(isKey([])).toBe(false);
expect(isKey([1])).toBe(false);
expect(isKey([1, 2])).toBe(false);
expect(isKey([1, 2, 3])).toBe(false);
});
it('should return true for empty string', () => {
expect(isKey('')).toBe(true);
});
it('should return true for non-word characters', () => {
expect(isKey('^')).toBe(true);
expect(isKey('!')).toBe(true);
expect(isKey('@')).toBe(true);
});
});

View File

@ -0,0 +1,34 @@
import { isSymbol } from '../predicate/isSymbol';
/** Matches any deep property path. (e.g. `a.b[0].c`)*/
const regexIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/;
/** Matches any word character (alphanumeric & underscore).*/
const regexIsPlainProp = /^\w*$/;
/**
* Checks if `value` is a property name and not a property path. (It's ok that the `value` is not in the keys of the `object`)
* @param {unknown} value The value to check.
* @param {unknown} object The object to query.
* @returns {boolean} Returns `true` if `value` is a property name, else `false`.
*
* @example
* isKey('a', { a: 1 });
* // => true
*
* isKey('a.b', { a: { b: 2 } });
* // => false
*/
export function isKey(value?: unknown, object?: unknown): boolean {
if (Array.isArray(value)) {
return false;
}
if (typeof value === 'number' || typeof value === 'boolean' || value == null || isSymbol(value)) {
return true;
}
return (
(typeof value === 'string' && (regexIsPlainProp.test(value) || !regexIsDeepProp.test(value))) ||
(object != null && Object.hasOwn(object, value as PropertyKey))
);
}

View File

@ -0,0 +1,83 @@
import { describe, expect, it } from 'vitest';
import { orderBy } from './orderBy.ts';
import { falsey } from '../_internal/falsey.ts';
describe('orderBy', () => {
const objects = [
{ a: 'x', b: 3 },
{ a: 'y', b: 4 },
{ a: 'x', b: 1 },
{ a: 'y', b: 2 },
];
const nestedObj = [
{ id: '4', address: { zipCode: 4, streetName: 'Beta' } },
{ id: '3', address: { zipCode: 3, streetName: 'Alpha' } },
{ id: '1', address: { zipCode: 1, streetName: 'Alpha' } },
{ id: '2', address: { zipCode: 2, streetName: 'Alpha' } },
{ id: '5', address: { zipCode: 4, streetName: 'Alpha' } },
];
it('should return an empty array when the collection is null or undefined', () => {
const actual = [null, undefined].map(value => orderBy(value));
const expected = [[], []];
expect(actual).toEqual(expected);
});
it('should return a shallow copy of the collection when no keys are provided', () => {
const actual = orderBy(objects);
const expected = objects.slice();
expect(actual).toEqual(expected);
});
it('should return ascending ordered collection when no orders are provided', () => {
const actual = orderBy(objects, ['a']);
const expected = orderBy(objects, ['a'], ['asc']);
expect(actual).toEqual(expected);
});
it('should sort by a single property by a specified order', () => {
const actual = orderBy(objects, 'a', 'desc');
const expected = [objects[1], objects[3], objects[0], objects[2]];
expect(actual).toEqual(expected);
});
it('should sort by nested key in array format', () => {
const actual = orderBy(nestedObj, [['address', 'zipCode'], ['address.streetName']], ['asc', 'desc']);
const expected = [nestedObj[2], nestedObj[3], nestedObj[1], nestedObj[0], nestedObj[4]];
expect(actual).toEqual(expected);
});
it('should sort by multiple properties by specified orders', () => {
const actual = orderBy(objects, ['a', 'b'], ['desc', 'asc']);
const expected = [objects[3], objects[1], objects[2], objects[0]];
expect(actual).toEqual(expected);
});
it('should sort by a property in ascending order when its order is not specified', () => {
const actual = orderBy(objects, ['a', 'b']);
const expected = [objects[2], objects[0], objects[3], objects[1]];
expect(actual).toEqual(expected);
});
it('should sort by a property in ascending order when its order is not specified and the collection is falsey', () => {
const actual = falsey.map((order, index) => orderBy(objects, ['a', 'b'], index ? ['desc', order] : ['desc']));
const expected = falsey.map(() => [objects[3], objects[1], objects[2], objects[0]]);
expect(actual).toEqual(expected);
});
it('should work with `orders` specified as string objects', () => {
const actual = orderBy(objects, ['a'], [Object('desc')]);
const expected = [objects[1], objects[3], objects[0], objects[2]];
expect(actual).toEqual(expected);
});
});

View File

@ -0,0 +1,99 @@
import { getPath } from '../_internal/getPath';
/**
* Sorts an array of objects based on multiple properties and their corresponding order directions.
*
* This function takes an array of objects, an array of keys to sort by, and an array of order directions.
* It returns the sorted array, ordering by each key according to its corresponding direction
* ('asc' for ascending or 'desc' for descending). If values for a key are equal,string
* it moves to the next key to determine the order.
*
* @template T - The type of elements in the array.
* @param {T[] | null} collection - The array of objects to be sorted.
* @param {string | Array<string | string[]>} keys - An array of keys (property names or property paths) to sort by.
* @param {unknown | unknown[]} orders - An array of order directions ('asc' for ascending or 'desc' for descending).
* @returns {T[]} - The sorted array.
*
* @example
* // Sort an array of objects by 'user' in ascending order and 'age' in descending order.
* const users = [
* { user: 'fred', age: 48 },
* { user: 'barney', age: 34 },
* { user: 'fred', age: 40 },
* { user: 'barney', age: 36 },
* ];
* const result = orderBy(users, ['user', 'age'], ['asc', 'desc']);
* // result will be:
* // [
* // { user: 'barney', age: 36 },
* // { user: 'barney', age: 34 },
* // { user: 'fred', age: 48 },
* // { user: 'fred', age: 40 },
* // ]
*/
export function orderBy<T extends object>(
collection?: T[] | null,
keys?: string | Array<string | string[]>,
orders?: unknown | unknown[]
): T[] {
if (collection == null) {
return [];
}
if (!Array.isArray(keys)) {
keys = keys == null ? [] : [keys];
}
if (!Array.isArray(orders)) {
orders = orders == null ? [] : [orders];
}
const compareValues = <V>(a: V, b: V, order: string) => {
if (a < b) {
return order === 'desc' ? 1 : -1; // Default is ascending order
}
if (a > b) {
return order === 'desc' ? -1 : 1;
}
return 0;
};
const getValueByPath = (key: string | string[], obj: T) => {
if (Array.isArray(key)) {
let value: object = obj;
for (let i = 0; i < key.length; i++) {
value = value[key[i] as keyof typeof value];
}
return value;
}
return obj[key as keyof typeof obj];
};
keys = keys.map(key => getPath(key, collection[0]));
const shallowCopiedCollection = collection.slice();
const orderedCollection = shallowCopiedCollection.sort((a, b) => {
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const valueA = getValueByPath(key, a);
const valueB = getValueByPath(key, b);
const order = String((orders as unknown[])[i]); // For Object('desc') case
const comparedResult = compareValues(valueA, valueB, order);
if (comparedResult !== 0) {
return comparedResult;
}
}
return 0;
});
return orderedCollection;
}

View File

@ -31,6 +31,7 @@ export { fill } from './array/fill.ts';
export { flatten } from './array/flatten.ts'; export { flatten } from './array/flatten.ts';
export { flattenDeep } from './array/flattenDeep.ts'; export { flattenDeep } from './array/flattenDeep.ts';
export { flattenDepth } from './array/flattenDepth.ts'; export { flattenDepth } from './array/flattenDepth.ts';
export { orderBy } from './array/orderBy.ts';
export { size } from './array/size.ts'; export { size } from './array/size.ts';
export { zipObjectDeep } from './array/zipObjectDeep.ts'; export { zipObjectDeep } from './array/zipObjectDeep.ts';
export { head as first } from '../array/head.ts'; export { head as first } from '../array/head.ts';
@ -50,6 +51,7 @@ export { isPlainObject } from './predicate/isPlainObject.ts';
export { isArray } from './predicate/isArray.ts'; export { isArray } from './predicate/isArray.ts';
export { isArguments } from './predicate/isArguments.ts'; export { isArguments } from './predicate/isArguments.ts';
export { isArrayLike } from './predicate/isArrayLike.ts'; export { isArrayLike } from './predicate/isArrayLike.ts';
export { isSymbol } from './predicate/isSymbol.ts';
export { isObjectLike } from './predicate/isObjectLike.ts'; export { isObjectLike } from './predicate/isObjectLike.ts';
export { isBoolean } from './predicate/isBoolean.ts'; export { isBoolean } from './predicate/isBoolean.ts';
export { isTypedArray } from './predicate/isTypedArray.ts'; export { isTypedArray } from './predicate/isTypedArray.ts';

View File

@ -0,0 +1,31 @@
import { describe, it, expect } from 'vitest';
import { isSymbol } from './isSymbol';
import { falsey } from '../_internal/falsey';
import { stubFalse } from '../_internal/stubFalse';
import { args } from '../_internal/args';
import { slice } from '../_internal/slice';
describe('isSymbol', () => {
it('should return `true` for symbols', () => {
expect(isSymbol(Symbol('a'))).toBe(true);
expect(isSymbol(Object(Symbol('a')))).toBe(true);
});
it('should return `false` for non-symbols', () => {
const expected = falsey.map(stubFalse);
const actual = falsey.map(isSymbol);
expect(actual).toEqual(expected);
expect(isSymbol(args)).toBe(false);
expect(isSymbol([1, 2, 3])).toBe(false);
expect(isSymbol(true)).toBe(false);
expect(isSymbol(new Date())).toBe(false);
expect(isSymbol(new Error())).toBe(false);
expect(isSymbol(slice)).toBe(false);
expect(isSymbol({ 0: 1, length: 1 })).toBe(false);
expect(isSymbol(1)).toBe(false);
expect(isSymbol(/x/)).toBe(false);
expect(isSymbol('a')).toBe(false);
});
});

View File

@ -0,0 +1,17 @@
/**
* Check whether a value is a symbol.
*
* This function can also serve as a type predicate in TypeScript, narrowing the type of the argument to `symbol`.
*
* @param {unknown} value The value to check.
* @returns {value is symbol} Returns `true` if `value` is a symbol, else `false`.
* @example
* isSymbol(Symbol.iterator);
* // => true
*
* isSymbol('abc');
* // => false
*/
export function isSymbol(value?: unknown): value is symbol {
return typeof value === 'symbol' || (value != null && value instanceof Symbol);
}