Skip to content
Merged
86 changes: 86 additions & 0 deletions base.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,92 @@ export type ParseOptions = {
```
*/
readonly parseFragmentIdentifier?: boolean;

/**
Specify a pre-defined schema to be used when parsing values. The types specified will take precedence over global parameters such as: `parseNumber`, `parseBooleans`, and `arrayFormat`.

Use this feature to override the type for a value. This can be useful when the type is ambiguous such as a phone number (see example 1 and 2).

Types specified here will be used even when global parsing options such as `parseNumber`, and `arrayFormat` are not enabled (see example 3).

NOTE: array types (`string[]` and `number[]`) will not work if `arrayFormat` is set to `none`.

@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
Parse a query utilizing all types:
```
import queryString from 'query-string';

queryString.parse("ids=001%2C002%2C003&items=1%2C2%2C3&price=22%2E00&nums=1%2C2%2C3&double=5&number=20", {
arrayFormat: "comma",
types: {
ids: "string",
items: "string[]",
price: "string",
nums: "number[]",
double: (value) => value * 2,
number: "number",
},
});
//=> {ids: '001,002,003', items: ['1', '2', '3'], price: '22.00', nums: [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
22 changes: 17 additions & 5 deletions base.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,16 @@ function getHash(url) {
return hash;
}

function parseValue(value, options) {
if (options.parseNumbers && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
function parseValue(value, options, type) {
if (type === 'string' && (typeof value === 'string')) {
return value;
}

if (typeof type === 'function' && (typeof value === 'string')) {
value = type(value);
} else if (type === 'number' && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
value = Number(value);
} else 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';
Expand All @@ -328,6 +336,7 @@ export function parse(query, options) {
arrayFormatSeparator: ',',
parseNumbers: false,
parseBooleans: false,
types: Object.create(null),
...options,
};

Expand Down Expand Up @@ -368,12 +377,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
114 changes: 114 additions & 0 deletions test/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,117 @@ test('query strings having (:list) colon-list-separator arrays', t => {
test('query strings having (:list) colon-list-separator arrays including null values', t => {
t.deepEqual(queryString.parse('bar:list=one&bar:list=two&foo', {arrayFormat: 'colon-list-separator'}), {bar: ['one', 'two'], foo: null});
});

test('types option: can override a parsed number to be a string ', t => {
t.deepEqual(queryString.parse('phoneNumber=%2B380951234567', {
parseNumbers: true,
types: {
phoneNumber: 'string',
},
}), {phoneNumber: '+380951234567'});
});

test('types option: can override a parsed boolean value to be a string', t => {
t.deepEqual(queryString.parse('question=true', {
parseBooleans: true,
types: {
question: 'string',
},
}), {
question: 'true',
});
});

test('types option: can override parsed numbers arrays to be string[]', t => {
t.deepEqual(queryString.parse('ids=999%2C998%2C997&items=1%2C2%2C3', {
arrayFormat: 'comma',
parseNumbers: true,
types: {
ids: 'string[]',
},
}), {
ids: ['999', '998', '997'],
items: [1, 2, 3],
});
});

test('types option: can override string arrays to be number[]', t => {
t.deepEqual(queryString.parse('ids=001%2C002%2C003&items=1%2C2%2C3', {
arrayFormat: 'comma',
types: {
ids: 'number[]',
},
}), {
ids: [1, 2, 3],
items: ['1', '2', '3'],
});
});

test('types option: can override an array to be string', t => {
t.deepEqual(queryString.parse('ids=001%2C002%2C003&items=1%2C2%2C3', {
arrayFormat: 'comma',
parseNumbers: true,
types: {
ids: 'string',
},
}), {
ids: '001,002,003',
items: [1, 2, 3],
});
});

test('types option: can override a separator array to be string ', t => {
t.deepEqual(queryString.parse('ids=001|002|003&items=1|2|3', {
arrayFormat: 'separator',
arrayFormatSeparator: '|',
parseNumbers: true,
types: {
ids: 'string',
},
}), {
ids: '001|002|003',
items: [1, 2, 3],
});
});

test('types option: when value is not of specified type, it will safely parse the value as string', t => {
t.deepEqual(queryString.parse('id=example', {
types: {
id: 'number',
},
}), {
id: 'example',
});
});

test('types option: will parse the value as number if specified in type but parseNumbers is false', t => {
t.deepEqual(queryString.parse('id=123', {
arrayFormat: 'comma',
types: {
id: 'number',
},
}), {
id: 123,
});
});

test('types option: all supported types work in conjunction with one another', t => {
t.deepEqual(queryString.parse('ids=001%2C002%2C003&items=1%2C2%2C3&price=22%2E00&nums=1%2C2%2C3&double=5&number=20', {
arrayFormat: 'comma',
types: {
ids: 'string',
items: 'string[]',
price: 'string',
nums: 'number[]',
double: value => value * 2,
number: 'number',
},
}), {
ids: '001,002,003',
items: ['1', '2', '3'],
price: '22.00',
nums: [1, 2, 3],
double: 10,
number: 20,
});
});