n2words
Version:
Convert numbers to words in 70+ languages with zero dependencies. Supports BigInt, decimals, and browser/Node.js environments.
57 lines (54 loc) • 3.06 kB
JavaScript
import { isPlainObject } from './is-plain-object.js'
/**
* Apply a form's option defaults and reject anything it doesn't declare.
*
* The exported defaults map (e.g. `cardinalDefaults`) is the single source of
* truth for each option's default value — the form never restates it, and the
* docs generator imports it rather than scraping the body. A malformed options
* argument (unknown key, wrong-typed value, inherited key) throws `TypeError`;
* a known option with a value outside its declared allowed set (an exported
* `<form>Values` map, e.g. gender) throws `RangeError` — right kind of value,
* out of range — instead of silently falling back to the default.
* @template {object} T
* @param {T | undefined} options - Caller-provided options, or undefined
* @param {Required<T>} defaults - The form's default for every option
* @param {Partial<Record<keyof T & string, readonly unknown[]>>} [values] - Allowed set per enum-valued option
* @returns {Required<T>} The options with every default applied
*/
export function resolveOptions(options, defaults, values) {
/** @type {Record<string, unknown>} */
const resolved = { ...defaults }
if (options === undefined) return /** @type {Required<T>} */ (resolved)
if (!isPlainObject(options)) {
throw new TypeError(`Invalid options: expected plain object or undefined, got ${typeof options}`)
}
const allowed = Object.keys(defaults)
for (const [key, value] of Object.entries(options)) {
// Own-property check: inherited keys (__proto__, constructor, …) are not
// options. Using `key in defaults` here would let them past the guard and
// into `resolved[key] = value` — a prototype-pollution vector. An unknown
// key is a malformed-argument (shape) error, hence TypeError — RangeError is
// reserved for a value outside an allowed range/set (see checkMax).
if (!Object.hasOwn(defaults, key)) {
throw new TypeError(`Unknown option "${key}" — expected one of: ${allowed.join(', ')}`)
}
// `{ key: undefined }` means "use the default": `key?: T` is `T | undefined`
// without exactOptionalPropertyTypes, and the old destructuring defaults
// treated undefined the same way. Omit it rather than reject it as a type.
if (value === undefined) continue
if (typeof value !== typeof resolved[key]) {
throw new TypeError(`Option "${key}" must be a ${typeof resolved[key]}, got ${typeof value}`)
}
if (values !== undefined && Object.hasOwn(values, key)) {
const set = /** @type {Record<string, readonly unknown[]>} */ (values)[key]
if (!set.includes(value)) {
// The received value is caller-supplied and arbitrary — stringify it so
// quotes/newlines can't garble the message. The allowed set is our own
// gate-verified declaration and reads cleaner unquoted.
throw new RangeError(`Option "${key}" must be one of: ${set.join(', ')} — got ${JSON.stringify(value)}`)
}
}
resolved[key] = value
}
return /** @type {Required<T>} */ (resolved)
}