From f093462ade844e27b6eb35fcf648357bda061d4d Mon Sep 17 00:00:00 2001 From: 43081j <43081j@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:58:36 +0100 Subject: [PATCH] feat: rework to use fast-querystring Uses a fork of fast-querystring instead of having to do two passes. --- .github/workflows/main.yml | 27 +++ .github/workflows/publish.yml | 47 ++++ LICENSE | 1 + README.md | 35 +++ bench/main.js | 17 +- package-lock.json | 30 ++- package.json | 13 +- src/parse.ts | 265 ++++++++++++++++++----- src/shared.ts | 48 +++- src/test/parse_test.ts | 148 +++++++++---- src/types/fast-decode-uri-component.d.ts | 3 + 11 files changed, 519 insertions(+), 115 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/publish.yml create mode 100644 README.md create mode 100644 src/types/fast-decode-uri-component.d.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..a3cafd4 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,27 @@ +name: Build & Test + +on: + push: + branches: [master] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Use Node v${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Install Dependencies + run: npm ci + - name: Lint + run: npm run lint + - name: Test + run: npm run test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..f9bd05e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,47 @@ +name: Publish Release (npm) + +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install Dependencies + run: npm ci + - name: Lint + run: npm run lint + - name: Test + run: npm test + + publish-npm: + needs: build + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22.x + registry-url: 'https://registry.npmjs.org' + cache: 'npm' + - run: npm ci + - run: npm version ${TAG_NAME} --git-tag-version=false + env: + TAG_NAME: ${{ github.ref_name }} + - run: npm publish --provenance --access public --tag next + if: "github.event.release.prerelease" + env: + NODE_AUTH_TOKEN: ${{ secrets.npm_token }} + - run: npm publish --provenance --access public + if: "!github.event.release.prerelease" + env: + NODE_AUTH_TOKEN: ${{ secrets.npm_token }} diff --git a/LICENSE b/LICENSE index a2728b1..24cb993 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2024 James Garbutt +Copyright (c) 2022 Yagiz Nizipli Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..2c29f96 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# nanoquery + +A lightweight query string parser/stringifier with support for nesting and some +configurability. + +Built on top of [fast-querystring](https://github.com/anonrig/fast-querystring). + +## Install + +```sh +npm i -S nanoquery +``` + +## Usage + +Parsing a query string: + +```ts +import {parse} from 'nanoquery'; + +parse('foo.bar=abc&baz=def'); + +/* + { + foo: { + bar: 'abc' + }, + baz: 'def' + } +*/ +``` + +## License + +MIT diff --git a/bench/main.js b/bench/main.js index 340df6f..56b467b 100644 --- a/bench/main.js +++ b/bench/main.js @@ -1,5 +1,6 @@ import {Bench} from 'tinybench'; import {parse} from '../lib/main.js'; +import {parse as fastParse} from 'fast-querystring'; const inputs = [ 'foo=123&bar=456', @@ -9,11 +10,17 @@ const inputs = [ ]; const bench = new Bench(); -bench.add('nanoquery (parse)', () => { - for (const input of inputs) { - parse(input); - } -}); +bench + .add('nanoquery (parse)', () => { + for (const input of inputs) { + parse(input); + } + }) + .add('fast-querystring (no nesting)', () => { + for (const input of inputs) { + fastParse(input); + } + }); await bench.warmup(); await bench.run(); diff --git a/package-lock.json b/package-lock.json index 1bbee7a..3129867 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,24 @@ { "name": "nanoquery", - "version": "0.0.1", + "version": "0.0.0-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoquery", - "version": "0.0.1", + "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "fast-querystring": "^1.1.2" + "dlv": "^1.1.3", + "dset": "^3.1.3", + "fast-decode-uri-component": "^1.0.1" }, "devDependencies": { "@eslint/js": "^9.1.1", + "@types/dlv": "^1.1.4", "@types/node": "^20.12.7", "c8": "^9.1.0", + "fast-querystring": "^1.1.2", "prettier": "^3.2.5", "tinybench": "^2.8.0", "typescript": "^5.4.5", @@ -199,6 +203,12 @@ "node": ">= 8" } }, + "node_modules/@types/dlv": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/dlv/-/dlv-1.1.4.tgz", + "integrity": "sha512-m8KmImw4Jt+4rIgupwfivrWEOnj1LzkmKkqbh075uG13eTQ1ZxHWT6T0vIdSQhLIjQCiR0n0lZdtyDOPO1x2Mw==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -701,6 +711,11 @@ "node": ">=8" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -714,6 +729,14 @@ "node": ">=6.0.0" } }, + "node_modules/dset": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.3.tgz", + "integrity": "sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -959,6 +982,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dev": true, "dependencies": { "fast-decode-uri-component": "^1.0.1" } diff --git a/package.json b/package.json index 56eb238..f510f9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoquery", - "version": "0.0.1", + "version": "0.0.0-dev", "type": "module", "description": "A small library for parsing and serialisation query strings", "main": "lib/main.js", @@ -13,7 +13,8 @@ "test": "npm run build && node --test lib/test/**/*_test.js", "lint": "eslint src", "format": "prettier --write src", - "bench": "node ./bench/main.js" + "bench": "node ./bench/main.js", + "prepare": "npm run build" }, "repository": { "type": "git", @@ -34,14 +35,18 @@ "homepage": "https://github.com/es-tooling/nanoquery#readme", "devDependencies": { "@eslint/js": "^9.1.1", + "@types/dlv": "^1.1.4", "@types/node": "^20.12.7", "c8": "^9.1.0", "prettier": "^3.2.5", "tinybench": "^2.8.0", "typescript": "^5.4.5", - "typescript-eslint": "^7.7.1" + "typescript-eslint": "^7.7.1", + "fast-querystring": "^1.1.2" }, "dependencies": { - "fast-querystring": "^1.1.2" + "dlv": "^1.1.3", + "dset": "^3.1.3", + "fast-decode-uri-component": "^1.0.1" } } diff --git a/src/parse.ts b/src/parse.ts index 1af254b..4513ec8 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -1,38 +1,17 @@ -import {type CONTINUE, type ArraySyntax} from './shared.js'; -import {parse as fastParse} from 'fast-querystring'; +import { + type ParseOptions, + type DeserializeKeyFunction, + type DeserializeValueFunction +} from './shared.js'; +import fastDecode from 'fast-decode-uri-component'; +import {dset} from 'dset'; +import dlv from 'dlv'; export type ParsedQuery = Record; - -export type DeserializeValueFunction = ( - key: string, - value: string -) => unknown | typeof CONTINUE; - -export type DeserializeKeyFunction = ( - key: string -) => PropertyKey | typeof CONTINUE; - -export interface ParseOptions { - // Enable parsing nested objects and arrays - // default: true - nested: boolean; - - // Array syntax - // default: "index" - arraySyntax: ArraySyntax; - - // Delimiter to split kv pairs by - // default: "&" - delimiter: string; - - // Custom deserializers - valueDeserializer: DeserializeValueFunction; - keyDeserializer: DeserializeKeyFunction; -} - export type UserParseOptions = Partial; +type DlvKey = string | (string | number)[]; -const defaultKeyDeserializer: DeserializeKeyFunction = (key) => { +export const numberKeyDeserializer: DeserializeKeyFunction = (key) => { const asNumber = Number(key); if (!Number.isNaN(asNumber)) { return asNumber; @@ -40,7 +19,10 @@ const defaultKeyDeserializer: DeserializeKeyFunction = (key) => { return key; }; -const defaultValueDeserializer: DeserializeValueFunction = (_key, value) => { +export const numberValueDeserializer: DeserializeValueFunction = ( + _key, + value +) => { const asNumber = Number(value); if (!Number.isNaN(asNumber)) { return asNumber; @@ -50,38 +32,201 @@ const defaultValueDeserializer: DeserializeValueFunction = (_key, value) => { const defaultOptions: ParseOptions = { nested: true, - arraySyntax: 'repeat', - delimiter: '&', - valueDeserializer: defaultValueDeserializer, - keyDeserializer: defaultKeyDeserializer + nestingSyntax: 'dot', + arrayRepeat: false, + arrayRepeatSyntax: 'repeat', + delimiter: 38 }; -export function parse( - input: string, - options?: Partial -): ParsedQuery { - const mergedOptions = {...defaultOptions, ...options}; - const getKey = mergedOptions.keyDeserializer; - const getValue = mergedOptions.valueDeserializer; - - const parsed: Record = fastParse(input); - const result: ParsedQuery = {}; - - for (const key in parsed) { - if (Object.prototype.hasOwnProperty.call(parsed, key)) { - const value = parsed[key]; - - if (Array.isArray(value)) { - result[getKey(key)] = value.map((v) => { - if (typeof v === 'string') { - return getValue('[]', v); +function splitByIndexPattern(input: string): string[] { + const result: string[] = []; + let buffer = ''; + + for (let i = 0; i < input.length; i++) { + const chr = input[i]; + const nextChr = input[i + 1]; + if (chr === '[' || chr === ']') { + result.push(buffer); + buffer = ''; + if (nextChr === '[') { + i++; + } + } else { + buffer += chr; + } + } + + if (buffer) { + result.push(buffer); + } + + return result; +} + +const regexPlus = /\+/g; +const Empty = function () {} as unknown as {new (): ParsedQuery}; +Empty.prototype = Object.create(null); + +/** + * Parses a query string into an object + * @param {string} input + * @param {ParseOptions=} options + */ +export function parse(input: string, options?: UserParseOptions): ParsedQuery { + const parseOptions: ParseOptions = {...defaultOptions, ...options}; + const charDelimiter = + typeof parseOptions.delimiter === 'string' + ? parseOptions.delimiter.charCodeAt(0) + : parseOptions.delimiter; + const { + valueDeserializer, + keyDeserializer, + arrayRepeatSyntax, + nested, + arrayRepeat, + nestingSyntax + } = parseOptions; + + // Optimization: Use new Empty() instead of Object.create(null) for performance + // v8 has a better optimization for initializing functions compared to Object + const result = new Empty(); + + if (typeof input !== 'string') { + return result; + } + + const inputLength = input.length; + let key = ''; + let value = ''; + let startingIndex = -1; + let equalityIndex = -1; + let shouldDecodeKey = false; + let shouldDecodeValue = false; + let keyHasPlus = false; + let valueHasPlus = false; + let hasBothKeyValuePair = false; + let c = 0; + + // Have a boundary of input.length + 1 to access last pair inside the loop. + for (let i = 0; i < inputLength + 1; i++) { + c = i !== inputLength ? input.charCodeAt(i) : charDelimiter; + + // Handle '&' and end of line to pass the current values to result + if (c === charDelimiter) { + hasBothKeyValuePair = equalityIndex > startingIndex; + + // Optimization: Reuse equality index to store the end of key + if (!hasBothKeyValuePair) { + equalityIndex = i; + } + + key = input.slice(startingIndex + 1, equalityIndex); + + // Add key/value pair only if the range size is greater than 1; a.k.a. contains at least "=" + if (hasBothKeyValuePair || key.length > 0) { + // Optimization: Replace '+' with space + if (keyHasPlus) { + key = key.replace(regexPlus, ' '); + } + + // Optimization: Do not decode if it's not necessary. + if (shouldDecodeKey) { + key = fastDecode(key) || key; + } + + if (hasBothKeyValuePair) { + value = input.slice(equalityIndex + 1, i); + + if (valueHasPlus) { + value = value.replace(regexPlus, ' '); + } + + if (shouldDecodeValue) { + value = fastDecode(value) || value; + } + } + + if ( + arrayRepeat && + arrayRepeatSyntax === 'bracket' && + key.endsWith('[]') + ) { + key = key.slice(0, -2); + } + + const newValue = valueDeserializer + ? valueDeserializer(key, value) + : value; + const newKey = keyDeserializer ? keyDeserializer(key) : key; + let dlvKey = (typeof newKey === 'string' ? newKey : [newKey]) as DlvKey; + + if (typeof dlvKey === 'string' && nested) { + if (nestingSyntax === 'index') { + dlvKey = splitByIndexPattern(dlvKey); + } else { + dlvKey = dlvKey.split('.'); + } + } + + const shouldDoNesting = + nested && (dlvKey as string[]).pop !== undefined; + const currentValue = shouldDoNesting + ? dlv(result, dlvKey) + : result[newKey]; + + if (currentValue === undefined || !arrayRepeat) { + if (shouldDoNesting) { + dset(result, dlvKey, newValue); + } else { + result[newKey] = newValue; + } + } else if (arrayRepeat) { + // Optimization: value.pop is faster than Array.isArray(value) + if ((currentValue as unknown[]).pop) { + (currentValue as unknown[]).push(newValue); + } else { + if (shouldDoNesting) { + dset(result, newKey as string, [currentValue, newValue]); + } else { + result[newKey] = [currentValue, newValue]; + } } - return v; - }); - } else if (typeof value === 'string') { - result[getKey(key)] = getValue(key, value); + } + } + + // Reset reading key value pairs + value = ''; + startingIndex = i; + equalityIndex = i; + shouldDecodeKey = false; + shouldDecodeValue = false; + keyHasPlus = false; + valueHasPlus = false; + } + // Check '=' + else if (c === 61) { + if (equalityIndex <= startingIndex) { + equalityIndex = i; + } + // If '=' character occurs again, we should decode the input. + else { + shouldDecodeValue = true; + } + } + // Check '+', and remember to replace it with empty space. + else if (c === 43) { + if (equalityIndex > startingIndex) { + valueHasPlus = true; + } else { + keyHasPlus = true; + } + } + // Check '%' character for encoding + else if (c === 37) { + if (equalityIndex > startingIndex) { + shouldDecodeValue = true; } else { - result[getKey(key)] = value; + shouldDecodeKey = true; } } } diff --git a/src/shared.ts b/src/shared.ts index 6c19f72..276f574 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -1,14 +1,52 @@ -export type ArraySyntax = - // `foo[0]=a&foo[1]=b` - | 'index' - // same as `index` but `foo[0]=a&foo[2]=b` would be ['a', undefined, 'b'] - | 'index-sparse' +export type ArrayRepeatSyntax = // `foo[]=a&foo[]=b` | 'bracket' // `foo=a&foo=b` | 'repeat'; +export type NestingSyntax = + // `foo.bar` + | 'dot' + // `foo[bar]` + | 'index'; + // This is a special return value for deserializers and serializers, to tell the library // to fall back to using the default (de)serialize function. // We can't just return `null` or `undefined` etc, because we may want to deserialize to that export const CONTINUE = Symbol('continue'); + +export type DeserializeValueFunction = ( + key: string, + value: string +) => unknown | typeof CONTINUE; + +export type DeserializeKeyFunction = ( + key: string +) => PropertyKey | typeof CONTINUE; + +export interface ParseOptions { + // Enable parsing nested objects and arrays + // default: true + nested: boolean; + + // Nesting syntax + // default: "index" + nestingSyntax: NestingSyntax; + + // Whether repeated keys should result in arrays + // e.g. `foo=1&foo=2` would become `foo: [1, 2]` + // default: false + arrayRepeat: boolean; + + // Array syntax + // default: "repeat" + arrayRepeatSyntax: ArrayRepeatSyntax; + + // Delimiter to split kv pairs by + // default: "&" + delimiter: string | number; + + // Custom deserializers + valueDeserializer?: DeserializeValueFunction; + keyDeserializer?: DeserializeKeyFunction; +} diff --git a/src/test/parse_test.ts b/src/test/parse_test.ts index 8462756..24b714c 100644 --- a/src/test/parse_test.ts +++ b/src/test/parse_test.ts @@ -1,6 +1,11 @@ import * as assert from 'node:assert/strict'; import {test} from 'node:test'; -import {parse, type UserParseOptions} from '../parse.js'; +import { + parse, + type UserParseOptions, + numberKeyDeserializer, + numberValueDeserializer +} from '../parse.js'; type TestCase = { input: string; @@ -8,89 +13,156 @@ type TestCase = { options?: UserParseOptions; }; +const createSparseArray = (arr: T[]): T[] => { + const newArr = Array(arr.length); + for (let i = 0; i < arr.length; i++) { + const item = arr[i]; + if (item === undefined) { + continue; + } + newArr[i] = item; + } + return newArr; +}; + const testCases: TestCase[] = [ // Defaults { - input: 'foo=1&bar=2', - output: {foo: 1, bar: 2} + input: 'foo=x&bar=y', + output: {foo: 'x', bar: 'y'} }, { - input: 'foo=1&foo=2', - output: {foo: [1, 2]} + input: 'foo=x&foo=y', + output: {foo: 'y'} }, { - input: 'foo[bar]=123', - output: {foo: {bar: 123}} + input: 'foo.bar=x', + output: {foo: {bar: 'x'}} }, { - input: 'foo[]=1', - output: {'foo[]': 1} + input: 'foo[bar]=x', + output: {'foo[bar]': 'x'} }, { - input: 'foo[0]=1', - output: {'foo[0]': 1} + input: 'foo[]=x', + output: {'foo[]': 'x'} }, { - input: 'foo=1;bar=2', - output: {foo: '1;bar=2'} + input: 'foo[0]=x', + output: {'foo[0]': 'x'} + }, + { + input: 'foo=x;bar=y', + output: {foo: 'x;bar=y'} + }, + + // Number deserializers + { + input: '303=foo', + output: {'303': 'foo'} }, { input: '303=foo', - output: {303: 'foo'} + output: {303: 'foo'}, + options: {keyDeserializer: numberKeyDeserializer} + }, + { + input: 'foo=303', + output: {foo: 303}, + options: {valueDeserializer: numberValueDeserializer} + }, + { + input: 'foo.0=1&foo.1=2', + output: {foo: [1, 2]}, + options: {valueDeserializer: numberValueDeserializer} }, // Array syntax: bracket { - input: 'foo[]=1&foo[]=2', - output: {foo: [1, 2]}, - options: {arraySyntax: 'bracket'} + input: 'foo[]=x&foo[]=y', + output: {foo: ['x', 'y']}, + options: {arrayRepeat: true, arrayRepeatSyntax: 'bracket'} }, - // Array syntax: repeat (DEFAULT) + // Array syntax: repeat { - input: 'foo=1&foo=2', - output: {foo: [1, 2]}, - options: {arraySyntax: 'repeat'} + input: 'foo=x&foo=y', + output: {foo: ['x', 'y']}, + options: {arrayRepeat: true, arrayRepeatSyntax: 'repeat'} }, - // Array syntax: index + + // Nesting syntax: index { - input: 'foo[0]=1&foo[1]=2', - output: {foo: [1, 2]}, - options: {arraySyntax: 'index'} + input: 'foo[0]=x&foo[1]=y', + output: {foo: ['x', 'y']}, + options: {nested: true, nestingSyntax: 'index'} }, { - input: 'foo[0]=1&foo[2]=2', - output: {foo: [1, 2]}, - options: {arraySyntax: 'index'} + input: 'foo[0]=x&foo[1]=y', + output: {'foo[0]': 'x', 'foo[1]': 'y'}, + options: {nested: true, nestingSyntax: 'dot'} }, - // Array syntax: index-sparse { - input: 'foo[0]=1&foo[2]=2', - output: {foo: [1, undefined, 2]}, - options: {arraySyntax: 'index-sparse'} + input: 'foo[bar]=x&foo[baz]=y', + output: {foo: {bar: 'x', baz: 'y'}}, + options: {nested: true, nestingSyntax: 'index'} + }, + { + input: 'foo[bar]=x&foo[baz]=y', + output: {'foo[bar]': 'x', 'foo[baz]': 'y'}, + options: {nested: true, nestingSyntax: 'dot'} + }, + + // Nesting syntax: dot + { + input: 'foo.0=x&foo.1=y', + output: {foo: ['x', 'y']}, + options: {nested: true, nestingSyntax: 'dot'} + }, + { + input: 'foo.0=x&foo.1=y', + output: {'foo.0': 'x', 'foo.1': 'y'}, + options: {nested: true, nestingSyntax: 'index'} + }, + { + input: 'foo.bar=x&foo.baz=y', + output: {foo: {bar: 'x', baz: 'y'}}, + options: {nested: true, nestingSyntax: 'dot'} + }, + { + input: 'foo.bar=x&foo.baz=y', + output: {'foo.bar': 'x', 'foo.baz': 'y'}, + options: {nested: true, nestingSyntax: 'index'} + }, + + // Sparse array with nestinh + { + input: 'foo[0]=x&foo[2]=y', + output: {foo: createSparseArray(['x', undefined, 'y'])}, + options: {nested: true, nestingSyntax: 'index'} }, // Delimiter: ; { - input: 'foo=1;bar=2', - output: {foo: 1, bar: 2}, + input: 'foo=x;bar=y', + output: {foo: 'x', bar: 'y'}, options: {delimiter: ';'} }, // Nested: false { - input: 'foo[bar]=123', - output: {'foo[bar]': 123}, + input: 'foo[bar]=x', + output: {'foo[bar]': 'x'}, options: {nested: false} }, // With a key deserializer { input: 'three=foo&four=bar', - output: {3: 'foo', four: 'bar'}, + output: {trzy: 'foo', four: 'bar'}, options: { keyDeserializer: (key) => { if (key === 'three') { - return 3; + return 'trzy'; } return key; } diff --git a/src/types/fast-decode-uri-component.d.ts b/src/types/fast-decode-uri-component.d.ts new file mode 100644 index 0000000..3331a74 --- /dev/null +++ b/src/types/fast-decode-uri-component.d.ts @@ -0,0 +1,3 @@ +declare module 'fast-decode-uri-component' { + export default function decode(val: string): string | null; +}