Repository: sindresorhus/query-string (js)

Issue:
Add parameter skipEmptyString
I love the skipNull that is available in the stringify.
Would it be posible to also include a skipEmpty parameter that filters all params where the value = ""

Pull request description:
Add `skipEmptyString` option
Fixes #244

Patch:
diff --git a/index.d.ts b/index.d.ts
index 587eaa3..d5012b4 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -283,6 +283,33 @@ export interface StringifyOptions {
 	```
 	*/
 	readonly skipNull?: boolean;
+
+	/**
+	Skip keys with an empty string as the value.
+
+	@default false
+
+	@example
+	```
+	import queryString = require('query-string');
+
+	queryString.stringify({a: 1, b: '', c: '', d: 4}, {
+		skipEmptyString: true
+	});
+	//=> 'a=1&d=4'
+	```
+	
+	@example
+	```
+	import queryString = require('query-string');
+
+	queryString.stringify({a: '', b: ''}, {
+		skipEmptyString: true
+	});
+	//=> ''
+	```
+	*/
+	readonly skipEmptyString?: boolean;
 }
 
 /**
diff --git a/index.js b/index.js
index 9c63162..cad13ca 100644
--- a/index.js
+++ b/index.js
@@ -3,12 +3,19 @@ const strictUriEncode = require('strict-uri-encode');
 const decodeComponent = require('decode-uri-component');
 const splitOnFirst = require('split-on-first');
 
+const isNullOrUndefined = value => value === null || value === undefined;
+
 function encoderForArrayFormat(options) {
 	switch (options.arrayFormat) {
 		case 'index':
 			return key => (result, value) => {
 				const index = result.length;
-				if (value === undefined || (options.skipNull && value === null)) {
+
+				if (
+					value === undefined ||
+					(options.skipNull && value === null) ||
+					(options.skipEmptyString && value === '')
+				) {
 					return result;
 				}
 
@@ -24,7 +31,11 @@ function encoderForArrayFormat(options) {
 
 		case 'bracket':
 			return key => (result, value) => {
-				if (value === undefined || (options.skipNull && value === null)) {
+				if (
+					value === undefined ||
+					(options.skipNull && value === null) ||
+					(options.skipEmptyString && value === '')
+				) {
 					return result;
 				}
 
@@ -51,7 +62,11 @@ function encoderForArrayFormat(options) {
 
 		default:
 			return key => (result, value) => {
-				if (value === undefined || (options.skipNull && value === null)) {
+				if (
+					value === undefined ||
+					(options.skipNull && value === null) ||
+					(options.skipEmptyString && value === '')
+				) {
 					return result;
 				}
 
@@ -280,14 +295,18 @@ exports.stringify = (object, options) => {
 
 	validateArrayFormatSeparator(options.arrayFormatSeparator);
 
+	const shouldFilter = key => (
+		(options.skipNull && isNullOrUndefined(object[key])) ||
+		(options.skipEmptyString && object[key] === '')
+	);
+
 	const formatter = encoderForArrayFormat(options);
 
-	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 objectCopy = {};
+
+	for (const key of Object.keys(object)) {
+		if (!shouldFilter(key)) {
+			objectCopy[key] = object[key];
 		}
 	}
 
diff --git a/readme.md b/readme.md
index 3dd2ea0..d6d2bb0 100644
--- a/readme.md
+++ b/readme.md
@@ -304,6 +304,31 @@ queryString.stringify({a: undefined, b: null}, {
 //=> ''
 ```
 
+##### skipEmptyString
+
+Skip keys with an empty string as the value.
+
+Type: `boolean`\
+Default: `false`
+
+```js
+const queryString = require('query-string');
+
+queryString.stringify({a: 1, b: '', c: '', d: 4}, {
+	skipEmptyString: true
+});
+//=> 'a=1&d=4'
+```
+
+```js
+const queryString = require('query-string');
+
+queryString.stringify({a: '', b: ''}, {
+	skipEmptyString: true
+});
+//=> ''
+```
+
 ### .extract(string)
 
 Extract a query string from a URL that can be passed into `.parse()`.


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index a4cb36d..19a6817 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -23,6 +23,7 @@ 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}));
+expectType<string>(queryString.stringify({foo: 'bar'}, {skipEmptyString: true}));
 const order = ['c', 'a', 'b'];
 expectType<string>(
 	queryString.stringify(
diff --git a/test/stringify-url.js b/test/stringify-url.js
index 2bde443..933af7a 100644
--- a/test/stringify-url.js
+++ b/test/stringify-url.js
@@ -19,6 +19,13 @@ test('stringify URL with a query string', t => {
 	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar?foo=baz', query: {foo: 'bar'}}), 'https://foo.bar?foo=bar');
 });
 
+test('skipEmptyString:: stringify URL with a query string', t => {
+	const config = {skipEmptyString: true};
+
+	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {foo: 'bar', baz: ''}}, config), 'https://foo.bar?foo=bar');
+	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {foo: 'bar', baz: ['', 'qux']}}, config), 'https://foo.bar?baz=qux&foo=bar');
+});
+
 test('stringify URL from the result of `parseUrl` without query string', t => {
 	const url = 'https://foo.bar';
 	const parsedUrl = queryString.parseUrl(url);
diff --git a/test/stringify.js b/test/stringify.js
index 147d2e2..5c3487b 100644
--- a/test/stringify.js
+++ b/test/stringify.js
@@ -204,6 +204,16 @@ test('should ignore null when skipNull is set', t => {
 	}), 'a=1&c=3');
 });
 
+test('should ignore emptyString when skipEmptyString is set', t => {
+	t.is(queryString.stringify({
+		a: 1,
+		b: '',
+		c: 3
+	}, {
+		skipEmptyString: true
+	}), 'a=1&c=3');
+});
+
 test('should ignore undefined when skipNull is set', t => {
 	t.is(queryString.stringify({
 		a: 1,
@@ -260,6 +270,50 @@ test('should ignore both null and undefined when skipNull is set for arrayFormat
 	}), 'a[0]=1&a[1]=2&c=1');
 });
 
+test('should ignore empty string when skipEmptyString is set for arrayFormat', t => {
+	t.is(queryString.stringify({
+		a: ['', 1, '', 2],
+		b: '',
+		c: 1
+	}, {
+		skipEmptyString: true
+	}), 'a=1&a=2&c=1');
+
+	t.is(queryString.stringify({
+		a: ['', 1, '', 2],
+		b: '',
+		c: 1
+	}, {
+		skipEmptyString: true,
+		arrayFormat: 'bracket'
+	}), 'a[]=1&a[]=2&c=1');
+
+	t.is(queryString.stringify({
+		a: ['', 1, '', 2],
+		b: '',
+		c: 1
+	}, {
+		skipEmptyString: true,
+		arrayFormat: 'comma'
+	}), 'a=1,2&c=1');
+
+	t.is(queryString.stringify({
+		a: ['', 1, '', 2],
+		b: '',
+		c: 1
+	}, {
+		skipEmptyString: true,
+		arrayFormat: 'index'
+	}), 'a[0]=1&a[1]=2&c=1');
+
+	t.is(queryString.stringify({
+		a: ['', '', '', ''],
+		c: 1
+	}, {
+		skipEmptyString: true
+	}), 'c=1');
+});
+
 test('stringify throws TypeError for invalid arrayFormatSeparator', t => {
 	t.throws(_ => queryString.stringify({}, {arrayFormatSeparator: ',,'}), {
 		instanceOf: TypeError