Repository: sindresorhus/query-string (js)

Issue:
`skipNulls` option for queryString.stringify
Hi!

I am now comparing 2 libs for future use on my project: `qs` and `query-string`.
The second one has some advantages like encoding/decoding booleans and numbers which are important for me. But the 1st one has `skipNulls` option which allows to skip `null` and `undefined` values:

```js

const obj1 = {
  a: 1,
  b: '2',
  c: [3, 4, 5],
  d: true,
  e: 'just string',
  f: null,
  g: undefined,
};

// qs
// can skip null/undefined values
const ex2 = qs.stringify(obj1, {
  addQueryPrefix: true,
  arrayFormat: 'comma',
  skipNulls: true,
});
const ex3 = qs.parse(ex2, {
  parseArrays: true,
  comma: true,
  ignoreQueryPrefix: true,
});

// query-string
// can store numbers and booleans
const ex4 = queryString.stringify(obj1, {
  strict: true,
});
const ex5 = queryString.parse(ex4, {
  parseNumbers: true,
  parseBooleans: true,
});

console.log('ex2', ex2);
console.log('ex3', ex3);
console.log('ex4', ex4);
console.log('ex5', ex5);
```

![Screenshot from 2019-07-26 10-48-56](https://user-images.githubusercontent.com/10671879/61935355-0296c780-af93-11e9-99fd-54e04dd25f94.png)

### Does it sound reasonable to implement this option?

Pull request description:
Add option to skip keys with the value of `null` in `.stringify()`
Follow up on #197 to fix #196.

Closes #197

Patch:
diff --git a/index.d.ts b/index.d.ts
index cf85856..a59cf21 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -192,6 +192,28 @@ export interface StringifyOptions {
 	```
 	*/
 	readonly sort?: ((itemLeft: string, itemRight: string) => number) | false;
+
+	/**
+	Skip keys with `null` as the value.
+	
+	Note that keys with `undefined` as the value are always skipped.
+
+	@default false
+
+	@example
+	```
+	queryString.stringify({a: 1, b: undefined, c: null, d: 4}, {
+		skipNull: true
+	});
+	//=> 'a=1&d=4'
+
+	queryString.stringify({a: undefined, b: null}, {
+		skipNull: true
+	});
+	//=> ''
+	```
+	*/
+	readonly skipNull?: boolean;
 }
 
 /**
@@ -204,5 +226,7 @@ export function stringify(
 
 /**
 Extract a query string from a URL that can be passed into `.parse()`.
+
+Note: This behaviour can be changed with the `skipNull` option.
 */
 export function extract(url: string): string;
diff --git a/index.js b/index.js
index b122f27..e80016d 100644
--- a/index.js
+++ b/index.js
@@ -8,7 +8,7 @@ function encoderForArrayFormat(options) {
 		case 'index':
 			return key => (result, value) => {
 				const index = result.length;
-				if (value === undefined) {
+				if (value === undefined || (options.skipNull && value === null)) {
 					return result;
 				}
 
@@ -24,7 +24,7 @@ function encoderForArrayFormat(options) {
 
 		case 'bracket':
 			return key => (result, value) => {
-				if (value === undefined) {
+				if (value === undefined || (options.skipNull && value === null)) {
 					return result;
 				}
 
@@ -36,12 +36,12 @@ function encoderForArrayFormat(options) {
 			};
 
 		case 'comma':
-			return key => (result, value, index) => {
+			return key => (result, value) => {
 				if (value === null || value === undefined || value.length === 0) {
 					return result;
 				}
 
-				if (index === 0) {
+				if (result.length === 0) {
 					return [[encode(key, options), '=', encode(value, options)].join('')];
 				}
 
@@ -50,7 +50,7 @@ function encoderForArrayFormat(options) {
 
 		default:
 			return key => (result, value) => {
-				if (value === undefined) {
+				if (value === undefined || (options.skipNull && value === null)) {
 					return result;
 				}
 
@@ -257,7 +257,17 @@ exports.stringify = (object, options) => {
 	}, options);
 
 	const formatter = encoderForArrayFormat(options);
-	const keys = Object.keys(object);
+
+	const objectCopy = Object.assign({}, object);
+	if (options.skipNull) {
+		for (const key of Object.keys(objectCopy)) {
+			if (objectCopy[key] === undefined || objectCopy[key] === null) {
+				delete objectCopy[key];
+			}
+		}
+	}
+
+	const keys = Object.keys(objectCopy);
 
 	if (options.sort !== false) {
 		keys.sort(options.sort);
diff --git a/readme.md b/readme.md
index 297a47d..74cd058 100644
--- a/readme.md
+++ b/readme.md
@@ -204,10 +204,35 @@ queryString.stringify({b: 1, c: 2, a: 3}, {sort: false});
 
 If omitted, keys are sorted using `Array#sort()`, which means, converting them to strings and comparing strings in Unicode code point order.
 
+##### skipNull
+
+Skip keys with `null` as the value.
+
+Note that keys with `undefined` as the value are always skipped.
+
+Type: `boolean`<br>
+Default: `false`
+
+```js
+queryString.stringify({a: 1, b: undefined, c: null, d: 4}, {
+	skipNull: true
+});
+//=> 'a=1&d=4'
+```
+
+```js
+queryString.stringify({a: undefined, b: null}, {
+	skipNull: true
+});
+//=> ''
+```
+
 ### .extract(string)
 
 Extract a query string from a URL that can be passed into `.parse()`.
 
+Note: This behaviour can be changed with the `skipNull` option.
+
 ### .parseUrl(string, options?)
 
 Extract the URL and the query string as an object.
@@ -265,14 +290,8 @@ queryString.stringify({foo: undefined});
 ```
 
 
----
+## query-string for enterprise
+
+Available as part of the Tidelift Subscription.
 
-<div align="center">
-	<b>
-		<a href="https://tidelift.com/subscription/pkg/npm-query-string?utm_source=npm-query-string&utm_medium=referral&utm_campaign=readme">Get professional support for this package with a Tidelift subscription</a>
-	</b>
-	<br>
-	<sub>
-		Tidelift helps make open source sustainable for maintainers while giving companies<br>assurances about security, maintenance, and licensing for their dependencies.
-	</sub>
-</div>
+The maintainers of query-string and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-query-string?utm_source=npm-query-string&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index 11e15c8..a4cb36d 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -22,6 +22,7 @@ expectType<string>(queryString.stringify({foo: 'bar'}, {arrayFormat: 'index'}));
 expectType<string>(queryString.stringify({foo: 'bar'}, {arrayFormat: 'none'}));
 expectType<string>(queryString.stringify({foo: 'bar'}, {arrayFormat: 'comma'}));
 expectType<string>(queryString.stringify({foo: 'bar'}, {sort: false}));
+expectType<string>(queryString.stringify({foo: 'bar'}, {skipNull: true}));
 const order = ['c', 'a', 'b'];
 expectType<string>(
 	queryString.stringify(
diff --git a/test/stringify.js b/test/stringify.js
index 8553ebd..09b6371 100644
--- a/test/stringify.js
+++ b/test/stringify.js
@@ -128,7 +128,7 @@ test('array stringify representation with array commas', t => {
 
 test('array stringify representation with array commas and null value', t => {
 	t.is(queryString.stringify({
-		foo: ['a', null, ''],
+		foo: [null, 'a', null, ''],
 		bar: [null]
 	}, {
 		arrayFormat: 'comma'
@@ -193,3 +193,69 @@ test('should disable sorting', t => {
 		sort: false
 	}), 'c=foo&b=bar&a=baz');
 });
+
+test('should ignore null when skipNull is set', t => {
+	t.is(queryString.stringify({
+		a: 1,
+		b: null,
+		c: 3
+	}, {
+		skipNull: true
+	}), 'a=1&c=3');
+});
+
+test('should ignore undefined when skipNull is set', t => {
+	t.is(queryString.stringify({
+		a: 1,
+		b: undefined,
+		c: 3
+	}, {
+		skipNull: true
+	}), 'a=1&c=3');
+});
+
+test('should ignore both null and undefined when skipNull is set', t => {
+	t.is(queryString.stringify({
+		a: undefined,
+		b: null
+	}, {
+		skipNull: true
+	}), '');
+});
+
+test('should ignore both null and undefined when skipNull is set for arrayFormat', t => {
+	t.is(queryString.stringify({
+		a: [undefined, null, 1, undefined, 2, null],
+		b: null,
+		c: 1
+	}, {
+		skipNull: true
+	}), 'a=1&a=2&c=1');
+
+	t.is(queryString.stringify({
+		a: [undefined, null, 1, undefined, 2, null],
+		b: null,
+		c: 1
+	}, {
+		skipNull: true,
+		arrayFormat: 'bracket'
+	}), 'a[]=1&a[]=2&c=1');
+
+	t.is(queryString.stringify({
+		a: [undefined, null, 1, undefined, 2, null],
+		b: null,
+		c: 1
+	}, {
+		skipNull: true,
+		arrayFormat: 'comma'
+	}), 'a=1,2&c=1');
+
+	t.is(queryString.stringify({
+		a: [undefined, null, 1, undefined, 2, null],
+		b: null,
+		c: 1
+	}, {
+		skipNull: true,
+		arrayFormat: 'index'
+	}), 'a[0]=1&a[1]=2&c=1');
+});