Skip to content

Commit

Permalink
Add types option to the .parse() method (#385)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
scottenock and sindresorhus authored Jul 22, 2024
1 parent 3d8fbf2 commit 672eb82
Show file tree
Hide file tree
Showing 4 changed files with 371 additions and 9 deletions.
111 changes: 110 additions & 1 deletion base.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,14 @@ export type ParseOptions = {
//=> {foo: ['1', '2', '3']}
```
*/
readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'bracket-separator' | 'colon-list-separator' | 'none';
readonly arrayFormat?:
| 'bracket'
| 'index'
| 'comma'
| 'separator'
| 'bracket-separator'
| 'colon-list-separator'
| 'none';

/**
The character used to separate array elements when using `{arrayFormat: 'separator'}`.
Expand Down Expand Up @@ -169,6 +176,108 @@ export type ParseOptions = {
```
*/
readonly parseFragmentIdentifier?: boolean;

/**
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`.
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).
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).
NOTE: Array types (`string[]` and `number[]`) will have no effect if `arrayFormat` is set to `none` (see example 5).
@default {}
@example
Parse `phoneNumber` as a string, overriding the `parseNumber` option:
```
import queryString from 'query-string';
queryString.parse('?phoneNumber=%2B380951234567&id=1', {
parseNumbers: true,
types: {
phoneNumber: 'string',
}
});
//=> {phoneNumber: '+380951234567', id: 1}
```
@example
Parse `items` as an array of strings, overriding the `parseNumber` option:
```
import queryString from 'query-string';
queryString.parse('?age=20&items=1%2C2%2C3', {
parseNumber: true,
types: {
items: 'string[]',
}
});
//=> {age: 20, items: ['1', '2', '3']}
```
@example
Parse `age` as a number, even when `parseNumber` is false:
```
import queryString from 'query-string';
queryString.parse('?age=20&id=01234&zipcode=90210', {
types: {
age: 'number',
}
});
//=> {age: 20, id: '01234', zipcode: '90210 }
```
@example
Parse `age` using a custom value parser:
```
import queryString from 'query-string';
queryString.parse('?age=20&id=01234&zipcode=90210', {
types: {
age: (value) => value * 2,
}
});
//=> {age: 40, id: '01234', zipcode: '90210 }
```
@example
Array types will have no effect when `arrayFormat` is set to `none`
```
queryString.parse('ids=001%2C002%2C003&foods=apple%2Corange%2Cmango', {
arrayFormat: 'none',
types: {
ids: 'number[]',
foods: 'string[]',
},
}
//=> {ids:'001,002,003', foods:'apple,orange,mango'}
```
@example
Parse a query utilizing all types:
```
import queryString from 'query-string';
queryString.parse('?ids=001%2C002%2C003&items=1%2C2%2C3&price=22%2E00&numbers=1%2C2%2C3&double=5&number=20', {
arrayFormat: 'comma',
types: {
ids: 'string',
items: 'string[]',
price: 'string',
numbers: 'number[]',
double: (value) => value * 2,
number: 'number',
},
});
//=> {ids: '001,002,003', items: ['1', '2', '3'], price: '22.00', numbers: [1, 2, 3], double: 10, number: 20}
```
*/
readonly types?: Record<
string,
'number' | 'string' | 'string[]' | 'number[]' | ((value: string) => unknown)
>;
};

// eslint-disable-next-line @typescript-eslint/ban-types
Expand Down
34 changes: 26 additions & 8 deletions base.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import decodeComponent from 'decode-uri-component';
import splitOnFirst from 'split-on-first';
import {includeKeys} from 'filter-obj';
import splitOnFirst from 'split-on-first';

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

Expand Down Expand Up @@ -300,11 +300,25 @@ function getHash(url) {
return hash;
}

function parseValue(value, options) {
function parseValue(value, options, type) {
if (type === 'string' && typeof value === 'string') {
return value;
}

if (typeof type === 'function' && typeof value === 'string') {
return type(value);
}

if (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
return value.toLowerCase() === 'true';
}

if (type === 'number' && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
return Number(value);
}

if (options.parseNumbers && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
value = Number(value);
} else if (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
value = value.toLowerCase() === 'true';
return Number(value);
}

return value;
Expand All @@ -328,6 +342,7 @@ export function parse(query, options) {
arrayFormatSeparator: ',',
parseNumbers: false,
parseBooleans: false,
types: Object.create(null),
...options,
};

Expand Down Expand Up @@ -368,12 +383,15 @@ export function parse(query, options) {
}

for (const [key, value] of Object.entries(returnValue)) {
if (typeof value === 'object' && value !== null) {
if (typeof value === 'object' && value !== null && options.types[key] !== 'string') {
for (const [key2, value2] of Object.entries(value)) {
value[key2] = parseValue(value2, options);
const type = options.types[key] ? options.types[key].replace('[]', '') : undefined;
value[key2] = parseValue(value2, options, type);
}
} else if (typeof value === 'object' && value !== null && options.types[key] === 'string') {
returnValue[key] = Object.values(value).join(options.arrayFormatSeparator);
} else {
returnValue[key] = parseValue(value, options);
returnValue[key] = parseValue(value, options, options.types[key]);
}
}

Expand Down
108 changes: 108 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,114 @@ queryString.parse('foo=true', {parseBooleans: true});

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

##### types

Type: `object`\
Default: `{}`

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`.

Use this feature to override the type of a value. This can be useful when the type is ambiguous such as a phone number.

It is possible to provide a custom function as the parameter type. The parameter's value will equal the function's return value.

Supported Types:

- `'string'`: Parse `phoneNumber` as a string (overriding the `parseNumber` option):

```js
import queryString from 'query-string';

queryString.parse('?phoneNumber=%2B380951234567&id=1', {
parseNumbers: true,
types: {
phoneNumber: 'string',
}
});
//=> {phoneNumber: '+380951234567', id: 1}
```

- `'number'`: Parse `age` as a number (even when `parseNumber` is false):

```js
import queryString from 'query-string';

queryString.parse('?age=20&id=01234&zipcode=90210', {
types: {
age: 'number',
}
});
//=> {age: 20, id: '01234', zipcode: '90210 }
```

- `'string[]'`: Parse `items` as an array of strings (overriding the `parseNumber` option):

```js
import queryString from 'query-string';

queryString.parse('?age=20&items=1%2C2%2C3', {
parseNumber: true,
types: {
items: 'string[]',
}
});
//=> {age: 20, items: ['1', '2', '3']}
```

- `'number[]'`: Parse `items` as an array of numbers (even when `parseNumber` is false):

```js
import queryString from 'query-string';

queryString.parse('?age=20&items=1%2C2%2C3', {
types: {
items: 'number[]',
}
});
//=> {age: '20', items: [1, 2, 3]}
```

- `'Function'`: Provide a custom function as the parameter type. The parameter's value will equal the function's return value.

```js
import queryString from 'query-string';

queryString.parse('?age=20&id=01234&zipcode=90210', {
types: {
age: (value) => value * 2,
}
});
//=> {age: 40, id: '01234', zipcode: '90210 }
```

NOTE: Array types (`string[]` and `number[]`) will have no effect if `arrayFormat` is set to `none`.

```js
queryString.parse('ids=001%2C002%2C003&foods=apple%2Corange%2Cmango', {
arrayFormat: 'none',
types: {
ids: 'number[]',
foods: 'string[]',
},
}
//=> {ids:'001,002,003', foods:'apple,orange,mango'}
```
###### Function
```js
import queryString from 'query-string';

queryString.parse('?age=20&id=01234&zipcode=90210', {
types: {
age: (value) => value * 2,
}
});
//=> {age: 40, id: '01234', zipcode: '90210 }
```
Parse the value as a boolean type instead of string type if it's a boolean.
### .stringify(object, options?)
Stringify an object into a query string and sorting the keys.
Expand Down
Loading

0 comments on commit 672eb82

Please sign in to comment.