Skip to content

Commit

Permalink
Merge branch 'master' into fix_npm_packaging
Browse files Browse the repository at this point in the history
  • Loading branch information
wjhsf authored Feb 29, 2024
2 parents 7af9431 + 1b6db59 commit d9187a0
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 5 deletions.
29 changes: 29 additions & 0 deletions lib/__tests__/util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { safeToString } from '../utils'

describe('safeToString', () => {
const recursiveArray: unknown[] = [1]
recursiveArray.push([[recursiveArray], 2, [[recursiveArray]]], 3)
const testCases = [
[undefined, 'undefined'],
[null, 'null'],
[true, 'true'],
['string', 'string'],
[123, '123'],
[321n, '321'],
[{ object: 'yes' }, '[object Object]'],
[(a: number, b: number) => a + b, '(a, b) => a + b'],
[Symbol('safeToString'), 'Symbol(safeToString)'],
[Object.create(null), '[object Object]'],
// eslint-disable-next-line no-sparse-arrays
[[1, 'hello', , undefined, , true, null], '1,hello,,,,true,'],
[
[Object.create(null), Symbol('safeToString')],
'[object Object],Symbol(safeToString)',
],
[recursiveArray, '1,,2,,3'],
]

it.each(testCases)('works on %s', (input, output) => {
expect(safeToString(input)).toBe(String(output))
})
})
37 changes: 32 additions & 5 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,44 @@ export interface ErrorCallback {
export const objectToString = (obj: unknown) =>
Object.prototype.toString.call(obj)

/** Safely converts any value to string, using the value's own `toString` when available. */
export const safeToString = (val: unknown) => {
// Ideally, we'd just use String() for everything, but it breaks if `toString` is missing (mostly
// values with no prototype), so we have to use Object#toString as a fallback.
/**
* Converts an array to string, safely handling symbols, null prototype objects, and recursive arrays.
*/
const safeArrayToString = (
arr: unknown[],
seenArrays: WeakSet<object>,
): string => {
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toString#description
if (typeof arr.join !== 'function') return objectToString(arr)
seenArrays.add(arr)
const mapped = arr.map((val) =>
val === null || val === undefined || seenArrays.has(val)
? ''
: safeToStringImpl(val, seenArrays),
)
return mapped.join()
}

const safeToStringImpl = (
val: unknown,
seenArrays?: WeakSet<object>,
): string => {
// Using .toString() fails for null/undefined and implicit conversion (val + "") fails for symbols
// and objects with null prototype
if (val === undefined || val === null || typeof val.toString === 'function') {
return String(val)
return Array.isArray(val)
? // Arrays have a weird custom toString that we need to replicate
safeArrayToString(val, seenArrays ?? new WeakSet())
: String(val)
} else {
// This case should just be objects with null prototype, so we can just use Object#toString
return objectToString(val)
}
}

/** Safely converts any value to string, using the value's own `toString` when available. */
export const safeToString = (val: unknown) => safeToStringImpl(val)

/** Utility object for promise/callback interop. */
export interface PromiseCallback<T> {
promise: Promise<T>
Expand Down

0 comments on commit d9187a0

Please sign in to comment.