Skip to content

Commit 672eb82

Browse files
Add types option to the .parse() method (#385)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 3d8fbf2 commit 672eb82

File tree

4 files changed

+371
-9
lines changed

4 files changed

+371
-9
lines changed

base.d.ts

+110-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,14 @@ export type ParseOptions = {
8787
//=> {foo: ['1', '2', '3']}
8888
```
8989
*/
90-
readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'bracket-separator' | 'colon-list-separator' | 'none';
90+
readonly arrayFormat?:
91+
| 'bracket'
92+
| 'index'
93+
| 'comma'
94+
| 'separator'
95+
| 'bracket-separator'
96+
| 'colon-list-separator'
97+
| 'none';
9198

9299
/**
93100
The character used to separate array elements when using `{arrayFormat: 'separator'}`.
@@ -169,6 +176,108 @@ export type ParseOptions = {
169176
```
170177
*/
171178
readonly parseFragmentIdentifier?: boolean;
179+
180+
/**
181+
Specify a pre-defined schema to be used when parsing values. The types specified will take precedence over options such as: `parseNumber`, `parseBooleans`, and `arrayFormat`.
182+
183+
Use this feature to override the type of a value. This can be useful when the type is ambiguous such as a phone number (see example 1 and 2).
184+
185+
It is possible to provide a custom function as the parameter type. The parameter's value will equal the function's return value (see example 4).
186+
187+
NOTE: Array types (`string[]` and `number[]`) will have no effect if `arrayFormat` is set to `none` (see example 5).
188+
189+
@default {}
190+
191+
@example
192+
Parse `phoneNumber` as a string, overriding the `parseNumber` option:
193+
```
194+
import queryString from 'query-string';
195+
196+
queryString.parse('?phoneNumber=%2B380951234567&id=1', {
197+
parseNumbers: true,
198+
types: {
199+
phoneNumber: 'string',
200+
}
201+
});
202+
//=> {phoneNumber: '+380951234567', id: 1}
203+
```
204+
205+
@example
206+
Parse `items` as an array of strings, overriding the `parseNumber` option:
207+
```
208+
import queryString from 'query-string';
209+
210+
queryString.parse('?age=20&items=1%2C2%2C3', {
211+
parseNumber: true,
212+
types: {
213+
items: 'string[]',
214+
}
215+
});
216+
//=> {age: 20, items: ['1', '2', '3']}
217+
```
218+
219+
@example
220+
Parse `age` as a number, even when `parseNumber` is false:
221+
```
222+
import queryString from 'query-string';
223+
224+
queryString.parse('?age=20&id=01234&zipcode=90210', {
225+
types: {
226+
age: 'number',
227+
}
228+
});
229+
//=> {age: 20, id: '01234', zipcode: '90210 }
230+
```
231+
232+
@example
233+
Parse `age` using a custom value parser:
234+
```
235+
import queryString from 'query-string';
236+
237+
queryString.parse('?age=20&id=01234&zipcode=90210', {
238+
types: {
239+
age: (value) => value * 2,
240+
}
241+
});
242+
//=> {age: 40, id: '01234', zipcode: '90210 }
243+
```
244+
245+
@example
246+
Array types will have no effect when `arrayFormat` is set to `none`
247+
```
248+
queryString.parse('ids=001%2C002%2C003&foods=apple%2Corange%2Cmango', {
249+
arrayFormat: 'none',
250+
types: {
251+
ids: 'number[]',
252+
foods: 'string[]',
253+
},
254+
}
255+
//=> {ids:'001,002,003', foods:'apple,orange,mango'}
256+
```
257+
258+
@example
259+
Parse a query utilizing all types:
260+
```
261+
import queryString from 'query-string';
262+
263+
queryString.parse('?ids=001%2C002%2C003&items=1%2C2%2C3&price=22%2E00&numbers=1%2C2%2C3&double=5&number=20', {
264+
arrayFormat: 'comma',
265+
types: {
266+
ids: 'string',
267+
items: 'string[]',
268+
price: 'string',
269+
numbers: 'number[]',
270+
double: (value) => value * 2,
271+
number: 'number',
272+
},
273+
});
274+
//=> {ids: '001,002,003', items: ['1', '2', '3'], price: '22.00', numbers: [1, 2, 3], double: 10, number: 20}
275+
```
276+
*/
277+
readonly types?: Record<
278+
string,
279+
'number' | 'string' | 'string[]' | 'number[]' | ((value: string) => unknown)
280+
>;
172281
};
173282

174283
// eslint-disable-next-line @typescript-eslint/ban-types

base.js

+26-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import decodeComponent from 'decode-uri-component';
2-
import splitOnFirst from 'split-on-first';
32
import {includeKeys} from 'filter-obj';
3+
import splitOnFirst from 'split-on-first';
44

55
const isNullOrUndefined = value => value === null || value === undefined;
66

@@ -300,11 +300,25 @@ function getHash(url) {
300300
return hash;
301301
}
302302

303-
function parseValue(value, options) {
303+
function parseValue(value, options, type) {
304+
if (type === 'string' && typeof value === 'string') {
305+
return value;
306+
}
307+
308+
if (typeof type === 'function' && typeof value === 'string') {
309+
return type(value);
310+
}
311+
312+
if (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
313+
return value.toLowerCase() === 'true';
314+
}
315+
316+
if (type === 'number' && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
317+
return Number(value);
318+
}
319+
304320
if (options.parseNumbers && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
305-
value = Number(value);
306-
} else if (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
307-
value = value.toLowerCase() === 'true';
321+
return Number(value);
308322
}
309323

310324
return value;
@@ -328,6 +342,7 @@ export function parse(query, options) {
328342
arrayFormatSeparator: ',',
329343
parseNumbers: false,
330344
parseBooleans: false,
345+
types: Object.create(null),
331346
...options,
332347
};
333348

@@ -368,12 +383,15 @@ export function parse(query, options) {
368383
}
369384

370385
for (const [key, value] of Object.entries(returnValue)) {
371-
if (typeof value === 'object' && value !== null) {
386+
if (typeof value === 'object' && value !== null && options.types[key] !== 'string') {
372387
for (const [key2, value2] of Object.entries(value)) {
373-
value[key2] = parseValue(value2, options);
388+
const type = options.types[key] ? options.types[key].replace('[]', '') : undefined;
389+
value[key2] = parseValue(value2, options, type);
374390
}
391+
} else if (typeof value === 'object' && value !== null && options.types[key] === 'string') {
392+
returnValue[key] = Object.values(value).join(options.arrayFormatSeparator);
375393
} else {
376-
returnValue[key] = parseValue(value, options);
394+
returnValue[key] = parseValue(value, options, options.types[key]);
377395
}
378396
}
379397

readme.md

+108
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,114 @@ queryString.parse('foo=true', {parseBooleans: true});
224224

225225
Parse the value as a boolean type instead of string type if it's a boolean.
226226

227+
##### types
228+
229+
Type: `object`\
230+
Default: `{}`
231+
232+
Specify a pre-defined schema to be used when parsing values. The types specified will take precedence over options such as: `parseNumber`, `parseBooleans`, and `arrayFormat`.
233+
234+
Use this feature to override the type of a value. This can be useful when the type is ambiguous such as a phone number.
235+
236+
It is possible to provide a custom function as the parameter type. The parameter's value will equal the function's return value.
237+
238+
Supported Types:
239+
240+
- `'string'`: Parse `phoneNumber` as a string (overriding the `parseNumber` option):
241+
242+
```js
243+
import queryString from 'query-string';
244+
245+
queryString.parse('?phoneNumber=%2B380951234567&id=1', {
246+
parseNumbers: true,
247+
types: {
248+
phoneNumber: 'string',
249+
}
250+
});
251+
//=> {phoneNumber: '+380951234567', id: 1}
252+
```
253+
254+
- `'number'`: Parse `age` as a number (even when `parseNumber` is false):
255+
256+
```js
257+
import queryString from 'query-string';
258+
259+
queryString.parse('?age=20&id=01234&zipcode=90210', {
260+
types: {
261+
age: 'number',
262+
}
263+
});
264+
//=> {age: 20, id: '01234', zipcode: '90210 }
265+
```
266+
267+
- `'string[]'`: Parse `items` as an array of strings (overriding the `parseNumber` option):
268+
269+
```js
270+
import queryString from 'query-string';
271+
272+
queryString.parse('?age=20&items=1%2C2%2C3', {
273+
parseNumber: true,
274+
types: {
275+
items: 'string[]',
276+
}
277+
});
278+
//=> {age: 20, items: ['1', '2', '3']}
279+
```
280+
281+
- `'number[]'`: Parse `items` as an array of numbers (even when `parseNumber` is false):
282+
283+
```js
284+
import queryString from 'query-string';
285+
286+
queryString.parse('?age=20&items=1%2C2%2C3', {
287+
types: {
288+
items: 'number[]',
289+
}
290+
});
291+
//=> {age: '20', items: [1, 2, 3]}
292+
```
293+
294+
- `'Function'`: Provide a custom function as the parameter type. The parameter's value will equal the function's return value.
295+
296+
```js
297+
import queryString from 'query-string';
298+
299+
queryString.parse('?age=20&id=01234&zipcode=90210', {
300+
types: {
301+
age: (value) => value * 2,
302+
}
303+
});
304+
//=> {age: 40, id: '01234', zipcode: '90210 }
305+
```
306+
307+
NOTE: Array types (`string[]` and `number[]`) will have no effect if `arrayFormat` is set to `none`.
308+
309+
```js
310+
queryString.parse('ids=001%2C002%2C003&foods=apple%2Corange%2Cmango', {
311+
arrayFormat: 'none',
312+
types: {
313+
ids: 'number[]',
314+
foods: 'string[]',
315+
},
316+
}
317+
//=> {ids:'001,002,003', foods:'apple,orange,mango'}
318+
```
319+
320+
###### Function
321+
322+
```js
323+
import queryString from 'query-string';
324+
325+
queryString.parse('?age=20&id=01234&zipcode=90210', {
326+
types: {
327+
age: (value) => value * 2,
328+
}
329+
});
330+
//=> {age: 40, id: '01234', zipcode: '90210 }
331+
```
332+
333+
Parse the value as a boolean type instead of string type if it's a boolean.
334+
227335
### .stringify(object, options?)
228336
229337
Stringify an object into a query string and sorting the keys.

0 commit comments

Comments
 (0)