UNPKG

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
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) }