From 74fbd0c005f26f341ee6a5a115d9ee9fe075f255 Mon Sep 17 00:00:00 2001 From: robonen Date: Mon, 8 Jun 2026 15:50:59 +0700 Subject: [PATCH] feat(stdlib): add immutable array ops, deep set, and isEqual comparator Adds arrays/{move,insert,swap,remove}, collections/set, and comparators/isEqual (NaN/Date/RegExp/Map/Set/cycle-safe), wired into the barrels. --- core/stdlib/src/arrays/index.ts | 4 + core/stdlib/src/arrays/insert/index.test.ts | 30 ++++++ core/stdlib/src/arrays/insert/index.ts | 24 +++++ core/stdlib/src/arrays/move/index.test.ts | 29 ++++++ core/stdlib/src/arrays/move/index.ts | 28 ++++++ core/stdlib/src/arrays/remove/index.test.ts | 26 +++++ core/stdlib/src/arrays/remove/index.ts | 25 +++++ core/stdlib/src/arrays/swap/index.test.ts | 26 +++++ core/stdlib/src/arrays/swap/index.ts | 29 ++++++ core/stdlib/src/collections/index.ts | 1 + core/stdlib/src/collections/set/index.test.ts | 47 +++++++++ core/stdlib/src/collections/set/index.ts | 47 +++++++++ core/stdlib/src/comparators/index.ts | 1 + .../src/comparators/isEqual/index.test.ts | 57 +++++++++++ core/stdlib/src/comparators/isEqual/index.ts | 95 +++++++++++++++++++ core/stdlib/src/index.ts | 1 + .../src/text/levenshtein-distance/index.ts | 2 +- core/stdlib/src/text/template/index.ts | 6 +- core/stdlib/src/types/js/complex.test.ts | 2 + core/stdlib/src/types/js/primitives.test.ts | 2 +- core/stdlib/src/types/js/primitives.ts | 2 +- core/stdlib/src/types/ts/string.test-d.ts | 2 +- 22 files changed, 479 insertions(+), 7 deletions(-) create mode 100644 core/stdlib/src/arrays/insert/index.test.ts create mode 100644 core/stdlib/src/arrays/insert/index.ts create mode 100644 core/stdlib/src/arrays/move/index.test.ts create mode 100644 core/stdlib/src/arrays/move/index.ts create mode 100644 core/stdlib/src/arrays/remove/index.test.ts create mode 100644 core/stdlib/src/arrays/remove/index.ts create mode 100644 core/stdlib/src/arrays/swap/index.test.ts create mode 100644 core/stdlib/src/arrays/swap/index.ts create mode 100644 core/stdlib/src/collections/set/index.test.ts create mode 100644 core/stdlib/src/collections/set/index.ts create mode 100644 core/stdlib/src/comparators/index.ts create mode 100644 core/stdlib/src/comparators/isEqual/index.test.ts create mode 100644 core/stdlib/src/comparators/isEqual/index.ts diff --git a/core/stdlib/src/arrays/index.ts b/core/stdlib/src/arrays/index.ts index 9f7f897..efe6439 100644 --- a/core/stdlib/src/arrays/index.ts +++ b/core/stdlib/src/arrays/index.ts @@ -1,10 +1,14 @@ export * from './cluster'; export * from './first'; export * from './groupBy'; +export * from './insert'; export * from './last'; +export * from './move'; export * from './partition'; export * from './range'; +export * from './remove'; export * from './sum'; +export * from './swap'; export * from './toArray'; export * from './unique'; export * from './zip'; diff --git a/core/stdlib/src/arrays/insert/index.test.ts b/core/stdlib/src/arrays/insert/index.test.ts new file mode 100644 index 0000000..e4fe048 --- /dev/null +++ b/core/stdlib/src/arrays/insert/index.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { insert } from '.'; + +describe('insert', () => { + it('insert a single item', () => { + expect(insert(['a', 'c'], 1, 'b')).toEqual(['a', 'b', 'c']); + }); + + it('insert multiple items', () => { + expect(insert(['a', 'd'], 1, 'b', 'c')).toEqual(['a', 'b', 'c', 'd']); + }); + + it('prepend at index 0', () => { + expect(insert(['b', 'c'], 0, 'a')).toEqual(['a', 'b', 'c']); + }); + + it('append when the index is too large', () => { + expect(insert(['a'], 99, 'b', 'c')).toEqual(['a', 'b', 'c']); + }); + + it('clamp a negative index to 0', () => { + expect(insert(['b'], -5, 'a')).toEqual(['a', 'b']); + }); + + it('never mutate the source', () => { + const source = ['a', 'b']; + insert(source, 1, 'x'); + expect(source).toEqual(['a', 'b']); + }); +}); diff --git a/core/stdlib/src/arrays/insert/index.ts b/core/stdlib/src/arrays/insert/index.ts new file mode 100644 index 0000000..2f98fb8 --- /dev/null +++ b/core/stdlib/src/arrays/insert/index.ts @@ -0,0 +1,24 @@ +/** + * @name insert + * @category Arrays + * @description Return a new array with `items` inserted at `index`. The index is + * clamped into `[0, length]`, so a too-large index appends. Never mutates. + * + * @param {readonly T[]} array - The source array + * @param {number} index - Position to insert at + * @param {...T} items - Items to insert + * @returns {T[]} A new array with the items inserted + * + * @example + * insert(['a', 'c'], 1, 'b'); // ['a', 'b', 'c'] + * insert(['a'], 99, 'b', 'c'); // ['a', 'b', 'c'] + * + * @since 0.0.10 + */ +export function insert(array: readonly T[], index: number, ...items: T[]): T[] { + const result = array.slice(); + const target = Math.max(0, Math.min(index, result.length)); + result.splice(target, 0, ...items); + + return result; +} diff --git a/core/stdlib/src/arrays/move/index.test.ts b/core/stdlib/src/arrays/move/index.test.ts new file mode 100644 index 0000000..fff3d83 --- /dev/null +++ b/core/stdlib/src/arrays/move/index.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; +import { move } from '.'; + +describe('move', () => { + it('move an item forward', () => { + expect(move(['a', 'b', 'c'], 0, 2)).toEqual(['b', 'c', 'a']); + }); + + it('move an item backward', () => { + expect(move(['a', 'b', 'c'], 2, 0)).toEqual(['c', 'a', 'b']); + }); + + it('clamp the target index', () => { + expect(move(['a', 'b', 'c'], 0, 99)).toEqual(['b', 'c', 'a']); + }); + + it('return a copy unchanged for an out-of-range source', () => { + const source = ['a', 'b']; + const result = move(source, 5, 0); + expect(result).toEqual(['a', 'b']); + expect(result).not.toBe(source); + }); + + it('never mutate the source', () => { + const source = ['a', 'b', 'c']; + move(source, 0, 2); + expect(source).toEqual(['a', 'b', 'c']); + }); +}); diff --git a/core/stdlib/src/arrays/move/index.ts b/core/stdlib/src/arrays/move/index.ts new file mode 100644 index 0000000..189cdf8 --- /dev/null +++ b/core/stdlib/src/arrays/move/index.ts @@ -0,0 +1,28 @@ +/** + * @name move + * @category Arrays + * @description Return a new array with the item at `from` moved to `to`. Out-of-range + * `from` returns a shallow copy unchanged; `to` is clamped into range. Never mutates. + * + * @param {readonly T[]} array - The source array + * @param {number} from - Index to move from + * @param {number} to - Index to move to + * @returns {T[]} A new array with the item moved + * + * @example + * move(['a', 'b', 'c'], 0, 2); // ['b', 'c', 'a'] + * + * @since 0.0.10 + */ +export function move(array: readonly T[], from: number, to: number): T[] { + const result = array.slice(); + + if (from < 0 || from >= result.length) + return result; + + const item = result.splice(from, 1)[0] as T; + const target = Math.max(0, Math.min(to, result.length)); + result.splice(target, 0, item); + + return result; +} diff --git a/core/stdlib/src/arrays/remove/index.test.ts b/core/stdlib/src/arrays/remove/index.test.ts new file mode 100644 index 0000000..4fa4b5e --- /dev/null +++ b/core/stdlib/src/arrays/remove/index.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; +import { remove } from '.'; + +describe('remove', () => { + it('remove an item by index', () => { + expect(remove(['a', 'b', 'c'], 1)).toEqual(['a', 'c']); + }); + + it('remove the first and last items', () => { + expect(remove(['a', 'b', 'c'], 0)).toEqual(['b', 'c']); + expect(remove(['a', 'b', 'c'], 2)).toEqual(['a', 'b']); + }); + + it('return a copy unchanged for an out-of-range index', () => { + const source = ['a', 'b']; + const result = remove(source, 9); + expect(result).toEqual(['a', 'b']); + expect(result).not.toBe(source); + }); + + it('never mutate the source', () => { + const source = ['a', 'b', 'c']; + remove(source, 1); + expect(source).toEqual(['a', 'b', 'c']); + }); +}); diff --git a/core/stdlib/src/arrays/remove/index.ts b/core/stdlib/src/arrays/remove/index.ts new file mode 100644 index 0000000..0aba8c0 --- /dev/null +++ b/core/stdlib/src/arrays/remove/index.ts @@ -0,0 +1,25 @@ +/** + * @name remove + * @category Arrays + * @description Return a new array with the item at `index` removed. Returns a shallow + * copy unchanged when the index is out of range. Never mutates. + * + * @param {readonly T[]} array - The source array + * @param {number} index - Index of the item to remove + * @returns {T[]} A new array without the removed item + * + * @example + * remove(['a', 'b', 'c'], 1); // ['a', 'c'] + * + * @since 0.0.10 + */ +export function remove(array: readonly T[], index: number): T[] { + const result = array.slice(); + + if (index < 0 || index >= result.length) + return result; + + result.splice(index, 1); + + return result; +} diff --git a/core/stdlib/src/arrays/swap/index.test.ts b/core/stdlib/src/arrays/swap/index.test.ts new file mode 100644 index 0000000..798a16f --- /dev/null +++ b/core/stdlib/src/arrays/swap/index.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; +import { swap } from '.'; + +describe('swap', () => { + it('swap two items', () => { + expect(swap(['a', 'b', 'c'], 0, 2)).toEqual(['c', 'b', 'a']); + }); + + it('return a copy unchanged when indices are equal', () => { + const source = ['a', 'b']; + const result = swap(source, 1, 1); + expect(result).toEqual(['a', 'b']); + expect(result).not.toBe(source); + }); + + it('return a copy unchanged for out-of-range indices', () => { + expect(swap(['a', 'b'], 0, 9)).toEqual(['a', 'b']); + expect(swap(['a', 'b'], -1, 1)).toEqual(['a', 'b']); + }); + + it('never mutate the source', () => { + const source = ['a', 'b', 'c']; + swap(source, 0, 2); + expect(source).toEqual(['a', 'b', 'c']); + }); +}); diff --git a/core/stdlib/src/arrays/swap/index.ts b/core/stdlib/src/arrays/swap/index.ts new file mode 100644 index 0000000..7fc4074 --- /dev/null +++ b/core/stdlib/src/arrays/swap/index.ts @@ -0,0 +1,29 @@ +/** + * @name swap + * @category Arrays + * @description Return a new array with the items at indices `a` and `b` swapped. + * Returns a shallow copy unchanged when either index is out of range or equal. + * Never mutates. + * + * @param {readonly T[]} array - The source array + * @param {number} a - First index + * @param {number} b - Second index + * @returns {T[]} A new array with the two items swapped + * + * @example + * swap(['a', 'b', 'c'], 0, 2); // ['c', 'b', 'a'] + * + * @since 0.0.10 + */ +export function swap(array: readonly T[], a: number, b: number): T[] { + const result = array.slice(); + + if (a < 0 || b < 0 || a >= result.length || b >= result.length || a === b) + return result; + + const temp = result[a] as T; + result[a] = result[b] as T; + result[b] = temp; + + return result; +} diff --git a/core/stdlib/src/collections/index.ts b/core/stdlib/src/collections/index.ts index a34bde4..b990690 100644 --- a/core/stdlib/src/collections/index.ts +++ b/core/stdlib/src/collections/index.ts @@ -1 +1,2 @@ export * from './get'; +export * from './set'; diff --git a/core/stdlib/src/collections/set/index.test.ts b/core/stdlib/src/collections/set/index.test.ts new file mode 100644 index 0000000..766a27c --- /dev/null +++ b/core/stdlib/src/collections/set/index.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import { set } from '.'; + +describe('set', () => { + it('set a top-level property', () => { + expect(set({ name: 'John' }, 'name', 'Jane')).toEqual({ name: 'Jane' }); + }); + + it('set a nested object property', () => { + expect(set({ user: { name: 'John' } }, 'user.name', 'Jane')).toEqual({ user: { name: 'Jane' } }); + }); + + it('set through an array index', () => { + expect(set({ items: [{ id: 1 }, { id: 2 }] }, 'items.1.id', 9)).toEqual({ items: [{ id: 1 }, { id: 9 }] }); + }); + + it('create missing object intermediates', () => { + expect(set({}, 'a.b.c', 42)).toEqual({ a: { b: { c: 42 } } }); + }); + + it('create an array when the next segment is numeric', () => { + const result = set({} as Record, 'items.0.id', 1); + expect(Array.isArray((result as any).items)).toBe(true); + expect(result).toEqual({ items: [{ id: 1 }] }); + }); + + it('overwrite a non-object intermediate', () => { + expect(set({ a: 1 } as Record, 'a.b', 2)).toEqual({ a: { b: 2 } }); + }); + + it('mutate and return the same reference', () => { + const source = { a: 1 }; + expect(set(source, 'a', 2)).toBe(source); + expect(source.a).toBe(2); + }); + + it('preserve falsy values', () => { + expect(set({}, 'count', 0)).toEqual({ count: 0 }); + expect(set({}, 'flag', false)).toEqual({ flag: false }); + }); + + it('return the object unchanged for an empty path', () => { + const source = { a: 1 }; + expect(set(source, '', 2)).toBe(source); + expect(source).toEqual({ a: 1 }); + }); +}); diff --git a/core/stdlib/src/collections/set/index.ts b/core/stdlib/src/collections/set/index.ts new file mode 100644 index 0000000..d2a7f61 --- /dev/null +++ b/core/stdlib/src/collections/set/index.ts @@ -0,0 +1,47 @@ +import type { Collection } from '../../types'; + +// Hoisted so it is compiled once rather than re-created on each `set` call. +const NUMERIC_SEGMENT = /^\d+$/; + +/** + * @name set + * @category Collections + * @description Write a deeply nested value into a collection by a dot-separated path, + * creating any missing intermediate containers along the way. A numeric next segment + * creates an array, otherwise an object. Mutates and returns the original collection. + * + * @param {Collection} obj - The target object or array (mutated in place) + * @param {string} path - Dot-separated path, e.g. `'user.addresses.0.street'` + * @param {unknown} value - The value to assign at the path + * @returns {Collection} The same `obj`, for chaining + * + * @example + * set({ user: { name: 'John' } }, 'user.name', 'Jane'); // { user: { name: 'Jane' } } + * set({}, 'items.0.id', 1); // { items: [{ id: 1 }] } + * + * @since 0.0.10 + */ +export function set(obj: O, path: string, value: unknown): O { + if (path === '') + return obj; + + const keys = path.split('.'); + const lastKey = keys[keys.length - 1]!; + let current: any = obj; + + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]!; + const next = current[key]; + + // Create the missing intermediate: an array when the next segment is a numeric + // index, otherwise a plain object. + if (next === null || typeof next !== 'object') + current[key] = NUMERIC_SEGMENT.test(keys[i + 1]!) ? [] : {}; + + current = current[key]; + } + + current[lastKey] = value; + + return obj; +} diff --git a/core/stdlib/src/comparators/index.ts b/core/stdlib/src/comparators/index.ts new file mode 100644 index 0000000..016dfbe --- /dev/null +++ b/core/stdlib/src/comparators/index.ts @@ -0,0 +1 @@ +export * from './isEqual'; diff --git a/core/stdlib/src/comparators/isEqual/index.test.ts b/core/stdlib/src/comparators/isEqual/index.test.ts new file mode 100644 index 0000000..a03d343 --- /dev/null +++ b/core/stdlib/src/comparators/isEqual/index.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest'; +import { isEqual } from '.'; + +describe('isEqual', () => { + it('compare primitives', () => { + expect(isEqual(1, 1)).toBe(true); + expect(isEqual('a', 'a')).toBe(true); + expect(isEqual(1, 2)).toBe(false); + expect(isEqual(1, '1')).toBe(false); + expect(isEqual(null, null)).toBe(true); + expect(isEqual(null, undefined)).toBe(false); + }); + + it('treat NaN as equal to NaN', () => { + expect(isEqual(Number.NaN, Number.NaN)).toBe(true); + expect(isEqual(Number.NaN, 1)).toBe(false); + }); + + it('compare arrays deeply', () => { + expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true); + expect(isEqual([1, [2, 3]], [1, [2, 3]])).toBe(true); + expect(isEqual([1, 2], [1, 2, 3])).toBe(false); + expect(isEqual([1, { a: 2 }], [1, { a: 3 }])).toBe(false); + }); + + it('compare plain objects deeply', () => { + expect(isEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } })).toBe(true); + expect(isEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false); + expect(isEqual({ a: 1 }, { a: 2 })).toBe(false); + }); + + it('compare dates and regexps', () => { + expect(isEqual(new Date(0), new Date(0))).toBe(true); + expect(isEqual(new Date(0), new Date(1))).toBe(false); + expect(isEqual(/a/gi, /a/gi)).toBe(true); + expect(isEqual(/a/g, /a/i)).toBe(false); + }); + + it('compare Map and Set', () => { + expect(isEqual(new Map([['a', 1]]), new Map([['a', 1]]))).toBe(true); + expect(isEqual(new Map([['a', 1]]), new Map([['a', 2]]))).toBe(false); + expect(isEqual(new Set([1, 2]), new Set([1, 2]))).toBe(true); + expect(isEqual(new Set([1, 2]), new Set([1, 3]))).toBe(false); + }); + + it('distinguish arrays from objects', () => { + expect(isEqual([], {})).toBe(false); + }); + + it('handle circular references', () => { + const a: any = { name: 'a' }; + a.self = a; + const b: any = { name: 'a' }; + b.self = b; + expect(isEqual(a, b)).toBe(true); + }); +}); diff --git a/core/stdlib/src/comparators/isEqual/index.ts b/core/stdlib/src/comparators/isEqual/index.ts new file mode 100644 index 0000000..3d9256f --- /dev/null +++ b/core/stdlib/src/comparators/isEqual/index.ts @@ -0,0 +1,95 @@ +function equals(a: any, b: any, seen: WeakMap): boolean { + if (a === b) + return true; + + // NaN is the only value not equal to itself; treat NaN === NaN. + if (typeof a === 'number' && typeof b === 'number') + return Number.isNaN(a) && Number.isNaN(b); + + if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') + return false; + + // Cycle guard: if we have already paired `a`, it must pair with the same `b`. + const paired = seen.get(a); + if (paired !== undefined) + return paired === b; + seen.set(a, b); + + if (a instanceof Date || b instanceof Date) + return a instanceof Date && b instanceof Date && a.getTime() === b.getTime(); + + if (a instanceof RegExp || b instanceof RegExp) + return a instanceof RegExp && b instanceof RegExp && a.source === b.source && a.flags === b.flags; + + const aIsArray = Array.isArray(a); + const bIsArray = Array.isArray(b); + if (aIsArray || bIsArray) { + if (!aIsArray || !bIsArray || a.length !== b.length) + return false; + + for (let i = 0; i < a.length; i++) { + if (!equals(a[i], b[i], seen)) + return false; + } + + return true; + } + + if (a instanceof Map || b instanceof Map) { + if (!(a instanceof Map) || !(b instanceof Map) || a.size !== b.size) + return false; + + for (const [key, value] of a) { + if (!b.has(key) || !equals(value, b.get(key), seen)) + return false; + } + + return true; + } + + if (a instanceof Set || b instanceof Set) { + if (!(a instanceof Set) || !(b instanceof Set) || a.size !== b.size) + return false; + + for (const value of a) { + if (!b.has(value)) + return false; + } + + return true; + } + + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + if (aKeys.length !== bKeys.length) + return false; + + for (const key of aKeys) { + if (!Object.prototype.hasOwnProperty.call(b, key) || !equals(a[key], b[key], seen)) + return false; + } + + return true; +} + +/** + * @name isEqual + * @category Comparators + * @description Deep structural equality between two values. Handles primitives + * (NaN-aware), `Date`, `RegExp`, arrays, `Map`, `Set`, and plain objects, and is + * safe against circular references. `Set` membership is compared shallowly. + * + * @param {unknown} a - The first value + * @param {unknown} b - The second value + * @returns {boolean} `true` if the values are deeply equal + * + * @example + * isEqual({ a: [1, 2] }, { a: [1, 2] }); // true + * isEqual([1, { b: 2 }], [1, { b: 3 }]); // false + * isEqual(Number.NaN, Number.NaN); // true + * + * @since 0.0.10 + */ +export function isEqual(a: unknown, b: unknown): boolean { + return equals(a, b, new WeakMap()); +} diff --git a/core/stdlib/src/index.ts b/core/stdlib/src/index.ts index 6ee821e..db18442 100644 --- a/core/stdlib/src/index.ts +++ b/core/stdlib/src/index.ts @@ -2,6 +2,7 @@ export * from './arrays'; export * from './async'; export * from './bits'; export * from './collections'; +export * from './comparators'; export * from './functions'; export * from './math'; export * from './objects'; diff --git a/core/stdlib/src/text/levenshtein-distance/index.ts b/core/stdlib/src/text/levenshtein-distance/index.ts index 4176c7d..1ab504a 100644 --- a/core/stdlib/src/text/levenshtein-distance/index.ts +++ b/core/stdlib/src/text/levenshtein-distance/index.ts @@ -31,7 +31,7 @@ export function levenshteinDistance(left: string, right: string): number { for (let j = 1; j <= innerLength; j++) { const cost = outerChar === inner[j - 1] ? 0 : 1; - current[j]! = Math.min( + current[j] = Math.min( prev[j]! + 1, // insertion current[j - 1]! + 1, // deletion prev[j - 1]! + cost, // substitution diff --git a/core/stdlib/src/text/template/index.ts b/core/stdlib/src/text/template/index.ts index 1ee2584..827a365 100644 --- a/core/stdlib/src/text/template/index.ts +++ b/core/stdlib/src/text/template/index.ts @@ -15,7 +15,7 @@ export type TemplateFallback = string | ((key: string) => string); /** * Type of a template string with placeholders. */ -const TEMPLATE_PLACEHOLDER = /\{\s*([^{}]+?)\s*\}/gm; +const TEMPLATE_PLACEHOLDER = /\{([^{}]+)\}/g; /** * Removes the placeholder syntax from a template string. @@ -82,8 +82,8 @@ export function templateObject< T extends string, A extends GenerateTypes, TemplateValue> & Collection, >(template: T, args: A, fallback?: TemplateFallback): string { - return template.replace(TEMPLATE_PLACEHOLDER, (_match, key: string) => { - const value = get(args, key); + return template.replaceAll(TEMPLATE_PLACEHOLDER, (_match, key: string) => { + const value = get(args, key.trim()); if (value !== null && value !== undefined) return String(value); diff --git a/core/stdlib/src/types/js/complex.test.ts b/core/stdlib/src/types/js/complex.test.ts index fa27245..9e70f26 100644 --- a/core/stdlib/src/types/js/complex.test.ts +++ b/core/stdlib/src/types/js/complex.test.ts @@ -38,6 +38,7 @@ describe('complex', () => { }); it('true for class instances and null-prototype objects', () => { + // eslint-disable-next-line @typescript-eslint/no-extraneous-class -- fixture for the instance check class Foo {} expect(isObject(new Foo())).toBe(true); @@ -48,6 +49,7 @@ describe('complex', () => { describe('isRegExp', () => { it('true if the value is a regexp', () => { expect(isRegExp(/test/)).toBe(true); + // eslint-disable-next-line prefer-regex-literals -- intentionally testing the constructor form expect(isRegExp(new RegExp('test'))).toBe(true); }); diff --git a/core/stdlib/src/types/js/primitives.test.ts b/core/stdlib/src/types/js/primitives.test.ts index 976daa3..25318a2 100644 --- a/core/stdlib/src/types/js/primitives.test.ts +++ b/core/stdlib/src/types/js/primitives.test.ts @@ -72,7 +72,7 @@ describe('primitives', () => { describe('isSymbol', () => { it('true if the value is a symbol', () => { - expect(isSymbol(Symbol())).toBe(true); + expect(isSymbol(Symbol('test'))).toBe(true); }); it('false if the value is not a symbol', () => { diff --git a/core/stdlib/src/types/js/primitives.ts b/core/stdlib/src/types/js/primitives.ts index a169202..8e585e0 100644 --- a/core/stdlib/src/types/js/primitives.ts +++ b/core/stdlib/src/types/js/primitives.ts @@ -80,7 +80,7 @@ export const isSymbol = (value: any): value is symbol => typeof value === 'symbo * * @since 0.0.2 */ -export const isUndefined = (value: any): value is undefined => typeof value === 'undefined'; +export const isUndefined = (value: any): value is undefined => value === undefined; /** * @name isNull diff --git a/core/stdlib/src/types/ts/string.test-d.ts b/core/stdlib/src/types/ts/string.test-d.ts index 2a57d4f..b2bea16 100644 --- a/core/stdlib/src/types/ts/string.test-d.ts +++ b/core/stdlib/src/types/ts/string.test-d.ts @@ -6,7 +6,7 @@ describe('string', () => { it('should be a string', () => { expectTypeOf(Number(1)).toExtend(); expectTypeOf(String(1)).toExtend(); - expectTypeOf(Symbol()).toExtend(); + expectTypeOf(Symbol('test')).toExtend(); expectTypeOf([1]).toExtend(); expectTypeOf(new Object()).toExtend(); expectTypeOf(new Date()).toExtend();