From 511942a774bf49050d2c95baae82acc1de16eb68 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Wed, 3 Oct 2018 13:01:02 -0700 Subject: [PATCH 1/3] Update to TS@next and fix resulting type errors Also simplify and modify type aliases where necessary. Note that I haven't fixed any implicit any errors. I also kept the balance between d.ts and jsdoc about the same. In lib/formats.ts, stricter call is unable to handle the multiple overloads of String.prototype.replace, so I had to manually type it to the one I wanted. In lib/q.d.ts, ObjectCoercible and ObjectIndexable were effectively the same, so I removed ObjectIndexable. Also, `{}` subsumes primitive, so I changed it to `object`: ```ts type ObjectCoercible = object | NonNullishPrimitive; ``` I changed some similar types that were almost right: `Concatable` needs to be `Concatable<{}>` because of the way it's used in utility.compact, and `object | Primitive` is the same as `unknown`, so I changed `Value` to `Concatable`. In lib/utils.js, I had to edit `merge` in a few places. At least a couple of the edits looked like they were fixing potential bugs, but I had trouble visualising the execution of `merge` so I can't be sure. 1. When `typeof source !== "object"`, it could still be `"boolean"`, but booleans can't be used to index into Object.prototype, so I added an additional guard. 2. `typeof target !== "object"` is always false, so I deleted the branch that used it; the code inside was a type error because the compiler thinks it can't be reached. I couldn't convince myself that it was wrong. 3. Narrowing by element access failed, so I had to create a `const tmp = target[i]` before checking that `typeof tmp === "object"`. 4. In `compact`, and I had to add a type annotation to `queue` and then a cast when pushing into it. The former isn't that surprising, considering that it's supposed to have a complex type that we can't infer. The latter is a result of checks that prove that the object structure has a recursive level that needs to be compacted, but the compiler can't follow the checks and needs to be told explicitly about the type. In tsconfig.json, I had to manually set `"target": "esnext"` to avoid some bogus errors. Breakdown of the changes: * 4 cases where the compiler failed and needed a workaround * 2 possible bugs * 3 cases where the type system is full of confusing, similar types --- lib/formats.js | 1 + lib/qs.d.ts | 15 ++++++++------- lib/utils.js | 14 ++++++-------- tsconfig.json | 1 + 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/formats.js b/lib/formats.js index d05cc15f..fe97aa0d 100644 --- a/lib/formats.js +++ b/lib/formats.js @@ -1,5 +1,6 @@ 'use strict'; +/** @type {function(string | RegExp, string): string} */ var replace = String.prototype.replace; var percentTwenties = /%20/g; diff --git a/lib/qs.d.ts b/lib/qs.d.ts index 0f12482c..8fe8c5d5 100644 --- a/lib/qs.d.ts +++ b/lib/qs.d.ts @@ -54,16 +54,17 @@ export type UtilOptionsInternal = UtilOptions & { type Nullish = null | undefined; type NonNullishPrimitive = boolean | number | string | symbol; -type ObjectCoercible = {} | NonNullishPrimitive; -type ObjectIndexable = { [key: string]: Value } | NonNullishPrimitive; +type ObjectCoercible = object | NonNullishPrimitive; type Primitive = Nullish | NonNullishPrimitive; type Concatable = T | T[]; -type Value = Concatable; +type Value = Concatable; +// TODO: type NonPrimitive = object; for use in JSDoc + export type arrayToObject = (source: Value[], options?: UtilOptionsInternal) => object; export type assign = (target: object, source: object) => /* target */ object; type queueObject = { - [key: string]: Concatable, + [key: string]: Concatable<{}>, }; type queueItem = { obj: queueObject, @@ -74,7 +75,7 @@ export type compact = (value: ObjectCoercible) => ObjectCoercible; export type isBuffer = (obj: (Buffer | Value) & ({ constructor?: typeof Buffer })) => boolean; export type isRegExp = (obj: RegExp | Value) => boolean; export type merge = ( - target: ObjectIndexable[] | Object, + target: ObjectCoercible[] | object, source?: ObjectCoercible, options?: UtilOptionsInternal, ) => /* target | */ object; @@ -99,7 +100,7 @@ export type StringifyOptionsInternal = UtilOptions & { encode: boolean, encoder: Encoder, encodeValuesOnly: boolean, - filter: Filter | Array + filter: Filter | Array format: Format, serializeDate: DateSerializer, skipNulls: boolean, @@ -113,7 +114,7 @@ export type StringifyOptions = Partial & { indices?: boolean, }; -export type Stringify = (object: ObjectIndexable | Nullish, opts?: StringifyOptions) => string; +export type Stringify = (object: ObjectCoercible | Nullish, opts?: StringifyOptions) => string; export type StringifyInternal = ( object: Value, diff --git a/lib/utils.js b/lib/utils.js index 6d66265f..e9fd89a5 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -53,7 +53,7 @@ var merge = function merge(target, source, options) { if (typeof source !== 'object') { if (Array.isArray(target)) { target.push(source); - } else if (typeof target === 'object') { + } else if (typeof target === 'object' && typeof source !== "boolean") { if ((options && (options.plainObjects || options.allowPrototypes)) || !has.call(Object.prototype, source)) { target[source] = true; } @@ -64,10 +64,6 @@ var merge = function merge(target, source, options) { return target; } - if (typeof target !== 'object') { - return [target].concat(source); - } - var mergeTarget = target; if (Array.isArray(target) && !Array.isArray(source)) { mergeTarget = arrayToObject(target, options); @@ -76,8 +72,9 @@ var merge = function merge(target, source, options) { if (Array.isArray(target) && Array.isArray(source)) { source.forEach(function (item, i) { if (has.call(target, i)) { - if (target[i] && typeof target[i] === 'object') { - target[i] = merge(target[i], item, options); + var tmp = target[i]; // narrowing by element access failed here + if (tmp && typeof tmp === 'object') { + target[i] = merge(tmp, item, options); } else { target.push(item); } @@ -184,6 +181,7 @@ var encode = function encode(str, defaultEncoder, charset) { /** @type {import('./qs').compact} */ var compact = function compact(value) { + /** @type {import('./qs').queueItem[]} */ var queue = [{ obj: { o: value }, prop: 'o' }]; var refs = []; @@ -196,7 +194,7 @@ var compact = function compact(value) { var key = keys[j]; var val = obj[key]; if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) { - queue.push({ obj: obj, prop: key }); + queue.push({ obj: /** @type {{ [key: string]: object}} */(obj), prop: key }); refs.push(val); } } diff --git a/tsconfig.json b/tsconfig.json index 3f919d43..8d9efd87 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "module": "commonjs", + "target": "esnext", "noEmit": true, "esModuleInterop": true, "allowJs": true, From 1b10cecaa48da6175457cc94db4e1ad8647fba81 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Wed, 3 Oct 2018 15:34:51 -0700 Subject: [PATCH 2/3] Remove d.ts files Note that qs.d.ts remains. The other d.ts files were actually confusing the compiler's module resolution such that `var x = require('./x')` would assign the type `any` to `x` when both `x.js` and `x.d.ts` existed. With better typings, I had to fix a number of discrepancies, mainly in the types: In parse.js, the options initialisation pattern is not well-understood by the compiler and requires a type annotation for the initial assignment, then an temp variable with a cast once all the values have been filled in: ```js var internalOptions = /** @type {ParseOptionsInternal} */(options); ``` The Typescript-like pattern would be more like: ```js /** @type {ParseOptions} */ var options = opts ? utils.assign({}, opts) : {}; var internalOptions = { ignoreQueryPrefix: options.ignoreQueryPrefix === true, // ... } ``` In q.d.ts, I had to unsimplify `type Value = Concatable`. Turns out `unknown` doesn't narrow correctly. util.compact's type is actually `object => object` not `ObjectCoercible => ObjectCoercible`, since it doesn't appear to ever return a primitive. The rest of the type changes were just correcting discrepencies in the options types since the checker wasn't really checking those because of the duplicate d.ts files. In stringify, the filter can be an array of string or number. However, in this case, elements of the filter array end up used in places where only a string is expected. I inserted a couple of conversions to string where required. In the parse tests, a few tests rely on the fact that a previous test asserts that a field exists, but the type system doesn't know this to be true. I just annotated these instances with `any` because the type checking isn't adding value here. --- lib/formats.d.ts | 3 --- lib/index.d.ts | 9 --------- lib/parse.d.ts | 3 --- lib/parse.js | 8 +++++--- lib/qs.d.ts | 24 ++++++++++++------------ lib/stringify.d.ts | 3 --- lib/stringify.js | 8 +++++--- lib/utils.d.ts | 3 --- test/parse.js | 3 +++ 9 files changed, 25 insertions(+), 39 deletions(-) delete mode 100644 lib/formats.d.ts delete mode 100644 lib/index.d.ts delete mode 100644 lib/parse.d.ts delete mode 100644 lib/stringify.d.ts delete mode 100644 lib/utils.d.ts diff --git a/lib/formats.d.ts b/lib/formats.d.ts deleted file mode 100644 index 4a63d648..00000000 --- a/lib/formats.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { FormatsExport } from './qs'; - -export = FormatsExport diff --git a/lib/index.d.ts b/lib/index.d.ts deleted file mode 100644 index 2737b8f4..00000000 --- a/lib/index.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Stringify, Parse, FormatsExport } from './qs'; - -type qs = { - stringify: Stringify, - parse: Parse, - formats: FormatsExport, -}; - -export = qs; diff --git a/lib/parse.d.ts b/lib/parse.d.ts deleted file mode 100644 index ce5e561e..00000000 --- a/lib/parse.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Parse } from './qs'; - -export = Parse; diff --git a/lib/parse.js b/lib/parse.js index fd9a14a2..a9d9d5b1 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -213,6 +213,7 @@ var parseKeys = function parseQueryStringKeys(givenKey, val, options) { /** @type {import('./qs').Parse} */ module.exports = function (str, opts) { + /** @type {import('./qs').ParseOptions} */ var options = opts ? utils.assign({}, opts) : {}; if (options.decoder !== null && options.decoder !== undefined && typeof options.decoder !== 'function') { @@ -242,7 +243,8 @@ module.exports = function (str, opts) { return options.plainObjects ? Object.create(null) : {}; } - var tempObj = typeof str === 'string' ? parseValues(str, options) : str; + var internalOptions = /** @type {ParseOptionsInternal} */(options); + var tempObj = typeof str === 'string' ? parseValues(str, internalOptions) : str; var obj = options.plainObjects ? Object.create(null) : {}; // Iterate over the keys and setup the new object @@ -250,8 +252,8 @@ module.exports = function (str, opts) { var keys = Object.keys(tempObj); for (var i = 0; i < keys.length; ++i) { var key = keys[i]; - var newObj = parseKeys(key, tempObj[key], options); - obj = utils.merge(obj, newObj, options); + var newObj = parseKeys(key, tempObj[key], internalOptions); + obj = utils.merge(obj, newObj, internalOptions); } return utils.compact(obj); diff --git a/lib/qs.d.ts b/lib/qs.d.ts index 8fe8c5d5..3c2fe79e 100644 --- a/lib/qs.d.ts +++ b/lib/qs.d.ts @@ -57,7 +57,7 @@ type NonNullishPrimitive = boolean | number | string | symbol; type ObjectCoercible = object | NonNullishPrimitive; type Primitive = Nullish | NonNullishPrimitive; type Concatable = T | T[]; -type Value = Concatable; +type Value = Concatable; // TODO: type NonPrimitive = object; for use in JSDoc @@ -71,7 +71,7 @@ type queueItem = { prop: keyof queueObject, }; export type compactQueue = (queue: queueItem[]) => void; -export type compact = (value: ObjectCoercible) => ObjectCoercible; +export type compact = (value: object) => object; export type isBuffer = (obj: (Buffer | Value) & ({ constructor?: typeof Buffer })) => boolean; export type isRegExp = (obj: RegExp | Value) => boolean; export type merge = ( @@ -96,11 +96,11 @@ export type DateSerializer = (date: Date) => string | number; export type StringifyOptionsInternal = UtilOptions & { arrayFormat: ArrayFormat, charsetSentinel: boolean, - delimiter: string | RegExp, + delimiter: string, encode: boolean, encoder: Encoder, encodeValuesOnly: boolean, - filter: Filter | Array + filter: Filter | Array format: Format, serializeDate: DateSerializer, skipNulls: boolean, @@ -118,17 +118,17 @@ export type Stringify = (object: ObjectCoercible | Nullish, opts?: StringifyOpti export type StringifyInternal = ( object: Value, - prefix: string, + prefix: string | number, generateArrayPrefix: arrayPrefixGenerator, - strictNullHandling: boolean, - skipNulls: boolean, - encoder: Encoder, - filter: Filter, - sort: Comparator, + strictNullHandling: boolean | undefined, + skipNulls: boolean | undefined, + encoder: Encoder | null, + filter: Filter | Array | undefined, + sort: Comparator | null, allowDots: boolean, serializeDate: DateSerializer, formatter: Formatter, - encodeValuesOnly: boolean, + encodeValuesOnly: boolean | undefined, charset: Charset, ) => Concatable; @@ -142,7 +142,7 @@ export type ParseOptionsInternal = UtilOptions & { delimiter: string | RegExp, depth: number, ignoreQueryPrefix: boolean, - interpretNumericEntities: InterpretNumericEntities | undefined, + interpretNumericEntities: boolean | undefined, parameterLimit: number, parseArrays: boolean, strictNullHandling: boolean, diff --git a/lib/stringify.d.ts b/lib/stringify.d.ts deleted file mode 100644 index 5dfd809e..00000000 --- a/lib/stringify.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Stringify } from './qs'; - -export = Stringify; diff --git a/lib/stringify.js b/lib/stringify.js index 8e341bdf..7930c495 100644 --- a/lib/stringify.js +++ b/lib/stringify.js @@ -24,7 +24,7 @@ var arrayPrefixGenerators = { var toISO = Date.prototype.toISOString; -/** @type {StringifyOptions & Pick} */ +/** @type {StringifyOptions & Pick} */ var defaults = { addQueryPrefix: false, allowDots: false, @@ -61,6 +61,7 @@ var stringify = function stringify( // eslint-disable-line func-name-matching encodeValuesOnly, charset ) { + prefix = '' + prefix; /** @type {string | number | boolean | Buffer | undefined | object} */ var obj = object; if (typeof filter === 'function') { @@ -99,7 +100,7 @@ var stringify = function stringify( // eslint-disable-line func-name-matching } for (var i = 0; i < objKeys.length; ++i) { - var key = objKeys[i]; + var key = '' + objKeys[i]; if (skipNulls && obj[key] === null) { continue; @@ -147,6 +148,7 @@ var stringify = function stringify( // eslint-disable-line func-name-matching /** @type {import('./qs').Stringify} */ module.exports = function (object, opts) { + /** @type {import('./qs').StringifyOptions} */ var options = opts ? utils.assign({}, opts) : {}; if (options.encoder !== null && options.encoder !== undefined && typeof options.encoder !== 'function') { @@ -196,7 +198,7 @@ module.exports = function (object, opts) { /** @type {ArrayFormat} */ var arrayFormat; - if (options.arrayFormat in arrayPrefixGenerators) { + if (options.arrayFormat && options.arrayFormat in arrayPrefixGenerators) { arrayFormat = options.arrayFormat; } else if ('indices' in options) { arrayFormat = options.indices ? 'indices' : 'repeat'; diff --git a/lib/utils.d.ts b/lib/utils.d.ts deleted file mode 100644 index a3f14672..00000000 --- a/lib/utils.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { utils } from './qs'; - -export = utils; diff --git a/test/parse.js b/test/parse.js index fde54d00..b63857d3 100644 --- a/test/parse.js +++ b/test/parse.js @@ -310,10 +310,12 @@ test('parse()', function (t) { }); t.test('allows disabling array parsing', function (st) { + /** @type {*} */ var indices = qs.parse('a[0]=b&a[1]=c', { parseArrays: false }); st.deepEqual(indices, { a: { 0: 'b', 1: 'c' } }); st.equal(Array.isArray(indices.a), false, 'parseArrays:false, indices case is not an array'); + /** @type {*} */ var emptyBrackets = qs.parse('a[]=b', { parseArrays: false }); st.deepEqual(emptyBrackets, { a: { 0: 'b' } }); st.equal(Array.isArray(emptyBrackets.a), false, 'parseArrays:false, empty brackets case is not an array'); @@ -446,6 +448,7 @@ test('parse()', function (t) { a.b = 'c'; st.deepEqual(qs.parse(a), { b: 'c' }); + /** @type {*} */ var result = qs.parse({ a: a }); st.equal('a' in result, true, 'result has "a" property'); st.deepEqual(result.a, a); From cc45aac277a82fc0b6b9e73d7853fe6d3fc07e9d Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Thu, 4 Oct 2018 13:35:36 -0700 Subject: [PATCH 3/3] Move file-specific types back to JSDoc Global aliases like Nullish are now global so they don't need to be imported. I also introduced `type NonPrimitive = object` to work around the way Typescript treats `object` as `any` in JS. The move required hardly any changes; in fact, I basically restored the annotations from commit 856582edfff28053801677a5f8e2de075580e830 with updates for the fixes in my previous commits. Introducing NonPrimitive required a few changes since `object` is stricter than `any`. The most annoying is a cast on ```js /** @type {any} */([]).concat(leaf); ``` Because with strictNullChecks on, the type of `[]` is `never[]`, which is technically correct but usually annoying. (strictNullChecks is usually not a good fit for JS codebases that don't want to adopt Typescript idioms completely, and `[].concat` is the best example of this.) --- lib/formats.js | 18 ++++-- lib/parse.js | 46 +++++++++++--- lib/qs.d.ts | 152 +--------------------------------------------- lib/stringify.js | 80 ++++++++++++++++++++---- lib/utils.js | 81 ++++++++++++++++++++---- test/parse.js | 2 +- test/stringify.js | 10 +-- 7 files changed, 196 insertions(+), 193 deletions(-) diff --git a/lib/formats.js b/lib/formats.js index fe97aa0d..28d53d13 100644 --- a/lib/formats.js +++ b/lib/formats.js @@ -4,8 +4,20 @@ var replace = String.prototype.replace; var percentTwenties = /%20/g; -/** @type {import('./qs').FormatsExport} */ -var formatsExport = { +/** @typedef {'RFC1738' | 'RFC3986'} Format */ +/** @callback Formatter + * + * @param {string | Buffer} value + * @returns {string} + */ + +/** @type {{ + * default: Format, + * formatters: { [s: string]: Formatter }, + * RFC1738: 'RFC1738', + * RFC3986: 'RFC3986', + * }} */ +module.exports = { 'default': 'RFC3986', formatters: { @@ -20,5 +32,3 @@ var formatsExport = { RFC1738: 'RFC1738', RFC3986: 'RFC3986' }; - -module.exports = formatsExport; diff --git a/lib/parse.js b/lib/parse.js index a9d9d5b1..56de93d5 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -4,8 +4,30 @@ var utils = require('./utils'); var has = Object.prototype.hasOwnProperty; -/** @typedef {import('./qs').ParseOptionsInternal} ParseOptionsInternal */ -/** @typedef {import('./qs').ParseOptions} ParseOptions */ +/** @typedef {import('./utils').UtilOptions} UtilOptions */ +/** @typedef {import('./utils').Decoder} Decoder */ + +/** @callback InterpretNumericEntities + * + * @param {string} str + * @returns str + */ +/** @typedef ParseOptionsInternalType + * + * @property {boolean} allowPrototypes + * @property {number} arrayLimit + * @property {string | RegExp} delimiter + * @property {boolean} ignoreQueryPrefix + * @property {boolean | InterpretNumericEntities} interpretNumericEntities + * @property {number} parameterLimit + * @property {boolean} strictNullHandling + * @property {boolean} charsetSentinel + * @property {Decoder} decoder + * @property {number} depth + * @property {boolean} parseArrays + */ +/** @typedef {UtilOptions & ParseOptionsInternalType} ParseOptionsInternal */ +/** @typedef {Partial & { decoder?: Decoder }} ParseOptions */ /** @type {ParseOptions & Pick} */ var defaults = { @@ -25,7 +47,7 @@ var defaults = { strictNullHandling: false }; -/** @type {import('./qs').InterpretNumericEntities} */ +/** @type {InterpretNumericEntities} */ var interpretNumericEntities = function (str) { return str.replace( /&#(\d+);/g, @@ -53,7 +75,7 @@ var charsetSentinel = 'utf8=%E2%9C%93'; // encodeURIComponent('✓') /** * @param {string} str * @param {ParseOptionsInternal} options - * @returns {object | Array<*>} + * @returns {NonPrimitive | Array<*>} */ var parseValues = function parseQueryStringValues(str, options) { var obj = {}; @@ -112,7 +134,7 @@ var parseValues = function parseQueryStringValues(str, options) { /** * @param {string[]} chain - * @param {object=} val + * @param {NonPrimitive=} val * @param {ParseOptionsInternal} options * @returns */ @@ -124,7 +146,7 @@ var parseObject = function (chain, val, options) { var root = chain[i]; if (root === '[]' && options.parseArrays) { - obj = [].concat(leaf); + obj = /** @type {*} */([]).concat(leaf); } else { obj = options.plainObjects ? Object.create(null) : {}; var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root; @@ -153,9 +175,9 @@ var parseObject = function (chain, val, options) { /** * @param {string=} givenKey - * @param {object=} val + * @param {NonPrimitive=} val * @param {ParseOptionsInternal} options - * @returns {object} + * @returns {NonPrimitive | undefined} */ var parseKeys = function parseQueryStringKeys(givenKey, val, options) { if (!givenKey) { @@ -211,9 +233,13 @@ var parseKeys = function parseQueryStringKeys(givenKey, val, options) { return parseObject(keys, val, options); }; -/** @type {import('./qs').Parse} */ +/** + * @param {NonPrimitive | string | Nullish} str + * @param {ParseOptions} [opts] + * @returns {NonPrimitive | Array} + */ module.exports = function (str, opts) { - /** @type {import('./qs').ParseOptions} */ + /** @type {ParseOptions} */ var options = opts ? utils.assign({}, opts) : {}; if (options.decoder !== null && options.decoder !== undefined && typeof options.decoder !== 'function') { diff --git a/lib/qs.d.ts b/lib/qs.d.ts index 3c2fe79e..2f44b850 100644 --- a/lib/qs.d.ts +++ b/lib/qs.d.ts @@ -1,158 +1,8 @@ -export type Format = 'RFC1738' | 'RFC3986'; - -export type Formatter = (value: string | Buffer) => string; - -type Formats = { - [format in Format]: format -}; - -type Formatters = { - [format in Format]: Formatter -}; - -export type FormatsExport = { - default: Format, - formatters: Formatters, -} & Formats; - -export type ArrayFormat = 'brackets' | 'indices' | 'repeat'; - -export type arrayPrefixGenerator = (prefix: string, key?: string) => string; - -export type arrayPrefixGenerators = { - [format in ArrayFormat]: arrayPrefixGenerator -} - -export type Filter = (prefix: string, obj: T) => T; - -export type Comparator = (a: Value, b: Value) => number; - -export type Charset = 'iso-8859-1' | 'utf-8'; - -export type Decoder = ( - str: string, - defaultDecoder: Decoder, - charset: Charset -) => string; - -export type Encoder = ( - str: string | Buffer, - defaultEncoder: Encoder, - charset?: Charset -) => string | Buffer; - -export type UtilOptions = { - allowDots: boolean, - charset: Charset, - plainObjects: boolean, -}; - -export type UtilOptionsInternal = UtilOptions & { - allowPrototypes: boolean, - arrayLimit: number, -}; - type Nullish = null | undefined; type NonNullishPrimitive = boolean | number | string | symbol; type ObjectCoercible = object | NonNullishPrimitive; type Primitive = Nullish | NonNullishPrimitive; type Concatable = T | T[]; type Value = Concatable; -// TODO: type NonPrimitive = object; for use in JSDoc - - -export type arrayToObject = (source: Value[], options?: UtilOptionsInternal) => object; -export type assign = (target: object, source: object) => /* target */ object; -type queueObject = { - [key: string]: Concatable<{}>, -}; -type queueItem = { - obj: queueObject, - prop: keyof queueObject, -}; -export type compactQueue = (queue: queueItem[]) => void; -export type compact = (value: object) => object; -export type isBuffer = (obj: (Buffer | Value) & ({ constructor?: typeof Buffer })) => boolean; -export type isRegExp = (obj: RegExp | Value) => boolean; -export type merge = ( - target: ObjectCoercible[] | object, - source?: ObjectCoercible, - options?: UtilOptionsInternal, -) => /* target | */ object; - -export type utils = { - arrayToObject: arrayToObject, - assign: assign, - compact: compact, - decode: Decoder, - encode: Encoder, - isBuffer: isBuffer, - isRegExp: isRegExp, - merge: merge, -} - -export type DateSerializer = (date: Date) => string | number; - -export type StringifyOptionsInternal = UtilOptions & { - arrayFormat: ArrayFormat, - charsetSentinel: boolean, - delimiter: string, - encode: boolean, - encoder: Encoder, - encodeValuesOnly: boolean, - filter: Filter | Array - format: Format, - serializeDate: DateSerializer, - skipNulls: boolean, - sort: Comparator, - strictNullHandling: boolean, -}; - -export type StringifyOptions = Partial & { - encoder?: Encoder | null, - addQueryPrefix?: boolean, - indices?: boolean, -}; - -export type Stringify = (object: ObjectCoercible | Nullish, opts?: StringifyOptions) => string; - -export type StringifyInternal = ( - object: Value, - prefix: string | number, - generateArrayPrefix: arrayPrefixGenerator, - strictNullHandling: boolean | undefined, - skipNulls: boolean | undefined, - encoder: Encoder | null, - filter: Filter | Array | undefined, - sort: Comparator | null, - allowDots: boolean, - serializeDate: DateSerializer, - formatter: Formatter, - encodeValuesOnly: boolean | undefined, - charset: Charset, -) => Concatable; - -export type InterpretNumericEntities = (str: string) => string; - -export type ParseOptionsInternal = UtilOptions & { - allowPrototypes: boolean, - arrayLimit: number, - charsetSentinel: boolean, - decoder: Decoder, - delimiter: string | RegExp, - depth: number, - ignoreQueryPrefix: boolean, - interpretNumericEntities: boolean | undefined, - parameterLimit: number, - parseArrays: boolean, - strictNullHandling: boolean, -}; - -export type ParseOptions = Partial & { - decoder?: Decoder, -}; -export type Parse = ( - str: object | string | Nullish, - opts?: ParseOptions, -) => object | Array; +type NonPrimitive = object; diff --git a/lib/stringify.js b/lib/stringify.js index 7930c495..11c4bd6d 100644 --- a/lib/stringify.js +++ b/lib/stringify.js @@ -1,15 +1,56 @@ 'use strict'; - -/** @typedef {import('./qs').Charset} Charset */ -/** @typedef {import('./qs').DateSerializer} DateSerializer */ -/** @typedef {import('./qs').ArrayFormat} ArrayFormat */ -/** @typedef {import('./qs').StringifyOptions} StringifyOptions */ -/** @typedef {import('./qs').StringifyOptionsInternal} StringifyOptionsInternal */ +/** @typedef {import('./utils').UtilOptions} UtilOptions */ +/** @typedef {import('./utils').Encoder} Encoder */ +/** @typedef {import('./utils').Charset} Charset */ + +/** + * @template T + * @callback Filter + * @param {string} prefix + * @param {T} obj + * @returns T + */ + /** + * @callback Comparator + * + * @param {Value} a + * @param {Value} b + * @returns {number} + */ +/** @callback DateSerializer + * @param {Date} date + * @returns {string | number} + */ +/** @typedef {'brackets' | 'indices' | 'repeat'} ArrayFormat */ +/** @typedef StringifyOptionsInternalType + * + * @property {ArrayFormat} arrayFormat + * @property {boolean} charsetSentinel + * @property {string} delimiter + * @property {boolean} encode + * @property {Encoder} encoder + * @property {boolean} encodeValuesOnly + * @property {Filter | Array} filter + * @property {import('./formats').Format} format + * @property {DateSerializer} serializeDate + * @property {boolean} skipNulls + * @property {Comparator} sort + * @property {boolean} strictNullHandling + */ +/** @typedef {UtilOptions & StringifyOptionsInternalType} StringifyOptionsInternal */ + /** @typedef StringifyOptionsType + * + * @property {(Encoder | null)=} encoder + * @property {boolean=} addQueryPrefix + * @property {boolean=} indices + */ +/** @typedef {Partial & StringifyOptionsType} StringifyOptions */ var utils = require('./utils'); var formats = require('./formats'); -/** @type {import('./qs').arrayPrefixGenerators} */ +/** @typedef {(prefix: string, key?: string) => string} ArrayPrefixGenerator */ +/** @type {{ [format in ArrayFormat]: ArrayPrefixGenerator }} */ var arrayPrefixGenerators = { brackets: function brackets(prefix) { // eslint-disable-line func-name-matching return prefix + '[]'; @@ -44,8 +85,22 @@ var defaults = { skipNulls: false, strictNullHandling: false }; - -/** @type {import('./qs').StringifyInternal} */ +/** + * @param {Value} object + * @param {string | number} prefix + * @param {ArrayPrefixGenerator} generateArrayPrefix + * @param {boolean | undefined} strictNullHandling + * @param {boolean | undefined} skipNulls + * @param {Encoder | null} encoder + * @param {Filter | Array | undefined} filter + * @param {Comparator | null} sort + * @param {boolean} allowDots + * @param {DateSerializer} serializeDate + * @param {import('./formats').Formatter} formatter + * @param {boolean | undefined} encodeValuesOnly + * @param {Charset} charset + * @returns {Concatable} +*/ var stringify = function stringify( // eslint-disable-line func-name-matching object, prefix, @@ -146,9 +201,12 @@ var stringify = function stringify( // eslint-disable-line func-name-matching return values; }; -/** @type {import('./qs').Stringify} */ +/** + * @param {ObjectCoercible | Nullish} object + * @param {StringifyOptions} [opts] + */ module.exports = function (object, opts) { - /** @type {import('./qs').StringifyOptions} */ + /** @type {StringifyOptions} */ var options = opts ? utils.assign({}, opts) : {}; if (options.encoder !== null && options.encoder !== undefined && typeof options.encoder !== 'function') { diff --git a/lib/utils.js b/lib/utils.js index e9fd89a5..942ce085 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,4 +1,39 @@ 'use strict'; +/** @typedef {'iso-8859-1' | 'utf-8'} Charset */ + +/** @callback Decoder + * + * @param {string} str + * @param {Decoder} defaultDecoder + * @param {Charset} charset + * @returns {string} + */ + +/** @callback Encoder + * + * @param {string | Buffer} str + * @param {Encoder} defaultEncoder + * @param {Charset=} charset + * @returns {string | Buffer} + */ + +/** @typedef UtilOptionsInternalType + * + * @property {boolean} allowPrototypes + * @property {number} arrayLimit + * + * @typedef {UtilOptions & UtilOptionsInternalType} UtilOptionsInternal + */ + +/** @typedef UtilOptions + * + * @property {boolean} allowDots + * @property {Charset} charset + * @property {boolean} plainObjects + */ + +/** @typedef {Object>} QueueObject */ +/** @typedef {{ obj: QueueObject, prop: keyof QueueObject }} QueueItem */ var has = Object.prototype.hasOwnProperty; @@ -11,7 +46,10 @@ var hexTable = (function () { return array; }()); -/** @type {import('./qs').compactQueue} */ +/** + * @param {QueueItem[]} queue + * @returns {void} + */ var compactQueue = function compactQueue(queue) { while (queue.length > 1) { var item = queue.pop(); @@ -32,7 +70,11 @@ var compactQueue = function compactQueue(queue) { } }; -/** @type {import('./qs').arrayToObject} */ +/** + * @param {Value[]} source + * @param {UtilOptionsInternal} [options] + * @returns {object} + */ var arrayToObject = function arrayToObject(source, options) { var obj = options && options.plainObjects ? Object.create(null) : {}; for (var i = 0; i < source.length; ++i) { @@ -44,7 +86,12 @@ var arrayToObject = function arrayToObject(source, options) { return obj; }; -/** @type {import('./qs').merge} */ +/** + * @param {ObjectCoercible[] | object} target + * @param {ObjectCoercible} [source] + * @param {UtilOptionsInternal} [options] + * @returns {object} maybe should be typeof target & object. + */ var merge = function merge(target, source, options) { if (!source) { return target; @@ -97,7 +144,11 @@ var merge = function merge(target, source, options) { }, mergeTarget); }; -/** @type {import('./qs').assign} */ +/** + * @param {object} target + * @param {object} source + * @returns object - should probably be `typeof target & object` + */ var assign = function assignSingleSource(target, source) { return Object.keys(source).reduce(function (acc, key) { acc[key] = source[key]; @@ -105,7 +156,7 @@ var assign = function assignSingleSource(target, source) { }, target); }; -/** @type {import('./qs').Decoder} */ +/** @type {Decoder} */ var decode = function (str, defaultDecoder, charset) { var strWithoutPlus = str.replace(/\+/g, ' '); if (charset === 'iso-8859-1') { @@ -120,7 +171,7 @@ var decode = function (str, defaultDecoder, charset) { } }; -/** @type {import('./qs').Encoder} */ +/** @type {Encoder} */ var encode = function encode(str, defaultEncoder, charset) { // This code was originally written by Brian White (mscdex) for the io.js core querystring library. // It has been adapted here for stricter adherence to RFC 3986 @@ -179,9 +230,12 @@ var encode = function encode(str, defaultEncoder, charset) { return out; }; -/** @type {import('./qs').compact} */ +/** + * @param {object} value + * @returns {object} + */ var compact = function compact(value) { - /** @type {import('./qs').queueItem[]} */ + /** @type {QueueItem[]} */ var queue = [{ obj: { o: value }, prop: 'o' }]; var refs = []; @@ -205,12 +259,18 @@ var compact = function compact(value) { return value; }; -/** @type {import('./qs').isRegExp} */ +/** + * @param {RegExp | Value} obj + * @returns boolean + */ var isRegExp = function isRegExp(obj) { return Object.prototype.toString.call(obj) === '[object RegExp]'; }; -/** @type {import('./qs').isBuffer} */ +/** + * @param {(Buffer | Value) & ({ constructor?: typeof Buffer })} obj + * @returns boolean + */ var isBuffer = function isBuffer(obj) { if (obj === null || typeof obj === 'undefined') { return false; @@ -219,7 +279,6 @@ var isBuffer = function isBuffer(obj) { return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj)); }; -/** @type {import('./qs').utils} */ module.exports = { arrayToObject: arrayToObject, assign: assign, diff --git a/test/parse.js b/test/parse.js index b63857d3..696e3a10 100644 --- a/test/parse.js +++ b/test/parse.js @@ -1,6 +1,6 @@ 'use strict'; -/** @typedef {import('../lib/qs').Decoder} Decoder */ +/** @typedef {import('../lib/utils').Decoder} Decoder */ var test = require('tape'); var qs = require('../'); diff --git a/test/stringify.js b/test/stringify.js index 3b657759..931bd2b1 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -1,8 +1,8 @@ 'use strict'; -/** @typedef {import('../lib/qs').Filter<*>} Filter */ -/** @typedef {import('../lib/qs').Encoder} Encoder */ -/** @typedef {import('../lib/qs').DateSerializer} DateSerializer */ +/** @typedef {import('../lib/stringify').Filter<*>} Filter */ +/** @typedef {import('../lib/stringify').Encoder} Encoder */ +/** @typedef {import('../lib/stringify').DateSerializer} DateSerializer */ var test = require('tape'); /** @type {import('../')} */ @@ -427,7 +427,7 @@ test('stringify()', function (t) { }); t.test('can sort the keys', function (st) { - /** @type {import('../lib/qs').Comparator} */ + /** @type {import('../lib/stringify').Comparator} */ var sort = function (a, b) { return String(a).localeCompare(String(b)); }; @@ -437,7 +437,7 @@ test('stringify()', function (t) { }); t.test('can sort the keys at depth 3 or more too', function (st) { - /** @type {import('../lib/qs').Comparator} */ + /** @type {import('../lib/stringify').Comparator} */ var sort = function (a, b) { return String(a).localeCompare(String(b)); };